diff --git a/mod.ts b/mod.ts index d3dd7cd..9d07358 100644 --- a/mod.ts +++ b/mod.ts @@ -3,7 +3,17 @@ /// import type { Socket } from "./socket.ts"; -import type { DataOf, EventMap, ListenEventMap, ResponseOf } from "./types.ts"; +import { + DataOf, + EventMap, + FailedResOf, + isPageCommitError, + ListenEventMap, + Result, + SuccessResOf, + TimeoutError, + UnexpectedError, +} from "./types.ts"; export * from "./types.ts"; export * from "./socket.ts"; @@ -12,12 +22,14 @@ export interface SocketOperator { event: EventName, data: DataOf, ) => Promise< - EventName extends "cursor" ? void - : ResponseOf<"socket.io-request">["data"] + Result< + SuccessResOf, + FailedResOf | UnexpectedError | TimeoutError + > >; response: ( ...events: EventName[] - ) => AsyncGenerator[0], void, unknown>; + ) => AsyncGenerator; } export const wrap = ( @@ -28,34 +40,70 @@ export const wrap = ( event: EventName, data: DataOf, ): Promise< - EventName extends "cursor" ? void - : ResponseOf<"socket.io-request">["data"] + Result< + SuccessResOf, + FailedResOf | UnexpectedError | TimeoutError + > > => { let id: number | undefined; - type ResolveType = EventName extends "cursor" ? void - : ResponseOf<"socket.io-request">["data"]; return new Promise((resolve, reject) => { const onDisconnect = (message: string) => { clearTimeout(id); reject(new Error(message)); }; - socket.emit(event, data, (response: ResponseOf) => { - clearTimeout(id); - socket.off("disconnect", onDisconnect); - if (response.error) { + socket.emit( + event, + data, + (response: { data: SuccessResOf } | { error: unknown }) => { + clearTimeout(id); + socket.off("disconnect", onDisconnect); + switch (event) { + case "socket.io-request": + if ("error" in response) { + if ( + typeof response.error === "object" && response.error && + "name" in response.error && + typeof response.error.name === "string" && + isPageCommitError({ name: response.error.name }) + ) { + resolve({ ok: false, value: response.error }); + } else { + resolve({ + ok: false, + value: { name: "UnexpectedError", value: response.error }, + }); + } + } else if ("data" in response) { + resolve({ ok: true, value: response.data }); + } + break; + case "cursor": + if ("error" in response) { + resolve({ + ok: false, + value: { name: "UnexpectedError", value: response.error }, + }); + } else if ("data" in response) { + resolve({ ok: true, value: response.data }); + } + break; + } reject( - new Error(JSON.stringify(response.error)), + new Error( + 'Invalid response: missing "data" or "error" field', + ), ); - } - if ("data" in response) { - resolve(response?.data as ResolveType); - } else { - resolve(undefined as ResolveType); - } - }); + }, + ); id = setTimeout(() => { socket.off("disconnect", onDisconnect); - reject(new Error(`Timeout: exceeded ${timeout}ms`)); + resolve({ + ok: false, + value: { + name: "TimeoutError", + message: `Timeout: exceeded ${timeout}ms`, + }, + }); }, timeout); socket.once("disconnect", onDisconnect); }); @@ -64,7 +112,7 @@ export const wrap = ( async function* response( ...events: EventName[] ) { - type Data = Parameters[0]; + type Data = ListenEventMap[EventName]; let _resolve: ((data: Data) => void) | undefined; const waitForEvent = () => new Promise((res) => _resolve = res); const resolve = (data: Data) => { diff --git a/types.ts b/types.ts index 74d4a69..cc72a88 100644 --- a/types.ts +++ b/types.ts @@ -1,19 +1,30 @@ -export type JoinRoomData = { - pageId: string | null; +export type JoinRoomRequest = + | JoinPageRoomRequest + | JoinProjectRoomRequest + | JoinStreamRoomRequest; + +export interface JoinProjectRoomRequest { + pageId: null; projectId: string; projectUpdatesStream: false; -} | { +} + +export interface JoinPageRoomRequest { + pageId: string; + projectId: string; + projectUpdatesStream: false; +} + +export interface JoinStreamRoomRequest { pageId: null; projectId: string; projectUpdatesStream: true; -}; +} export interface JoinRoomResponse { - data: { - success: true; - pageId: string | null; - projectId: string; - }; + success: true; + pageId: string | null; + projectId: string; } export interface ProjectUpdatesStreamCommit { @@ -23,85 +34,180 @@ export interface ProjectUpdatesStreamCommit { projectId: string; pageId: string; userId: string; - changes: Change[] | [Delete]; + changes: + | ( + | InsertChange + | UpdateChange + | DeleteChange + | TitleChange + | LinksChange + | IconsChange + )[] + | [DeletePageChange]; cursor: null; freeze: true; } + export type ProjectUpdatesStreamEvent = - & { - id: string; - pageId: string; - userId: string; - projectId: string; - created: number; - updated: number; - } - & ({ - type: "member.join" | "invitation.reset"; - } | { - type: "page.delete"; - data: { - titleLc: string; - }; - } | { - type: "admin.add" | "admin.delete" | "owner.set"; - targetUserId: string; - }); - -export interface CommitNotification { - kind: "page"; + | MemberJoinEvent + | InvitationResetEvent + | PageDeleteEvent + | AdminAddEvent + | AdminDeleteEvent + | OwnerSetEvent; + +export interface ProjectEvent { + id: string; + pageId: string; + userId: string; + projectId: string; + created: number; + updated: number; +} + +export interface PageDeleteEvent extends ProjectEvent { + type: "page.delete"; + data: { + titleLc: string; + }; +} + +export interface MemberJoinEvent extends ProjectEvent { + type: "member.join"; +} +export interface InvitationResetEvent extends ProjectEvent { + type: "invitation.reset"; +} +export interface AdminAddEvent extends ProjectEvent { + type: "admin.add"; + targetUserId: string; +} +export interface AdminDeleteEvent extends ProjectEvent { + type: "admin.delete"; + targetUserId: string; +} +export interface OwnerSetEvent extends ProjectEvent { + type: "owner.set"; + targetUserId: string; +} + +export interface CommitNotification extends PageCommit { id: string; +} + +export interface PageCommit { + kind: "page"; parentId: string; projectId: string; pageId: string; userId: string; - changes: Change[] | [Pin] | [Delete]; - cursor: null; + changes: Change[] | [PinChange] | [DeletePageChange]; + cursor?: null; freeze: true; } -export type CommitData = Omit; -export interface CommitResponse { - data: { - commitId: string; - }; +export interface PageCommitResponse { + commitId: string; +} + +export interface ErrorLike { + name: string; } + +export interface UnexpectedError { + name: "UnexpectedError"; + value: unknown; +} +export interface TimeoutError { + name: "TimeoutError"; + message: string; +} + +export type PageCommitError = + | SocketIOError + | DuplicateTitleError + | NotFastForwardError; + +/* the error that occurs when the socket.io causes an error +* +* when this error occurs, wait for a while and retry the request +*/ +export interface SocketIOError { + name: "SocketIOError"; +} +/** the error that occurs when the title is already in use */ +export interface DuplicateTitleError { + name: "DuplicateTitleError"; +} +/** the error caused when commitId is not latest */ +export interface NotFastForwardError { + name: "NotFastForwardError"; +} + +export const isPageCommitError = (error: ErrorLike): error is PageCommitError => + pageCommitErrorNames.includes(error.name); + +const pageCommitErrorNames = [ + "SocketIOError", + "DuplicateTitleError", + "NotFastForwardError", +]; + +export type Result = + | { ok: true; value: T } + | { ok: false; value: E }; + export interface EventMap { "socket.io-request": ( - data: { method: "commit"; data: CommitData } | { + req: { method: "commit"; data: PageCommit } | { method: "room:join"; - data: JoinRoomData; + data: JoinRoomRequest; }, - callback: ( - response: (CommitResponse | JoinRoomResponse) & { error?: unknown }, - ) => void, + success: PageCommitResponse | JoinRoomResponse, + failed: PageCommitError, ) => void; cursor: ( - data: Omit, - callback: (response: { error?: unknown }) => void, + req: Omit, + success: undefined, + failed: unknown, ) => void; } export interface ListenEventMap { - "projectUpdatesStream:commit": ( - data: ProjectUpdatesStreamCommit, - ) => void; - "projectUpdatesStream:event": ( - data: ProjectUpdatesStreamEvent, - ) => void; - commit: (data: CommitNotification) => void; - cursor: (data: MoveCursorData) => void; + "projectUpdatesStream:commit": ProjectUpdatesStreamCommit; + "projectUpdatesStream:event": ProjectUpdatesStreamEvent; + commit: CommitNotification; + cursor: MoveCursorData; + "quick-search:commit": QuickSearchCommit; + "quick-search:replace-link": QuickSearchReplaceLink; + "infobox:updating": boolean; + "infobox:reload": void; + "literal-database:reload": void; +} + +export interface QuickSearchCommit extends Omit { + changes: + | (TitleChange | LinksChange | DescriptionsChange | ImageChange)[] + | [DeletePageChange]; +} + +export interface QuickSearchReplaceLink { + from: string; + to: string; } + export type DataOf = Parameters< EventMap[Event] >[0]; -export type ResponseOf = Parameters< - Parameters< - EventMap[Event] - >[1] ->[0]; +export type SuccessResOf = Parameters< + EventMap[Event] +>[1]; +export type FailedResOf = Parameters< + EventMap[Event] +>[2]; export interface MoveCursorData { user: { id: string; + name: string; displayName: string; }; pageId: string; @@ -114,49 +220,66 @@ export interface MoveCursorData { } export type Change = - | InsertCommit - | UpdateCommit - | DeleteCommit - | LinksCommit - | ProjectLinksCommit - | DescriptionsCommit - | ImageCommit - | TitleCommit; -export interface InsertCommit { + | InsertChange + | UpdateChange + | DeleteChange + | LinksChange + | ProjectLinksChange + | DescriptionsChange + | ImageChange + | FilesChange + | HelpFeelsChange + | infoboxDefinitionChange + | TitleChange; +export interface InsertChange { _insert: string; lines: { id: string; text: string; }; } -export interface UpdateCommit { +export interface UpdateChange { _update: string; lines: { text: string; }; + noTimestampUpdate: unknown; } -export interface DeleteCommit { +export interface DeleteChange { _delete: string; lines: -1; } -export interface LinksCommit { +export interface LinksChange { links: string[]; } -export interface ProjectLinksCommit { +export interface ProjectLinksChange { projectLinks: string[]; } -export interface DescriptionsCommit { +export interface IconsChange { + icons: string[]; +} +export interface DescriptionsChange { descriptions: string[]; } -export interface ImageCommit { +export interface ImageChange { image: string | null; } -export interface TitleCommit { +export interface TitleChange { title: string; } -export interface Pin { +export interface FilesChange { + files: unknown[]; +} +export interface HelpFeelsChange { + helpfeels: unknown[]; +} +export interface infoboxDefinitionChange { + infoboxDefinition: unknown[]; +} +export interface PinChange { pin: number; } -export interface Delete { +export interface DeletePageChange { deleted: true; + merged?: true; }