Skip to content

Commit 953356f

Browse files
authored
Merge pull request #23 from takker99/inject-socket
WebSocketを使い回す
2 parents d4adcd7 + bf154db commit 953356f

File tree

9 files changed

+353
-256
lines changed

9 files changed

+353
-256
lines changed

browser/websocket/deletePage.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Socket, socketIO, wrap } from "../../deps/socket.ts";
2+
import { connect, disconnect } from "./socket.ts";
3+
import { getProjectId, getUserId } from "./id.ts";
4+
import { pull } from "./pull.ts";
5+
import { pushWithRetry } from "./_fetch.ts";
6+
7+
export interface DeletePageOptions {
8+
socket?: Socket;
9+
}
10+
11+
/** 指定したページを削除する
12+
*
13+
* @param project 削除したいページのproject
14+
* @param title 削除したいページのタイトル
15+
* @param options 使用したいSocketがあれば指定する
16+
*/
17+
export async function deletePage(
18+
project: string,
19+
title: string,
20+
options?: DeletePageOptions,
21+
): Promise<void> {
22+
const [
23+
{ pageId, commitId: parentId, persistent },
24+
projectId,
25+
userId,
26+
] = await Promise.all([
27+
pull(project, title),
28+
getProjectId(project),
29+
getUserId(),
30+
]);
31+
32+
if (!persistent) return;
33+
34+
const injectedSocket = options?.socket;
35+
const socket = injectedSocket ?? await socketIO();
36+
await connect(socket);
37+
const { request } = wrap(socket);
38+
try {
39+
await pushWithRetry(request, [{ deleted: true }], {
40+
projectId,
41+
pageId,
42+
parentId,
43+
userId,
44+
project,
45+
title,
46+
});
47+
} finally {
48+
if (!injectedSocket) await disconnect(socket);
49+
}
50+
}

browser/websocket/listen.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
ProjectUpdatesStreamCommit,
3+
ProjectUpdatesStreamEvent,
4+
Socket,
5+
socketIO,
6+
wrap,
7+
} from "../../deps/socket.ts";
8+
import { connect, disconnect } from "./socket.ts";
9+
import { getProjectId } from "./id.ts";
10+
export type {
11+
ProjectUpdatesStreamCommit,
12+
ProjectUpdatesStreamEvent,
13+
} from "../../deps/socket.ts";
14+
15+
export interface ListenStreamOptions {
16+
socket?: Socket;
17+
}
18+
19+
/** Streamを購読する
20+
*
21+
* @param project 購読したいproject
22+
* @param events 購読したいevent。配列で指定する
23+
* @param options 使用したいSocketがあれば指定する
24+
*/
25+
export async function* listenStream(
26+
project: string,
27+
events: ["commit" | "event", ...("commit" | "event")[]],
28+
options?: ListenStreamOptions,
29+
): AsyncGenerator<
30+
ProjectUpdatesStreamEvent | ProjectUpdatesStreamCommit,
31+
void,
32+
unknown
33+
> {
34+
const projectId = await getProjectId(project);
35+
36+
const injectedSocket = options?.socket;
37+
const socket = injectedSocket ?? await socketIO();
38+
await connect(socket);
39+
const { request, response } = wrap(socket);
40+
41+
try {
42+
// 部屋に入って購読し始める
43+
await request("socket.io-request", {
44+
method: "room:join",
45+
data: { projectId, pageId: null, projectUpdatesStream: true },
46+
});
47+
48+
yield* response(
49+
...events.map((event) => `projectUpdatesStream:${event}` as const),
50+
);
51+
} finally {
52+
if (!injectedSocket) await disconnect(socket);
53+
}
54+
}

browser/websocket/mod.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from "./room.ts";
2-
export * from "./shortcuts.ts";
3-
export * from "./stream.ts";
2+
export * from "./patch.ts";
3+
export * from "./deletePage.ts";
4+
export * from "./pin.ts";
5+
export * from "./listen.ts";

