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;
}