From 9bc8667987828df196c9e40a403ad5df928f9545 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Thu, 3 Mar 2022 15:54:46 +0900 Subject: [PATCH 1/7] =?UTF-8?q?:sparkles:=20Socket.IO=E3=81=AEwrapper?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser/websocket/socket.ts | 38 +++++++++++++++++++++++++++++++++++++ deps/socket.ts | 1 + 2 files changed, 39 insertions(+) create mode 100644 browser/websocket/socket.ts diff --git a/browser/websocket/socket.ts b/browser/websocket/socket.ts new file mode 100644 index 0000000..82c9f00 --- /dev/null +++ b/browser/websocket/socket.ts @@ -0,0 +1,38 @@ +import { Socket, socketIO } from "../../deps/socket.ts"; +export type { Socket } from "../../deps/socket.ts"; + +/** 新しいsocketを作る */ +export function makeSocket() { + return socketIO(); +} + +/** websocketに(再)接続する + * + * @param socket 接続したいsocket + */ +export async function connect(socket: Socket): Promise { + if (socket.connected) return; + socket.connect(); + + return await new Promise((resolve) => + socket.once("connect", () => resolve()) + ); +} + +/** websocketを切断する + * + * @param socket 切断したいsocket + */ +export async function disconnect(socket: Socket): Promise { + if (socket.disconnected) return; + socket.disconnect(); + + return await new Promise((resolve) => { + const onDisconnect = (reason: Socket.DisconnectReason) => { + if (reason !== "io client disconnect") return; + resolve(); + socket.off("disconnect", onDisconnect); + }; + socket.on("disconnect", onDisconnect); + }); +} diff --git a/deps/socket.ts b/deps/socket.ts index e9e8cf3..2bd0b14 100644 --- a/deps/socket.ts +++ b/deps/socket.ts @@ -8,6 +8,7 @@ export type { Pin, ProjectUpdatesStreamCommit, ProjectUpdatesStreamEvent, + Socket, UpdateCommit, } from "https://raw.githubusercontent.com/takker99/scrapbox-userscript-websocket/0.1.4/mod.ts"; export { From 9e981a7b2f277f08afe9b8f340226e8f5be44891 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:07:53 +0900 Subject: [PATCH 2/7] :sparkles: Enable to run `patch()` with an existing socket --- browser/websocket/mod.ts | 1 + browser/websocket/patch.ts | 93 ++++++++++++++++++++++++++++++++++ browser/websocket/shortcuts.ts | 84 +----------------------------- 3 files changed, 96 insertions(+), 82 deletions(-) create mode 100644 browser/websocket/patch.ts diff --git a/browser/websocket/mod.ts b/browser/websocket/mod.ts index 7959b96..59728d4 100644 --- a/browser/websocket/mod.ts +++ b/browser/websocket/mod.ts @@ -1,3 +1,4 @@ export * from "./room.ts"; export * from "./shortcuts.ts"; +export * from "./patch.ts"; export * from "./stream.ts"; diff --git a/browser/websocket/patch.ts b/browser/websocket/patch.ts new file mode 100644 index 0000000..cde51c4 --- /dev/null +++ b/browser/websocket/patch.ts @@ -0,0 +1,93 @@ +import { Socket, socketIO, wrap } from "../../deps/socket.ts"; +import { connect, disconnect } from "./socket.ts"; +import { getProjectId, getUserId } from "./id.ts"; +import { makeChanges } from "./makeChanges.ts"; +import { HeadData, pull } from "./pull.ts"; +import type { Line } from "../../deps/scrapbox.ts"; +import { pushCommit, pushWithRetry } from "./_fetch.ts"; + +export interface PatchOptions { + socket?: Socket; +} + +/** ページ全体を書き換える + * + * serverには書き換え前後の差分だけを送信する + * + * @param project 書き換えたいページのproject + * @param title 書き換えたいページのタイトル + * @param update 書き換え後の本文を作成する函数。引数には現在の本文が渡される。空配列を返すとページが削除される。undefinedを返すと書き換えを中断する + * @param options 使用したいSocketがあれば指定する + */ +export async function patch( + project: string, + title: string, + update: ( + lines: Line[], + metadata: HeadData, + ) => string[] | undefined | Promise, + options?: PatchOptions, +): Promise { + const [ + head_, + projectId, + userId, + ] = await Promise.all([ + pull(project, title), + getProjectId(project), + getUserId(), + ]); + + let head = head_; + + const injectedSocket = options?.socket; + const socket = injectedSocket ?? await socketIO(); + await connect(socket); + try { + const { request } = wrap(socket); + + // 3回retryする + for (let i = 0; i < 3; i++) { + try { + const pending = update(head.lines, head); + const newLines = pending instanceof Promise ? await pending : pending; + + if (!newLines) return; + + if (newLines.length === 0) { + await pushWithRetry(request, [{ deleted: true }], { + projectId, + pageId: head.pageId, + parentId: head.commitId, + userId, + project, + title, + }); + } + + const changes = makeChanges(head.lines, newLines, { userId, head }); + await pushCommit(request, changes, { + parentId: head.commitId, + projectId, + pageId: head.pageId, + userId, + }); + break; + } catch (_e: unknown) { + if (i === 2) { + throw Error("Faild to retry pushing."); + } + console.log( + "Faild to push a commit. Retry after pulling new commits", + ); + try { + head = await pull(project, title); + } catch (e: unknown) { + throw e; + } + } + } + } finally { + if (!injectedSocket) await disconnect(socket); + } +} diff --git a/browser/websocket/shortcuts.ts b/browser/websocket/shortcuts.ts index 0070336..d17e9b8 100644 --- a/browser/websocket/shortcuts.ts +++ b/browser/websocket/shortcuts.ts @@ -1,10 +1,8 @@ import { socketIO, wrap } from "../../deps/socket.ts"; import { getProjectId, getUserId } from "./id.ts"; -import { makeChanges } from "./makeChanges.ts"; -import { HeadData, pull } from "./pull.ts"; +import { pull } from "./pull.ts"; import { pinNumber } from "./pin.ts"; -import type { Line } from "../../deps/scrapbox.ts"; -import { pushCommit, pushWithRetry } from "./_fetch.ts"; +import { pushWithRetry } from "./_fetch.ts"; /** 指定したページを削除する * @@ -44,84 +42,6 @@ export async function deletePage( } } -/** ページ全体を書き換える - * - * serverには書き換え前後の差分だけを送信する - * - * @param project 書き換えたいページのproject - * @param title 書き換えたいページのタイトル - * @param update 書き換え後の本文を作成する函数。引数には現在の本文が渡される。空配列を返すとページが削除される。undefinedを返すと書き換えを中断する - */ -export async function patch( - project: string, - title: string, - update: ( - lines: Line[], - metadata: HeadData, - ) => string[] | undefined | Promise, -): Promise { - const [ - head_, - projectId, - userId, - ] = await Promise.all([ - pull(project, title), - getProjectId(project), - getUserId(), - ]); - - let head = head_; - - const io = await socketIO(); - try { - const { request } = wrap(io); - - // 3回retryする - for (let i = 0; i < 3; i++) { - try { - const pending = update(head.lines, head); - const newLines = pending instanceof Promise ? await pending : pending; - - if (!newLines) return; - - if (newLines.length === 0) { - await pushWithRetry(request, [{ deleted: true }], { - projectId, - pageId: head.pageId, - parentId: head.commitId, - userId, - project, - title, - }); - } - - const changes = makeChanges(head.lines, newLines, { userId, head }); - await pushCommit(request, changes, { - parentId: head.commitId, - projectId, - pageId: head.pageId, - userId, - }); - break; - } catch (_e: unknown) { - if (i === 2) { - throw Error("Faild to retry pushing."); - } - console.log( - "Faild to push a commit. Retry after pulling new commits", - ); - try { - head = await pull(project, title); - } catch (e: unknown) { - throw e; - } - } - } - } finally { - io.disconnect(); - } -} - export interface PinOption { /** ピン留め対象のページが存在しないときの振る舞いを変えるoption * From f3d1f1918e6d0e9fe1c557d228b78eba58839b00 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:14:14 +0900 Subject: [PATCH 3/7] :sparkles: Enable to run `deletePage()` with an existing socket --- browser/websocket/deletePage.ts | 50 +++++++++++++++++++++++++++++++++ browser/websocket/shortcuts.ts | 38 ------------------------- 2 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 browser/websocket/deletePage.ts diff --git a/browser/websocket/deletePage.ts b/browser/websocket/deletePage.ts new file mode 100644 index 0000000..c761fc4 --- /dev/null +++ b/browser/websocket/deletePage.ts @@ -0,0 +1,50 @@ +import { Socket, socketIO, wrap } from "../../deps/socket.ts"; +import { connect, disconnect } from "./socket.ts"; +import { getProjectId, getUserId } from "./id.ts"; +import { pull } from "./pull.ts"; +import { pushWithRetry } from "./_fetch.ts"; + +export interface DeletePageOptions { + socket?: Socket; +} + +/** 指定したページを削除する + * + * @param project 削除したいページのproject + * @param title 削除したいページのタイトル + * @param options 使用したいSocketがあれば指定する + */ +export async function deletePage( + project: string, + title: string, + options?: DeletePageOptions, +): Promise { + const [ + { pageId, commitId: parentId, persistent }, + projectId, + userId, + ] = await Promise.all([ + pull(project, title), + getProjectId(project), + getUserId(), + ]); + + if (!persistent) return; + + const injectedSocket = options?.socket; + const socket = injectedSocket ?? await socketIO(); + await connect(socket); + const { request } = wrap(socket); + try { + await pushWithRetry(request, [{ deleted: true }], { + projectId, + pageId, + parentId, + userId, + project, + title, + }); + } finally { + if (!injectedSocket) await disconnect(socket); + } +} diff --git a/browser/websocket/shortcuts.ts b/browser/websocket/shortcuts.ts index d17e9b8..17ec6f7 100644 --- a/browser/websocket/shortcuts.ts +++ b/browser/websocket/shortcuts.ts @@ -4,44 +4,6 @@ import { pull } from "./pull.ts"; import { pinNumber } from "./pin.ts"; import { pushWithRetry } from "./_fetch.ts"; -/** 指定したページを削除する - * - * @param project 削除したいページのproject - * @param title 削除したいページのタイトル - */ -export async function deletePage( - project: string, - title: string, -): Promise { - const [ - { pageId, commitId: parentId, persistent }, - projectId, - userId, - ] = await Promise.all([ - pull(project, title), - getProjectId(project), - getUserId(), - ]); - - if (!persistent) return; - - const io = await socketIO(); - const { request } = wrap(io); - - try { - await pushWithRetry(request, [{ deleted: true }], { - projectId, - pageId, - parentId, - userId, - project, - title, - }); - } finally { - io.disconnect(); - } -} - export interface PinOption { /** ピン留め対象のページが存在しないときの振る舞いを変えるoption * From 9056916f1eb2b3a2b840072e0f7a506ae46695ac Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:20:05 +0900 Subject: [PATCH 4/7] :sparkles: Enable to run `pin()` with an existing socket --- browser/websocket/mod.ts | 3 +- browser/websocket/pin.ts | 113 +++++++++++++++++++++++++++++++++ browser/websocket/shortcuts.ts | 101 ----------------------------- 3 files changed, 115 insertions(+), 102 deletions(-) delete mode 100644 browser/websocket/shortcuts.ts diff --git a/browser/websocket/mod.ts b/browser/websocket/mod.ts index 59728d4..6d79c00 100644 --- a/browser/websocket/mod.ts +++ b/browser/websocket/mod.ts @@ -1,4 +1,5 @@ export * from "./room.ts"; -export * from "./shortcuts.ts"; export * from "./patch.ts"; +export * from "./deletePage.ts"; +export * from "./pin.ts"; export * from "./stream.ts"; diff --git a/browser/websocket/pin.ts b/browser/websocket/pin.ts index 138d91e..d9ba8cf 100644 --- a/browser/websocket/pin.ts +++ b/browser/websocket/pin.ts @@ -1,2 +1,115 @@ +import { Socket, socketIO, wrap } from "../../deps/socket.ts"; +import { connect, disconnect } from "./socket.ts"; +import { getProjectId, getUserId } from "./id.ts"; +import { pull } from "./pull.ts"; +import { pushWithRetry } from "./_fetch.ts"; + +export interface PinOptions { + /** ピン留め対象のページが存在しないときの振る舞いを変えるoption + * + * -`true`: タイトルのみのページを作成してピン留めする + * - `false`: ピン留めしない + * + * @default false + */ + create?: boolean; + socket?: Socket; +} +/** 指定したページをピン留めする + * + * @param project ピン留めしたいページのproject + * @param title ピン留めしたいページのタイトル + * @param options 使用したいSocketがあれば指定する + */ +export async function pin( + project: string, + title: string, + options?: PinOptions, +): Promise { + const [ + head, + projectId, + userId, + ] = await Promise.all([ + pull(project, title), + getProjectId(project), + getUserId(), + ]); + + // 既にピン留めされている場合は何もしない + if (head.pin > 0 || (!head.persistent && !(options?.create ?? false))) return; + + const init = { + parentId: head.commitId, + projectId, + pageId: head.pageId, + userId, + project, + title, + }; + const injectedSocket = options?.socket; + const socket = injectedSocket ?? await socketIO(); + await connect(socket); + const { request } = wrap(socket); + + // タイトルのみのページを作る + if (!head.persistent) { + const commitId = await pushWithRetry(request, [{ title }], init); + init.parentId = commitId; + } + + try { + await pushWithRetry(request, [{ pin: pinNumber() }], init); + } finally { + if (!injectedSocket) await disconnect(socket); + } +} + +export interface UnPinOptions { + socket?: Socket; +} +/** 指定したページのピン留めを外す + * + * @param project ピン留めを外したいページのproject + * @param title ピン留めを外したいページのタイトル + */ +export async function unpin( + project: string, + title: string, + options: UnPinOptions, +): Promise { + const [ + head, + projectId, + userId, + ] = await Promise.all([ + pull(project, title), + getProjectId(project), + getUserId(), + ]); + + // 既にピンが外れているか、そもそも存在しないページの場合は何もしない + if (head.pin == 0 || !head.persistent) return; + + const init = { + parentId: head.commitId, + projectId, + pageId: head.pageId, + userId, + project, + title, + }; + const injectedSocket = options?.socket; + const socket = injectedSocket ?? await socketIO(); + await connect(socket); + const { request } = wrap(socket); + + try { + await pushWithRetry(request, [{ pin: 0 }], init); + } finally { + if (!injectedSocket) await disconnect(socket); + } +} + export const pinNumber = (): number => Number.MAX_SAFE_INTEGER - Math.floor(Date.now() / 1000); diff --git a/browser/websocket/shortcuts.ts b/browser/websocket/shortcuts.ts deleted file mode 100644 index 17ec6f7..0000000 --- a/browser/websocket/shortcuts.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { socketIO, wrap } from "../../deps/socket.ts"; -import { getProjectId, getUserId } from "./id.ts"; -import { pull } from "./pull.ts"; -import { pinNumber } from "./pin.ts"; -import { pushWithRetry } from "./_fetch.ts"; - -export interface PinOption { - /** ピン留め対象のページが存在しないときの振る舞いを変えるoption - * - * -`true`: タイトルのみのページを作成してピン留めする - * - `false`: ピン留めしない - * - * @default false - */ - create: boolean; -} -/** 指定したページをピン留めする - * - * @param project ピン留めしたいページのproject - * @param title ピン留めしたいページのタイトル - */ -export async function pin( - project: string, - title: string, - option?: PinOption, -): Promise { - const [ - head, - projectId, - userId, - ] = await Promise.all([ - pull(project, title), - getProjectId(project), - getUserId(), - ]); - - // 既にピン留めされている場合は何もしない - if (head.pin > 0 || (!head.persistent && !(option?.create ?? false))) return; - - const init = { - parentId: head.commitId, - projectId, - pageId: head.pageId, - userId, - project, - title, - }; - const io = await socketIO(); - const { request } = wrap(io); - - // タイトルのみのページを作る - if (!head.persistent) { - const commitId = await pushWithRetry(request, [{ title }], init); - init.parentId = commitId; - } - - try { - await pushWithRetry(request, [{ pin: pinNumber() }], init); - } finally { - io.disconnect(); - } -} -/** 指定したページのピン留めを外す - * - * @param project ピン留めを外したいページのproject - * @param title ピン留めを外したいページのタイトル - */ -export async function unpin( - project: string, - title: string, -): Promise { - const [ - head, - projectId, - userId, - ] = await Promise.all([ - pull(project, title), - getProjectId(project), - getUserId(), - ]); - - // 既にピンが外れているか、そもそも存在しないページの場合は何もしない - if (head.pin == 0 || !head.persistent) return; - - const init = { - parentId: head.commitId, - projectId, - pageId: head.pageId, - userId, - project, - title, - }; - const io = await socketIO(); - const { request } = wrap(io); - - try { - await pushWithRetry(request, [{ pin: 0 }], init); - } finally { - io.disconnect(); - } -} From fba7f5fb852d98a804bfff0917e7c6a1e8986213 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:41:03 +0900 Subject: [PATCH 5/7] :sparkles: Enable to run `listenStream()` with an existing socket --- browser/websocket/listen.ts | 53 +++++++++++++++++++++++++++++++++++++ browser/websocket/stream.ts | 35 ------------------------ 2 files changed, 53 insertions(+), 35 deletions(-) create mode 100644 browser/websocket/listen.ts delete mode 100644 browser/websocket/stream.ts diff --git a/browser/websocket/listen.ts b/browser/websocket/listen.ts new file mode 100644 index 0000000..45107bc --- /dev/null +++ b/browser/websocket/listen.ts @@ -0,0 +1,53 @@ +import { + ProjectUpdatesStreamCommit, + ProjectUpdatesStreamEvent, + Socket, + socketIO, + wrap, +} from "../../deps/socket.ts"; +import { connect, disconnect } from "./socket.ts"; +import { getProjectId } from "./id.ts"; +export type { + ProjectUpdatesStreamCommit, + ProjectUpdatesStreamEvent, +} from "../../deps/socket.ts"; + +export interface ListenStreamOptions { + socket?: Socket; +} + +/** Streamを購読する + * + * @param project 購読したいproject + * @param events 購読したいevent。配列で指定する + * @param options 使用したいSocketがあれば指定する + */ +export async function* listenStream( + project: string, + events: ["commit" | "event", ...("commit" | "event")[]], + options?: ListenStreamOptions, +): AsyncGenerator< + ProjectUpdatesStreamEvent | ProjectUpdatesStreamCommit, + void, + unknown +> { + const projectId = await getProjectId(project); + + const injectedSocket = options?.socket; + const socket = injectedSocket ?? await socketIO(); + await connect(socket); + const { request, response } = wrap(socket); + + // 部屋に入って購読し始める + await request("socket.io-request", { + method: "room:join", + data: { projectId, pageId: null, projectUpdatesStream: true }, + }); + try { + yield* response( + ...events.map((event) => `projectUpdatesStream:${event}` as const), + ); + } finally { + if (!injectedSocket) await disconnect(socket); + } +} diff --git a/browser/websocket/stream.ts b/browser/websocket/stream.ts deleted file mode 100644 index 7a5cdde..0000000 --- a/browser/websocket/stream.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ListenEventMap, socketIO, wrap } from "../../deps/socket.ts"; -import { getProjectId } from "./id.ts"; -export type { - ProjectUpdatesStreamCommit, - ProjectUpdatesStreamEvent, -} from "../../deps/socket.ts"; - -/** Streamを購読する - * - * @param project 購読したいproject - * @param events 購読したいeventの種類。複数種類を指定できる - */ -export async function* listenStream( - project: string, - ...events: EventName[] -) { - const projectId = await getProjectId(project); - - const io = await socketIO(); - const { request, response } = wrap(io); - await request("socket.io-request", { - method: "room:join", - data: { projectId, pageId: null, projectUpdatesStream: true }, - }); - try { - yield* response( - ...(events.length > 0 ? events : [ - "projectUpdatesStream:event", - "projectUpdatesStream:commit", - ] as const), - ); - } finally { - io.disconnect(); - } -} From fae79b4bb538bc7811e31a9203666354a839e827 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:43:36 +0900 Subject: [PATCH 6/7] =?UTF-8?q?:+1:=20=E5=85=A5=E5=AE=A4=E6=99=82=E3=81=AB?= =?UTF-8?q?=E4=BE=8B=E5=A4=96=E3=81=8C=E7=99=BA=E7=94=9F=E3=81=97=E3=81=9F?= =?UTF-8?q?=E3=81=A8=E3=81=8D=E3=82=82=E5=BE=8C=E5=A7=8B=E6=9C=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser/websocket/listen.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/browser/websocket/listen.ts b/browser/websocket/listen.ts index 45107bc..524c12f 100644 --- a/browser/websocket/listen.ts +++ b/browser/websocket/listen.ts @@ -38,12 +38,13 @@ export async function* listenStream( await connect(socket); const { request, response } = wrap(socket); - // 部屋に入って購読し始める - await request("socket.io-request", { - method: "room:join", - data: { projectId, pageId: null, projectUpdatesStream: true }, - }); try { + // 部屋に入って購読し始める + await request("socket.io-request", { + method: "room:join", + data: { projectId, pageId: null, projectUpdatesStream: true }, + }); + yield* response( ...events.map((event) => `projectUpdatesStream:${event}` as const), ); From bf154db0c067c2731825ff881359d83442896ea6 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:48:46 +0900 Subject: [PATCH 7/7] =?UTF-8?q?:bug:=20module=E5=90=8D=E3=81=AE=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=E3=82=92=E5=8F=8D=E6=98=A0=E3=81=97=E5=BF=98=E3=82=8C?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser/websocket/mod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/websocket/mod.ts b/browser/websocket/mod.ts index 6d79c00..a5932a5 100644 --- a/browser/websocket/mod.ts +++ b/browser/websocket/mod.ts @@ -2,4 +2,4 @@ export * from "./room.ts"; export * from "./patch.ts"; export * from "./deletePage.ts"; export * from "./pin.ts"; -export * from "./stream.ts"; +export * from "./listen.ts";