browser/websocket/patch.ts

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Socket, socketIO, wrap } from "../../deps/socket.ts";
2+
import { connect, disconnect } from "./socket.ts";
3+
import { getProjectId, getUserId } from "./id.ts";
4+
import { makeChanges } from "./makeChanges.ts";
5+
import { HeadData, pull } from "./pull.ts";
6+
import type { Line } from "../../deps/scrapbox.ts";
7+
import { pushCommit, pushWithRetry } from "./_fetch.ts";
8+
9+
export interface PatchOptions {
10+
socket?: Socket;
11+
}
12+
13+
/** ページ全体を書き換える
14+
*
15+
* serverには書き換え前後の差分だけを送信する
16+
*
17+
* @param project 書き換えたいページのproject
18+
* @param title 書き換えたいページのタイトル
19+
* @param update 書き換え後の本文を作成する函数。引数には現在の本文が渡される。空配列を返すとページが削除される。undefinedを返すと書き換えを中断する
20+
* @param options 使用したいSocketがあれば指定する
21+
*/
22+
export async function patch(
23+
project: string,
24+
title: string,
25+
update: (
26+
lines: Line[],
27+
metadata: HeadData,
28+
) => string[] | undefined | Promise<string[] | undefined>,
29+
options?: PatchOptions,
30+
): Promise<void> {
31+
const [
32+
head_,
33+
projectId,
34+
userId,
35+
] = await Promise.all([
36+
pull(project, title),
37+
getProjectId(project),
38+
getUserId(),
39+
]);
40+
41+
let head = head_;
42+
43+
const injectedSocket = options?.socket;
44+
const socket = injectedSocket ?? await socketIO();
45+
await connect(socket);
46+
try {
47+
const { request } = wrap(socket);
48+
49+
// 3回retryする
50+
for (let i = 0; i < 3; i++) {
51+
try {
52+
const pending = update(head.lines, head);
53+
const newLines = pending instanceof Promise ? await pending : pending;
54+
55+
if (!newLines) return;
56+
57+
if (newLines.length === 0) {
58+
await pushWithRetry(request, [{ deleted: true }], {
59+
projectId,
60+
pageId: head.pageId,
61+
parentId: head.commitId,
62+
userId,
63+
project,
64+
title,
65+
});
66+
}
67+
68+
const changes = makeChanges(head.lines, newLines, { userId, head });
69+
await pushCommit(request, changes, {
70+
parentId: head.commitId,
71+
projectId,
72+
pageId: head.pageId,
73+
userId,
74+
});
75+
break;
76+
} catch (_e: unknown) {
77+
if (i === 2) {
78+
throw Error("Faild to retry pushing.");
79+
}
80+
console.log(
81+
"Faild to push a commit. Retry after pulling new commits",
82+
);
83+
try {
84+
head = await pull(project, title);
85+
} catch (e: unknown) {
86+
throw e;
87+
}
88+
}
89+
}
90+
} finally {
91+
if (!injectedSocket) await disconnect(socket);
92+
}
93+
}

browser/websocket/pin.ts

+113
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,115 @@
1+
import { Socket, socketIO, wrap } from "../../deps/socket.ts";
2+
import { connect, disconnect } from "./socket.ts";
3+
import { getProjectId, getUserId } from "./id.ts";
4+
import { pull } from "./pull.ts";
5+
import { pushWithRetry } from "./_fetch.ts";
6+
7+
export interface PinOptions {
8+
/** ピン留め対象のページが存在しないときの振る舞いを変えるoption
9+
*
10+
* -`true`: タイトルのみのページを作成してピン留めする
11+
* - `false`: ピン留めしない
12+
*
13+
* @default false
14+
*/
15+
create?: boolean;
16+
socket?: Socket;
17+
}
18+
/** 指定したページをピン留めする
19+
*
20+
* @param project ピン留めしたいページのproject
21+
* @param title ピン留めしたいページのタイトル
22+
* @param options 使用したいSocketがあれば指定する
23+
*/
24+
export async function pin(
25+
project: string,
26+
title: string,
27+
options?: PinOptions,
28+
): Promise<void> {
29+
const [
30+
head,
31+
projectId,
32+
userId,
33+
] = await Promise.all([
34+
pull(project, title),
35+
getProjectId(project),
36+
getUserId(),
37+
]);
38+
39+
// 既にピン留めされている場合は何もしない
40+
if (head.pin > 0 || (!head.persistent && !(options?.create ?? false))) return;
41+
42+
const init = {
43+
parentId: head.commitId,
44+
projectId,
45+
pageId: head.pageId,
46+
userId,
47+
project,
48+
title,
49+
};
50+
const injectedSocket = options?.socket;
51+
const socket = injectedSocket ?? await socketIO();
52+
await connect(socket);
53+
const { request } = wrap(socket);
54+
55+
// タイトルのみのページを作る
56+
if (!head.persistent) {
57+
const commitId = await pushWithRetry(request, [{ title }], init);
58+
init.parentId = commitId;
59+
}
60+
61+
try {
62+
await pushWithRetry(request, [{ pin: pinNumber() }], init);
63+
} finally {
64+
if (!injectedSocket) await disconnect(socket);
65+
}
66+
}
67+
68+
export interface UnPinOptions {
69+
socket?: Socket;
70+
}
71+
/** 指定したページのピン留めを外す
72+
*
73+
* @param project ピン留めを外したいページのproject
74+
* @param title ピン留めを外したいページのタイトル
75+
*/
76+
export async function unpin(
77+
project: string,
78+
title: string,
79+
options: UnPinOptions,
80+
): Promise<void> {
81+
const [
82+
head,
83+
projectId,
84+
userId,
85+
] = await Promise.all([
86+
pull(project, title),
87+
getProjectId(project),
88+
getUserId(),
89+
]);
90+
91+
// 既にピンが外れているか、そもそも存在しないページの場合は何もしない
92+
if (head.pin == 0 || !head.persistent) return;
93+
94+
const init = {
95+
parentId: head.commitId,
96+
projectId,
97+
pageId: head.pageId,
98+
userId,
99+
project,
100+
title,
101+
};
102+
const injectedSocket = options?.socket;
103+
const socket = injectedSocket ?? await socketIO();
104+
await connect(socket);
105+
const { request } = wrap(socket);
106+
107+
try {
108+
await pushWithRetry(request, [{ pin: 0 }], init);
109+
} finally {
110+
if (!injectedSocket) await disconnect(socket);
111+
}
112+
}
113+
1114
export const pinNumber = (): number =>
2115
Number.MAX_SAFE_INTEGER - Math.floor(Date.now() / 1000);

0 commit comments

Comments
 (0)