From 4f0727a9f8cbcb7ab9e1b690ced7b3ac048ed93b Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:52:13 +0900 Subject: [PATCH 1/3] refactor: Examine how the scrapbox websocket peforms on `emit` and apply to API --- browser/websocket/@types/socket-io/emitter.ts | 24 - browser/websocket/@types/socket-io/index.ts | 39 -- browser/websocket/@types/socket-io/manager.ts | 486 ------------------ browser/websocket/@types/socket-io/parser.ts | 75 --- browser/websocket/@types/socket-io/socket.ts | 238 --------- .../@types/socket-io/typed-events.ts | 129 ----- browser/websocket/emit.ts | 131 +++++ browser/websocket/error.ts | 49 ++ browser/websocket/listen.ts | 33 +- browser/websocket/patch.ts | 5 +- browser/websocket/pin.ts | 6 +- browser/websocket/push.ts | 57 +- browser/websocket/socket-io.ts | 59 --- browser/websocket/socket.ts | 90 +++- browser/websocket/websocket-types.ts | 71 +-- browser/websocket/wrap.ts | 139 ----- deno.jsonc | 6 +- deno.lock | 83 ++- 18 files changed, 380 insertions(+), 1340 deletions(-) delete mode 100644 browser/websocket/@types/socket-io/emitter.ts delete mode 100644 browser/websocket/@types/socket-io/index.ts delete mode 100644 browser/websocket/@types/socket-io/manager.ts delete mode 100644 browser/websocket/@types/socket-io/parser.ts delete mode 100644 browser/websocket/@types/socket-io/socket.ts delete mode 100644 browser/websocket/@types/socket-io/typed-events.ts create mode 100644 browser/websocket/emit.ts create mode 100644 browser/websocket/error.ts delete mode 100644 browser/websocket/socket-io.ts delete mode 100644 browser/websocket/wrap.ts diff --git a/browser/websocket/@types/socket-io/emitter.ts b/browser/websocket/@types/socket-io/emitter.ts deleted file mode 100644 index bb7aa7d..0000000 --- a/browser/websocket/@types/socket-io/emitter.ts +++ /dev/null @@ -1,24 +0,0 @@ -// this file is based on https://cdn.esm.sh/v54/@types/component-emitter@1.2.10/index.d.ts -// Type definitions for component-emitter v1.2.1 -// Project: https://www.npmjs.com/package/component-emitter -// Definitions by: Peter Snider -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -// TypeScript Version: 2.2 - -// deno-lint-ignore-file ban-types no-explicit-any -interface Emitter { - on(event: Event, listener: Function): Emitter; - once(event: Event, listener: Function): Emitter; - off(event?: Event, listener?: Function): Emitter; - emit(event: Event, ...args: any[]): Emitter; - listeners(event: Event): Function[]; - hasListeners(event: Event): boolean; -} - -declare const Emitter: { - (obj?: object): Emitter; - new (obj?: object): Emitter; -}; - -export { Emitter }; diff --git a/browser/websocket/@types/socket-io/index.ts b/browser/websocket/@types/socket-io/index.ts deleted file mode 100644 index 7cca320..0000000 --- a/browser/websocket/@types/socket-io/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -// this file is copied from https://cdn.esm.sh/v54/socket.io-client@4.2.0/build/index.d.ts -import type { ManagerOptions } from "./manager.ts"; -import type { Socket, SocketOptions } from "./socket.ts"; -/** - * Looks up an existing `Manager` for multiplexing. - * If the user summons: - * - * `io('http://localhost/a');` - * `io('http://localhost/b');` - * - * We reuse the existing instance based on same scheme/port/host, - * and we initialize sockets for each namespace. - * - * @public - */ -declare function lookup(opts?: Partial): Socket; -declare function lookup( - uri: string, - opts?: Partial, -): Socket; -declare function lookup( - uri: string | Partial, - opts?: Partial, -): Socket; -/** - * Protocol version. - * - * @public - */ -export { protocol } from "./parser.ts"; -/** - * Expose constructors for standalone build. - * - * @public - */ -export type { Manager, ManagerOptions } from "./manager.ts"; -export { Socket } from "./socket.ts"; -export type { lookup as io, SocketOptions }; -export default lookup; diff --git a/browser/websocket/@types/socket-io/manager.ts b/browser/websocket/@types/socket-io/manager.ts deleted file mode 100644 index 85a4065..0000000 --- a/browser/websocket/@types/socket-io/manager.ts +++ /dev/null @@ -1,486 +0,0 @@ -// this file is copied from https://cdn.esm.sh/v54/socket.io-client@4.2.0/build/manager.d.ts -// deno-lint-ignore-file no-explicit-any camelcase ban-types -import type { Socket, SocketOptions } from "./socket.ts"; -import type { Packet } from "./parser.ts"; -import { - type DefaultEventsMap, - type EventsMap, - StrictEventEmitter, -} from "./typed-events.ts"; -interface EngineOptions { - /** - * The host that we're connecting to. Set from the URI passed when connecting - */ - host: string; - /** - * The hostname for our connection. Set from the URI passed when connecting - */ - hostname: string; - /** - * If this is a secure connection. Set from the URI passed when connecting - */ - secure: boolean; - /** - * The port for our connection. Set from the URI passed when connecting - */ - port: string; - /** - * Any query parameters in our uri. Set from the URI passed when connecting - */ - query: { - [key: string]: string; - }; - /** - * `http.Agent` to use, defaults to `false` (NodeJS only) - */ - agent: string | boolean; - /** - * Whether the client should try to upgrade the transport from - * long-polling to something better. - * @default true - */ - upgrade: boolean; - /** - * Forces JSONP for polling transport. - */ - forceJSONP: boolean; - /** - * Determines whether to use JSONP when necessary for polling. If - * disabled (by settings to false) an error will be emitted (saying - * "No transports available") if no other transports are available. - * If another transport is available for opening a connection (e.g. - * WebSocket) that transport will be used instead. - * @default true - */ - jsonp: boolean; - /** - * Forces base 64 encoding for polling transport even when XHR2 - * responseType is available and WebSocket even if the used standard - * supports binary. - */ - forceBase64: boolean; - /** - * Enables XDomainRequest for IE8 to avoid loading bar flashing with - * click sound. default to `false` because XDomainRequest has a flaw - * of not sending cookie. - * @default false - */ - enablesXDR: boolean; - /** - * The param name to use as our timestamp key - * @default 't' - */ - timestampParam: string; - /** - * Whether to add the timestamp with each transport request. Note: this - * is ignored if the browser is IE or Android, in which case requests - * are always stamped - * @default false - */ - timestampRequests: boolean; - /** - * A list of transports to try (in order). Engine.io always attempts to - * connect directly with the first one, provided the feature detection test - * for it passes. - * @default ['polling','websocket'] - */ - transports: string[]; - /** - * The port the policy server listens on - * @default 843 - */ - policyPost: number; - /** - * If true and if the previous websocket connection to the server succeeded, - * the connection attempt will bypass the normal upgrade process and will - * initially try websocket. A connection attempt following a transport error - * will use the normal upgrade process. It is recommended you turn this on - * only when using SSL/TLS connections, or if you know that your network does - * not block websockets. - * @default false - */ - rememberUpgrade: boolean; - /** - * Are we only interested in transports that support binary? - */ - onlyBinaryUpgrades: boolean; - /** - * Timeout for xhr-polling requests in milliseconds (0) (only for polling transport) - */ - requestTimeout: number; - /** - * Transport options for Node.js client (headers etc) - */ - transportOptions: Object; - /** - * (SSL) Certificate, Private key and CA certificates to use for SSL. - * Can be used in Node.js client environment to manually specify - * certificate information. - */ - pfx: string; - /** - * (SSL) Private key to use for SSL. Can be used in Node.js client - * environment to manually specify certificate information. - */ - key: string; - /** - * (SSL) A string or passphrase for the private key or pfx. Can be - * used in Node.js client environment to manually specify certificate - * information. - */ - passphrase: string; - /** - * (SSL) Public x509 certificate to use. Can be used in Node.js client - * environment to manually specify certificate information. - */ - cert: string; - /** - * (SSL) An authority certificate or array of authority certificates to - * check the remote host against.. Can be used in Node.js client - * environment to manually specify certificate information. - */ - ca: string | string[]; - /** - * (SSL) A string describing the ciphers to use or exclude. Consult the - * [cipher format list] - * (http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT) for - * details on the format.. Can be used in Node.js client environment to - * manually specify certificate information. - */ - ciphers: string; - /** - * (SSL) If true, the server certificate is verified against the list of - * supplied CAs. An 'error' event is emitted if verification fails. - * Verification happens at the connection level, before the HTTP request - * is sent. Can be used in Node.js client environment to manually specify - * certificate information. - */ - rejectUnauthorized: boolean; - /** - * Headers that will be passed for each request to the server (via xhr-polling and via websockets). - * These values then can be used during handshake or for special proxies. - */ - extraHeaders?: { - [header: string]: string; - }; - /** - * Whether to include credentials (cookies, authorization headers, TLS - * client certificates, etc.) with cross-origin XHR polling requests - * @default false - */ - withCredentials: boolean; - /** - * Whether to automatically close the connection whenever the beforeunload event is received. - * @default true - */ - closeOnBeforeunload: boolean; - /** - * Whether to always use the native timeouts. This allows the client to - * reconnect when the native timeout functions are overridden, such as when - * mock clocks are installed. - * @default false - */ - useNativeTimers: boolean; -} -export interface ManagerOptions extends EngineOptions { - /** - * Should we force a new Manager for this connection? - * @default false - */ - forceNew: boolean; - /** - * Should we multiplex our connection (reuse existing Manager) ? - * @default true - */ - multiplex: boolean; - /** - * The path to get our client file from, in the case of the server - * serving it - * @default '/socket.io' - */ - path: string; - /** - * Should we allow reconnections? - * @default true - */ - reconnection: boolean; - /** - * How many reconnection attempts should we try? - * @default Infinity - */ - reconnectionAttempts: number; - /** - * The time delay in milliseconds between reconnection attempts - * @default 1000 - */ - reconnectionDelay: number; - /** - * The max time delay in milliseconds between reconnection attempts - * @default 5000 - */ - reconnectionDelayMax: number; - /** - * Used in the exponential backoff jitter when reconnecting - * @default 0.5 - */ - randomizationFactor: number; - /** - * The timeout in milliseconds for our connection attempt - * @default 20000 - */ - timeout: number; - /** - * Should we automatically connect? - * @default true - */ - autoConnect: boolean; - /** - * weather we should unref the reconnect timer when it is - * create automatically - * @default false - */ - autoUnref: boolean; - /** - * the parser to use. Defaults to an instance of the Parser that ships with socket.io. - */ - parser: any; -} -interface ManagerReservedEvents { - open: () => void; - error: (err: Error) => void; - ping: () => void; - packet: (packet: Packet) => void; - close: (reason: string) => void; - reconnect_failed: () => void; - reconnect_attempt: (attempt: number) => void; - reconnect_error: (err: Error) => void; - reconnect: (attempt: number) => void; -} -export declare class Manager< - ListenEvents extends EventsMap = DefaultEventsMap, - EmitEvents extends EventsMap = ListenEvents, -> extends StrictEventEmitter<{}, {}, ManagerReservedEvents> { - /** - * The Engine.IO client instance - * - * @public - */ - engine: any; - /** - * @private - */ - _autoConnect: boolean; - /** - * @private - */ - _readyState: "opening" | "open" | "closed"; - /** - * @private - */ - _reconnecting: boolean; - private readonly uri; - opts: Partial; - private nsps; - private subs; - private backoff; - private setTimeoutFn; - private _reconnection; - private _reconnectionAttempts; - private _reconnectionDelay; - private _randomizationFactor; - private _reconnectionDelayMax; - private _timeout; - private encoder; - private decoder; - private skipReconnect; - /** - * `Manager` constructor. - * - * @param uri - engine instance or engine uri/opts - * @param opts - options - * @public - */ - constructor(opts: Partial); - constructor(uri?: string, opts?: Partial); - constructor( - uri?: string | Partial, - opts?: Partial, - ); - /** - * Sets the `reconnection` config. - * - * @param {Boolean} v - true/false if it should automatically reconnect - * @return {Manager} self or value - * @public - */ - reconnection(v: boolean): this; - reconnection(): boolean; - reconnection(v?: boolean): this | boolean; - /** - * Sets the reconnection attempts config. - * - * @param {Number} v - max reconnection attempts before giving up - * @return {Manager} self or value - * @public - */ - reconnectionAttempts(v: number): this; - reconnectionAttempts(): number; - reconnectionAttempts(v?: number): this | number; - /** - * Sets the delay between reconnections. - * - * @param {Number} v - delay - * @return {Manager} self or value - * @public - */ - reconnectionDelay(v: number): this; - reconnectionDelay(): number; - reconnectionDelay(v?: number): this | number; - /** - * Sets the randomization factor - * - * @param v - the randomization factor - * @return self or value - * @public - */ - randomizationFactor(v: number): this; - randomizationFactor(): number; - randomizationFactor(v?: number): this | number; - /** - * Sets the maximum delay between reconnections. - * - * @param v - delay - * @return self or value - * @public - */ - reconnectionDelayMax(v: number): this; - reconnectionDelayMax(): number; - reconnectionDelayMax(v?: number): this | number; - /** - * Sets the connection timeout. `false` to disable - * - * @param v - connection timeout - * @return self or value - * @public - */ - timeout(v: number | boolean): this; - timeout(): number | boolean; - timeout(v?: number | boolean): this | number | boolean; - /** - * Starts trying to reconnect if reconnection is enabled and we have not - * started reconnecting yet - * - * @private - */ - private maybeReconnectOnOpen; - /** - * Sets the current transport `socket`. - * - * @param {Function} fn - optional, callback - * @return self - * @public - */ - open(fn?: (err?: Error) => void): this; - /** - * Alias for open() - * - * @return self - * @public - */ - connect(fn?: (err?: Error) => void): this; - /** - * Called upon transport open. - * - * @private - */ - private onopen; - /** - * Called upon a ping. - * - * @private - */ - private onping; - /** - * Called with data. - * - * @private - */ - private ondata; - /** - * Called when parser fully decodes a packet. - * - * @private - */ - private ondecoded; - /** - * Called upon socket error. - * - * @private - */ - private onerror; - /** - * Creates a new socket for the given `nsp`. - * - * @return {Socket} - * @public - */ - socket(nsp: string, opts?: Partial): Socket; - /** - * Called upon a socket close. - * - * @param socket - * @private - */ - _destroy(socket: Socket): void; - /** - * Writes a packet. - * - * @param packet - * @private - */ - _packet( - packet: Partial< - Packet & { - query: string; - options: any; - } - >, - ): void; - /** - * Clean up transport subscriptions and packet buffer. - * - * @private - */ - private cleanup; - /** - * Close the current socket. - * - * @private - */ - _close(): void; - /** - * Alias for close() - * - * @private - */ - private disconnect; - /** - * Called upon engine close. - * - * @private - */ - private onclose; - /** - * Attempt a reconnection. - * - * @private - */ - private reconnect; - /** - * Called upon successful reconnect. - * - * @private - */ - private onreconnect; -} -export {}; diff --git a/browser/websocket/@types/socket-io/parser.ts b/browser/websocket/@types/socket-io/parser.ts deleted file mode 100644 index e2409e4..0000000 --- a/browser/websocket/@types/socket-io/parser.ts +++ /dev/null @@ -1,75 +0,0 @@ -// this file is copied from https://cdn.esm.sh/v54/socket.io-parser@4.0.4/dist/index.d.ts - -// deno-lint-ignore-file no-explicit-any -import { Emitter } from "./emitter.ts"; -/** - * Protocol version. - * - * @public - */ -export declare const protocol: number; -export declare enum PacketType { - CONNECT = 0, - DISCONNECT = 1, - EVENT = 2, - ACK = 3, - CONNECT_ERROR = 4, - BINARY_EVENT = 5, - BINARY_ACK = 6, -} -export interface Packet { - type: PacketType; - nsp: string; - data?: any; - id?: number; - attachments?: number; -} -/** - * A socket.io Encoder instance - */ -export declare class Encoder { - /** - * Encode a packet as a single string if non-binary, or as a - * buffer sequence, depending on packet type. - * - * @param {Object} obj - packet object - */ - encode(obj: Packet): any[]; - /** - * Encode packet as string. - */ - private encodeAsString; - /** - * Encode packet as 'buffer sequence' by removing blobs, and - * deconstructing packet into object with placeholders and - * a list of buffers. - */ - private encodeAsBinary; -} -/** - * A socket.io Decoder instance - * - * @return {Object} decoder - */ -export declare class Decoder extends Emitter { - private reconstructor; - constructor(); - /** - * Decodes an encoded packet string into packet JSON. - * - * @param {String} obj - encoded packet - */ - add(obj: any): void; - /** - * Decode a packet String (JSON data) - * - * @param {String} str - * @return {Object} packet - */ - private decodeString; - private static isPayloadValid; - /** - * Deallocates a parser's resources - */ - destroy(): void; -} diff --git a/browser/websocket/@types/socket-io/socket.ts b/browser/websocket/@types/socket-io/socket.ts deleted file mode 100644 index 916a852..0000000 --- a/browser/websocket/@types/socket-io/socket.ts +++ /dev/null @@ -1,238 +0,0 @@ -// deno-lint-ignore-file no-explicit-any camelcase -// this file is copied from https://cdn.esm.sh/v54/socket.io-client@4.2.0/build/socket.d.ts -import type { Packet } from "./parser.ts"; -import type { Manager } from "./manager.ts"; -import { - type DefaultEventsMap, - type EventNames, - type EventParams, - type EventsMap, - StrictEventEmitter, -} from "./typed-events.ts"; -export interface SocketOptions { - /** - * the authentication payload sent when connecting to the Namespace - */ - auth: { - [key: string]: any; - } | ((cb: (data: object) => void) => void); -} -interface SocketReservedEvents { - connect: () => void; - connect_error: (err: Error) => void; - disconnect: (reason: Socket.DisconnectReason) => void; -} -export declare class Socket< - ListenEvents extends EventsMap = DefaultEventsMap, - EmitEvents extends EventsMap = ListenEvents, -> extends StrictEventEmitter { - readonly io: Manager; - id: string; - connected: boolean; - disconnected: boolean; - auth: { - [key: string]: any; - } | ((cb: (data: object) => void) => void); - receiveBuffer: Array>; - sendBuffer: Array; - private readonly nsp; - private ids; - private acks; - private flags; - private subs?; - private _anyListeners; - /** - * `Socket` constructor. - * - * @public - */ - constructor(io: Manager, nsp: string, opts?: Partial); - /** - * Subscribe to open, close and packet events - * - * @private - */ - private subEvents; - /** - * Whether the Socket will try to reconnect when its Manager connects or reconnects - */ - get active(): boolean; - /** - * "Opens" the socket. - * - * @public - */ - connect(): this; - /** - * Alias for connect() - */ - open(): this; - /** - * Sends a `message` event. - * - * @return self - * @public - */ - send(...args: any[]): this; - /** - * Override `emit`. - * If the event is in `events`, it's emitted normally. - * - * @return self - * @public - */ - emit>( - ev: Ev, - ...args: EventParams - ): this; - /** - * Sends a packet. - * - * @param packet - * @private - */ - private packet; - /** - * Called upon engine `open`. - * - * @private - */ - private onopen; - /** - * Called upon engine or manager `error`. - * - * @param err - * @private - */ - private onerror; - /** - * Called upon engine `close`. - * - * @param reason - * @private - */ - private onclose; - /** - * Called with socket packet. - * - * @param packet - * @private - */ - private onpacket; - /** - * Called upon a server event. - * - * @param packet - * @private - */ - private onevent; - private emitEvent; - /** - * Produces an ack callback to emit with an event. - * - * @private - */ - private ack; - /** - * Called upon a server acknowlegement. - * - * @param packet - * @private - */ - private onack; - /** - * Called upon server connect. - * - * @private - */ - private onconnect; - /** - * Emit buffered events (received and emitted). - * - * @private - */ - private emitBuffered; - /** - * Called upon server disconnect. - * - * @private - */ - private ondisconnect; - /** - * Called upon forced client/server side disconnections, - * this method ensures the manager stops tracking us and - * that reconnections don't get triggered for this. - * - * @private - */ - private destroy; - /** - * Disconnects the socket manually. - * - * @return self - * @public - */ - disconnect(): this; - /** - * Alias for disconnect() - * - * @return self - * @public - */ - close(): this; - /** - * Sets the compress flag. - * - * @param compress - if `true`, compresses the sending data - * @return self - * @public - */ - compress(compress: boolean): this; - /** - * Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not - * ready to send messages. - * - * @returns self - * @public - */ - get volatile(): this; - /** - * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the - * callback. - * - * @param listener - * @public - */ - onAny(listener: (...args: any[]) => void): this; - /** - * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the - * callback. The listener is added to the beginning of the listeners array. - * - * @param listener - * @public - */ - prependAny(listener: (...args: any[]) => void): this; - /** - * Removes the listener that will be fired when any event is emitted. - * - * @param listener - * @public - */ - offAny(listener?: (...args: any[]) => void): this; - /** - * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated, - * e.g. to remove listeners. - * - * @public - */ - listenersAny(): ((...args: any[]) => void)[]; -} -export declare namespace Socket { - type DisconnectReason = - | "io server disconnect" - | "io client disconnect" - | "ping timeout" - | "transport close" - | "transport error"; -} -export {}; diff --git a/browser/websocket/@types/socket-io/typed-events.ts b/browser/websocket/@types/socket-io/typed-events.ts deleted file mode 100644 index b440c79..0000000 --- a/browser/websocket/@types/socket-io/typed-events.ts +++ /dev/null @@ -1,129 +0,0 @@ -// this file is copied from https://cdn.esm.sh/v54/socket.io-client@4.2.0/build/typed-events.d.ts -// deno-lint-ignore-file no-explicit-any ban-types -import { Emitter } from "./emitter.ts"; -/** - * An events map is an interface that maps event names to their value, which - * represents the type of the `on` listener. - */ -export interface EventsMap { - [event: string]: any; -} -/** - * The default events map, used if no EventsMap is given. Using this EventsMap - * is equivalent to accepting all event names, and any data. - */ -export interface DefaultEventsMap { - [event: string]: (...args: any[]) => void; -} -/** - * Returns a union type containing all the keys of an event map. - */ -export declare type EventNames = - & keyof Map - & (string | symbol); -/** The tuple type representing the parameters of an event listener */ -export declare type EventParams< - Map extends EventsMap, - Ev extends EventNames, -> = Parameters; -/** - * The event names that are either in ReservedEvents or in UserEvents - */ -export declare type ReservedOrUserEventNames< - ReservedEventsMap extends EventsMap, - UserEvents extends EventsMap, -> = EventNames | EventNames; -/** - * Type of a listener of a user event or a reserved event. If `Ev` is in - * `ReservedEvents`, the reserved event listener is returned. - */ -export declare type ReservedOrUserListener< - ReservedEvents extends EventsMap, - UserEvents extends EventsMap, - Ev extends ReservedOrUserEventNames, -> = FallbackToUntypedListener< - Ev extends EventNames ? ReservedEvents[Ev] - : Ev extends EventNames ? UserEvents[Ev] - : never ->; -/** - * Returns an untyped listener type if `T` is `never`; otherwise, returns `T`. - * - * This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833. - * Needed because of https://github.com/microsoft/TypeScript/issues/41778 - */ -declare type FallbackToUntypedListener = [T] extends [never] - ? (...args: any[]) => void | Promise - : T; -/** - * Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type - * parameters for mappings of event names to event data types, and strictly - * types method calls to the `EventEmitter` according to these event maps. - * - * @typeParam ListenEvents - `EventsMap` of user-defined events that can be - * listened to with `on` or `once` - * @typeParam EmitEvents - `EventsMap` of user-defined events that can be - * emitted with `emit` - * @typeParam ReservedEvents - `EventsMap` of reserved events, that can be - * emitted by socket.io with `emitReserved`, and can be listened to with - * `listen`. - */ -export declare abstract class StrictEventEmitter< - ListenEvents extends EventsMap, - EmitEvents extends EventsMap, - ReservedEvents extends EventsMap = {}, -> extends Emitter { - /** - * Adds the `listener` function as an event listener for `ev`. - * - * @param ev Name of the event - * @param listener Callback function - */ - on>( - ev: Ev, - listener: ReservedOrUserListener, - ): this; - /** - * Adds a one-time `listener` function as an event listener for `ev`. - * - * @param ev Name of the event - * @param listener Callback function - */ - once>( - ev: Ev, - listener: ReservedOrUserListener, - ): this; - /** - * Emits an event. - * - * @param ev Name of the event - * @param args Values to send to listeners of this event - */ - emit>( - ev: Ev, - ...args: EventParams - ): this; - /** - * Emits a reserved event. - * - * This method is `protected`, so that only a class extending - * `StrictEventEmitter` can emit its own reserved events. - * - * @param ev Reserved event name - * @param args Arguments to emit along with the event - */ - protected emitReserved>( - ev: Ev, - ...args: EventParams - ): this; - /** - * Returns the listeners listening to an event. - * - * @param event Event name - * @returns Array of listeners subscribed to `event` - */ - listeners>( - event: Ev, - ): ReservedOrUserListener[]; -} -export {}; diff --git a/browser/websocket/emit.ts b/browser/websocket/emit.ts new file mode 100644 index 0000000..920377f --- /dev/null +++ b/browser/websocket/emit.ts @@ -0,0 +1,131 @@ +import { createErr, createOk, type Result } from "option-t/plain_result"; +import type { Socket } from "socket.io-client"; +import type { + EmitEvents, + JoinRoomRequest, + ListenEvents, + MoveCursorData, + PageCommit, + PageCommitResponse, +} from "./websocket-types.ts"; +export * from "./websocket-types.ts"; +import { + isPageCommitError, + type PageCommitError, + type SocketIOServerDisconnectError, + type TimeoutError, + type UnexpectedRequestError, +} from "./error.ts"; +import type { JoinRoomResponse } from "./websocket-types.ts"; + +export interface WrapperdEmitEvents { + commit: { req: PageCommit; res: PageCommitResponse; err: PageCommitError }; + "room:join": { + req: JoinRoomRequest; + res: JoinRoomResponse; + err: void; + }; + cursor: { + req: Omit; + res: void; + err: void; + }; +} + +export interface EmitOptions { + timeout?: number; +} + +/** + * Sends an event to the socket and returns a promise that resolves with the result. + * + * @template EventName - The name of the event to emit. + * @param socket - The socket to emit the event on. + * @param event - The name of the event to emit. + * @param data - The data to send with the event. + * @param options - Optional options for the emit operation. + * @returns A promise that resolves with the result of the emit operation. + */ +export const emit = ( + socket: Socket, + event: EventName, + data: WrapperdEmitEvents[EventName]["req"], + options?: EmitOptions, +): Promise< + Result< + WrapperdEmitEvents[EventName]["res"], + | WrapperdEmitEvents[EventName]["err"] + | TimeoutError + | SocketIOServerDisconnectError + | UnexpectedRequestError + > +> => { + if (event === "cursor") { + socket.emit<"cursor">(event, data as WrapperdEmitEvents["cursor"]["req"]); + return Promise.resolve(createOk(undefined)); + } + + // [socket.io-request](https://github.com/shokai/socket.io-request)で処理されているイベント + // 同様の実装をすればいい + const { resolve, promise, reject } = Promise.withResolvers< + Result< + WrapperdEmitEvents[EventName]["res"], + | WrapperdEmitEvents[EventName]["err"] + | TimeoutError + | SocketIOServerDisconnectError + | UnexpectedRequestError + > + >(); + + const dispose = () => { + socket.off("disconnect", onDisconnect); + clearTimeout(timeoutId); + }; + const onDisconnect = (reason: Socket.DisconnectReason) => { + // "commit"および"room:join"で"io client disconnect"が発生することはない + if (reason === "io client disconnect") { + dispose(); + reject(new Error("io client disconnect")); + return; + } + // 復帰不能なエラー + if (reason === "io server disconnect") { + dispose(); + resolve(createErr({ name: "SocketIOError" })); + return; + } + // Ignore other reasons because socket.io will automatically reconnect + }; + socket.on("disconnect", onDisconnect); + const timeout = options?.timeout ?? 90000; + const timeoutId = setTimeout(() => { + dispose(); + resolve( + createErr({ + name: "TimeoutError", + message: `exceeded ${timeout} (ms)`, + }), + ); + }, timeout); + + const payload = event === "commit" + ? { method: "commit" as const, data: data as PageCommit } + : { method: "room:join" as const, data: data as JoinRoomRequest }; + + socket.emit("socket.io-request", payload, (res) => { + dispose(); + if ("error" in res) { + resolve( + createErr( + isPageCommitError(res.error) + ? res.error + : { name: "UnexpectedRequestError", ...res }, + ), + ); + return; + } + resolve(createOk(res.data)); + }); + + return promise; +}; diff --git a/browser/websocket/error.ts b/browser/websocket/error.ts new file mode 100644 index 0000000..04327e6 --- /dev/null +++ b/browser/websocket/error.ts @@ -0,0 +1,49 @@ +import type { JsonValue } from "@std/json"; + +/** the error that occurs when scrapbox.io throws serializable {@link Error} */ +export interface UnexpectedRequestError { + name: "UnexpectedRequestError"; + error: JsonValue; +} + +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 socket.io throws "io" server disconnect" */ +export interface SocketIOServerDisconnectError { + name: "SocketIOServerDisconnectError"; +} + +/** 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: { name: string }, +): error is PageCommitError => pageCommitErrorNames.includes(error.name); + +const pageCommitErrorNames = [ + "SocketIOError", + "DuplicateTitleError", + "NotFastForwardError", +]; diff --git a/browser/websocket/listen.ts b/browser/websocket/listen.ts index 66766db..cf1640a 100644 --- a/browser/websocket/listen.ts +++ b/browser/websocket/listen.ts @@ -4,17 +4,16 @@ import type { NotLoggedInError, NotMemberError, } from "@cosense/types/rest"; -import { - type ProjectUpdatesStreamCommit, - type ProjectUpdatesStreamEvent, - type Socket, - socketIO, - wrap, -} from "./wrap.ts"; +import type { + ProjectUpdatesStreamCommit, + ProjectUpdatesStreamEvent, +} from "./emit.ts"; import type { HTTPError } from "../../rest/responseIntoResult.ts"; import type { AbortError, NetworkError } from "../../rest/robustFetch.ts"; import { getProjectId } from "./pull.ts"; import { connect, disconnect } from "./socket.ts"; +import type { Socket } from "socket.io-client"; + export type { ProjectUpdatesStreamCommit, ProjectUpdatesStreamEvent, @@ -24,6 +23,14 @@ export interface ListenStreamOptions { socket?: Socket; } +export type ListenStreamError = + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError; + /** Streamを購読する * * @param project 購読したいproject @@ -37,12 +44,7 @@ export async function* listenStream( ): AsyncGenerator< Result< ProjectUpdatesStreamEvent | ProjectUpdatesStreamCommit, - | NotFoundError - | NotLoggedInError - | NotMemberError - | NetworkError - | AbortError - | HTTPError + ListenStreamError >, void, unknown @@ -55,8 +57,9 @@ export async function* listenStream( const projectId = unwrapOk(result); const injectedSocket = options?.socket; - const socket = injectedSocket ?? await socketIO(); - await connect(socket); + const result2 = await connect(injectedSocket); + if (isErr(result2)) throw new Error("Failed to connect to websocket"); + const socket = unwrapOk(result2); const { request, response } = wrap(socket); try { diff --git a/browser/websocket/patch.ts b/browser/websocket/patch.ts index 4e0fae9..ace5de7 100644 --- a/browser/websocket/patch.ts +++ b/browser/websocket/patch.ts @@ -1,9 +1,10 @@ -import type { Change, DeletePageChange, PinChange } from "./wrap.ts"; +import type { Change, DeletePageChange, PinChange } from "./emit.ts"; import { makeChanges } from "./makeChanges.ts"; import type { BaseLine, Page } from "@cosense/types/rest"; import { push, type PushError, type PushOptions } from "./push.ts"; import { suggestUnDupTitle } from "./suggestUnDupTitle.ts"; import type { Result } from "option-t/plain_result"; +import type { Socket } from "socket.io-client"; export type PatchOptions = PushOptions; @@ -32,7 +33,7 @@ export const patch = ( metadata: PatchMetadata, ) => string[] | undefined | Promise, options?: PatchOptions, -): Promise> => +): Promise> => push( project, title, diff --git a/browser/websocket/pin.ts b/browser/websocket/pin.ts index 9be4685..7fc19b7 100644 --- a/browser/websocket/pin.ts +++ b/browser/websocket/pin.ts @@ -1,5 +1,5 @@ import type { Result } from "option-t/plain_result"; -import type { Change, Socket } from "./wrap.ts"; +import type { Change } from "./emit.ts"; import { push, type PushError, type PushOptions } from "./push.ts"; export interface PinOptions extends PushOptions { @@ -39,9 +39,7 @@ export const pin = ( options, ); -export interface UnPinOptions { - socket?: Socket; -} +export interface UnPinOptions extends PushOptions {} /** 指定したページのピン留めを外す * * @param project ピン留めを外したいページのproject diff --git a/browser/websocket/push.ts b/browser/websocket/push.ts index 5cf3dda..258aaf0 100644 --- a/browser/websocket/push.ts +++ b/browser/websocket/push.ts @@ -1,16 +1,12 @@ -import { - type Change, - type DeletePageChange, - type PageCommit, - type PageCommitError, - type PageCommitResponse, - type PinChange, - type Socket, - socketIO, - type TimeoutError, - wrap, -} from "./wrap.ts"; +import type { + Change, + DeletePageChange, + PageCommit, + PinChange, +} from "./websocket-types.ts"; import { connect, disconnect } from "./socket.ts"; +import type { Socket } from "socket.io-client"; +import { emit } from "./emit.ts"; import { pull } from "./pull.ts"; import type { ErrorLike, @@ -31,6 +27,10 @@ import { import type { HTTPError } from "../../rest/responseIntoResult.ts"; import type { AbortError, NetworkError } from "../../rest/robustFetch.ts"; import type { TooLongURIError } from "../../rest/pages.ts"; +import type { + SocketIOServerDisconnectError, + UnexpectedRequestError, +} from "./error.ts"; export interface PushOptions { /** 外部からSocketを指定したいときに使う */ @@ -60,7 +60,8 @@ export interface UnexpectedError extends ErrorLike { export type PushError = | RetryError - | UnexpectedError + | SocketIOServerDisconnectError + | UnexpectedRequestError | NotFoundError | NotLoggedInError | Omit @@ -95,15 +96,19 @@ export const push = async ( | [PinChange], options?: PushOptions, ): Promise> => { - const injectedSocket = options?.socket; - const socket = injectedSocket ?? await socketIO(); - await connect(socket); + const result = await connect(options?.socket); + if (isErr(result)) { + return createErr({ + name: "UnexpectedRequestError", + error: unwrapErr(result), + }); + } + const socket = unwrapOk(result); const pullResult = await pull(project, title); if (isErr(pullResult)) return pullResult; let metadata = unwrapOk(pullResult); try { - const { request } = wrap(socket); let attempts = 0; let changes: Change[] | [DeletePageChange] | [PinChange] = []; let reason: "NotFastForwardError" | "DuplicateTitleError" | undefined; @@ -130,14 +135,7 @@ export const push = async ( // loop for push changes while (true) { - const result = (await request("socket.io-request", { - method: "commit", - data, - })) as Result< - PageCommitResponse, - UnexpectedError | TimeoutError | PageCommitError - >; - + const result = await emit(socket, "commit", data); if (createOk(result)) { metadata.commitId = unwrapOk(result).commitId; // success @@ -145,8 +143,11 @@ export const push = async ( } const error = unwrapErr(result); const name = error.name; - if (name === "UnexpectedError") { - return createErr({ name, message: JSON.stringify(error) }); + if ( + name === "SocketIOServerDisconnectError" || + name === "UnexpectedRequestError" + ) { + return createErr(error); } if (name === "TimeoutError" || name === "SocketIOError") { await delay(3000); @@ -171,6 +172,6 @@ export const push = async ( message: `Retrying exceeded the maxAttempts (${attempts}).`, }); } finally { - if (!injectedSocket) await disconnect(socket); + if (!options?.socket) await disconnect(socket); } }; diff --git a/browser/websocket/socket-io.ts b/browser/websocket/socket-io.ts deleted file mode 100644 index 8fb5652..0000000 --- a/browser/websocket/socket-io.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { - Manager, - ManagerOptions, - Socket, - SocketOptions, -} from "./@types/socket-io/index.ts"; -export type { Manager, ManagerOptions, Socket, SocketOptions }; - -export const socketIO = async (): Promise => { - const io = await importSocketIO(); - const socket = io("https://scrapbox.io", { - reconnectionDelay: 5000, - transports: ["websocket"], - }); - - await new Promise((resolve, reject) => { - const onDisconnect = (reason: Socket.DisconnectReason) => reject(reason); - socket.once("connect", () => { - socket.off("disconnect", onDisconnect); - resolve(); - }); - socket.once("disconnect", onDisconnect); - }); - return socket; -}; - -type IO = ( - uri: string, - opts?: Partial, -) => Socket; -declare const io: IO | undefined; -const version = "4.2.0"; -const url = - `https://cdnjs.cloudflare.com/ajax/libs/socket.io/${version}/socket.io.min.js`; -let error: string | Event | undefined; - -const importSocketIO = async (): Promise => { - if (error) throw error; - if (!document.querySelector(`script[src="${url}"]`)) { - const script = document.createElement("script"); - script.src = url; - await new Promise((resolve, reject) => { - script.onload = () => resolve(); - script.onerror = (e) => { - error = e; - reject(e); - }; - document.head.append(script); - }); - } - - return new Promise((resolve) => { - const id = setInterval(() => { - if (!io) return; - clearInterval(id); - resolve(io); - }, 500); - }); -}; diff --git a/browser/websocket/socket.ts b/browser/websocket/socket.ts index 02a7481..790dd57 100644 --- a/browser/websocket/socket.ts +++ b/browser/websocket/socket.ts @@ -1,38 +1,78 @@ -import { type Socket, socketIO } from "./socket-io.ts"; -export type { Socket } from "./socket-io.ts"; +import { io, type Socket } from "socket.io-client"; +import { createErr, createOk, type Result } from "option-t/plain_result"; +import type { EmitEvents, ListenEvents } from "./websocket-types.ts"; -/** 新しいsocketを作る */ -export const makeSocket = (): Promise => socketIO(); - -/** websocketに(再)接続する +/** connect to websocket * - * @param socket 接続したいsocket + * @param socket - The socket to be connected. If not provided, a new socket will be created + * @returns A promise that resolves to a socket if connected successfully, or an error if failed */ -export const connect = async (socket: Socket): Promise => { - if (socket.connected) return; +export const connect = (socket?: Socket): Promise< + Result, Socket.DisconnectReason> +> => { + if (socket?.connected) return Promise.resolve(createOk(socket)); + socket ??= io("https://scrapbox.io", { + reconnectionDelay: 5000, + transports: ["websocket"], + }); - const waiting = new Promise((resolve) => - socket.once("connect", () => resolve()) + const promise = new Promise< + Result, Socket.DisconnectReason> + >( + (resolve) => { + const onDisconnect = (reason: Socket.DisconnectReason) => + resolve(createErr(reason)); + socket.once("connect", () => { + socket.off("disconnect", onDisconnect); + resolve(createOk(socket)); + }); + socket.once("disconnect", onDisconnect); + }, ); socket.connect(); - await waiting; + return promise; }; -/** websocketを切断する +/** Disconnect the websocket * - * @param socket 切断したいsocket + * @param socket - The socket to be disconnected */ -export const disconnect = async (socket: Socket): Promise => { - if (socket.disconnected) return; +export const disconnect = ( + socket: Socket, +): Promise< + Result< + void, + | "io server disconnect" + | "ping timeout" + | "transport close" + | "transport error" + | "parse error" + > +> => { + if (socket.disconnected) return Promise.resolve(createOk(undefined)); - const waiting = new Promise((resolve) => { - const onDisconnect = (reason: Socket.DisconnectReason) => { - if (reason !== "io client disconnect") return; - resolve(); - socket.off("disconnect", onDisconnect); - }; - socket.on("disconnect", onDisconnect); - }); + const promise = new Promise< + Result< + void, + | "io server disconnect" + | "ping timeout" + | "transport close" + | "transport error" + | "parse error" + > + >( + (resolve) => { + const onDisconnect = (reason: Socket.DisconnectReason) => { + if (reason !== "io client disconnect") { + resolve(createErr(reason)); + return; + } + resolve(createOk(undefined)); + socket.off("disconnect", onDisconnect); + }; + socket.on("disconnect", onDisconnect); + }, + ); socket.disconnect(); - await waiting; + return promise; }; diff --git a/browser/websocket/websocket-types.ts b/browser/websocket/websocket-types.ts index b965e74..211851a 100644 --- a/browser/websocket/websocket-types.ts +++ b/browser/websocket/websocket-types.ts @@ -108,66 +108,21 @@ export interface PageCommit { 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 interface EventMap { +export interface EmitEvents { "socket.io-request": ( req: { method: "commit"; data: PageCommit } | { method: "room:join"; data: JoinRoomRequest; }, - success: PageCommitResponse | JoinRoomResponse, - failed: PageCommitError, - ) => void; - cursor: ( - req: Omit, - success: undefined, - failed: unknown, + callback: ( + res: + | { data: PageCommitResponse | JoinRoomResponse } + | { error: { name: string; message?: string } }, + ) => void, ) => void; + cursor: (req: Omit) => void; } -export interface ListenEventMap { +export interface ListenEvents { "projectUpdatesStream:commit": ProjectUpdatesStreamCommit; "projectUpdatesStream:event": ProjectUpdatesStreamEvent; commit: CommitNotification; @@ -190,16 +145,6 @@ export interface QuickSearchReplaceLink { to: string; } -export type DataOf = Parameters< - EventMap[Event] ->[0]; -export type SuccessResOf = Parameters< - EventMap[Event] ->[1]; -export type FailedResOf = Parameters< - EventMap[Event] ->[2]; - export interface MoveCursorData { user: { id: string; diff --git a/browser/websocket/wrap.ts b/browser/websocket/wrap.ts deleted file mode 100644 index 5c9c988..0000000 --- a/browser/websocket/wrap.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { createErr, createOk, type Result } from "option-t/plain_result"; -import type { Socket } from "./socket-io.ts"; -import { - type DataOf, - type EventMap, - type FailedResOf, - isPageCommitError, - type ListenEventMap, - type SuccessResOf, - type TimeoutError, - type UnexpectedError, -} from "./websocket-types.ts"; -export * from "./websocket-types.ts"; -export * from "./socket-io.ts"; - -export interface SocketOperator { - request: ( - event: EventName, - data: DataOf, - ) => Promise< - Result< - SuccessResOf, - FailedResOf | UnexpectedError | TimeoutError - > - >; - response: ( - ...events: EventName[] - ) => AsyncGenerator; -} - -export const wrap = ( - socket: Socket, - timeout = 90000, -): SocketOperator => { - const request = ( - event: EventName, - data: DataOf, - ): Promise< - Result< - SuccessResOf, - FailedResOf | UnexpectedError | TimeoutError - > - > => { - let id: number | undefined; - return new Promise((resolve, reject) => { - const onDisconnect = (message: string) => { - clearTimeout(id); - reject(new Error(message)); - }; - 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(createErr(response.error)); - } else { - resolve( - createErr(unexpectedError(response.error)), - ); - } - } else if ("data" in response) { - resolve(createOk(response.data)); - } - break; - case "cursor": - if ("error" in response) { - resolve( - createErr(unexpectedError(response.error)), - ); - } else if ("data" in response) { - resolve(createOk(response.data)); - } - break; - } - reject( - new Error( - 'Invalid response: missing "data" or "error" field', - ), - ); - }, - ); - id = setTimeout(() => { - socket.off("disconnect", onDisconnect); - resolve( - createErr({ - name: "TimeoutError", - message: `Timeout: exceeded ${timeout}ms`, - }), - ); - }, timeout); - socket.once("disconnect", onDisconnect); - }); - }; - - async function* response( - ...events: EventName[] - ) { - type Data = ListenEventMap[EventName]; - let _resolve: ((data: Data) => void) | undefined; - const waitForEvent = () => new Promise((res) => _resolve = res); - const resolve = (data: Data) => { - _resolve?.(data); - }; - - for (const event of events) { - socket.on( - event, - // @ts-ignore 何故か型推論に失敗する - resolve, - ); - } - try { - while (true) { - yield await waitForEvent(); - } - } finally { - for (const event of events) { - socket.off(event, resolve); - } - } - } - - return { request, response }; -}; - -const unexpectedError = (value: unknown): UnexpectedError => ({ - name: "UnexpectedError", - value, -}); diff --git a/deno.jsonc b/deno.jsonc index c5a243e..8803db1 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -17,10 +17,12 @@ "@std/assert": "jsr:@std/assert@1", "@std/async": "jsr:@std/async@1", "@std/encoding": "jsr:@std/encoding@1", - "@takker/md5": "jsr:@takker/md5@0.1", + "@std/json": "jsr:@std/json@^1.0.0", "@std/testing/snapshot": "jsr:@std/testing@1/snapshot", + "@takker/md5": "jsr:@takker/md5@0.1", "@takker/onp": "./vendor/raw.githubusercontent.com/takker99/onp/0.0.1/mod.ts", - "option-t": "npm:option-t@^49.1.0" + "option-t": "npm:option-t@^49.1.0", + "socket.io-client": "npm:socket.io-client@^4.7.5" }, "exports": { ".": "./mod.ts", diff --git a/deno.lock b/deno.lock index 3a80945..00e131c 100644 --- a/deno.lock +++ b/deno.lock @@ -8,15 +8,16 @@ "jsr:@std/assert@1": "jsr:@std/assert@1.0.2", "jsr:@std/assert@^1.0.2": "jsr:@std/assert@1.0.2", "jsr:@std/async@1": "jsr:@std/async@1.0.3", - "jsr:@std/async@^1.0.2": "jsr:@std/async@1.0.3", - "jsr:@std/data-structures@^1.0.1": "jsr:@std/data-structures@1.0.1", - "jsr:@std/encoding@1": "jsr:@std/encoding@1.0.2", + "jsr:@std/encoding@1": "jsr:@std/encoding@1.0.1", "jsr:@std/fs@^1.0.1": "jsr:@std/fs@1.0.1", "jsr:@std/internal@^1.0.1": "jsr:@std/internal@1.0.1", + "jsr:@std/json@^1.0.0": "jsr:@std/json@1.0.0", "jsr:@std/path@^1.0.2": "jsr:@std/path@1.0.2", + "jsr:@std/streams@^1.0.0": "jsr:@std/streams@1.0.0", "jsr:@std/testing@1": "jsr:@std/testing@1.0.0", "jsr:@takker/md5@0.1": "jsr:@takker/md5@0.1.0", - "npm:option-t@^49.1.0": "npm:option-t@49.1.0" + "npm:option-t@^49.1.0": "npm:option-t@49.1.0", + "npm:socket.io-client@^4.7.5": "npm:socket.io-client@4.7.5" }, "jsr": { "@core/unknownutil@4.2.0": { @@ -37,11 +38,8 @@ "@std/async@1.0.3": { "integrity": "6ed64678db43451683c6c176a21426a2ccd21ba0269ebb2c36133ede3f165792" }, - "@std/data-structures@1.0.1": { - "integrity": "e4fa6bcc33839979ac118e2746f349cd7b57c58bd3036b5b82ac608771ee856e" - }, - "@std/encoding@1.0.2": { - "integrity": "7ed640c777e3275550e2cd937c440acdbebfdcd2d13ef67052f0536bf43e707f" + "@std/encoding@1.0.1": { + "integrity": "5955c6c542ebb4ce6587c3b548dc71e07a6c27614f1976d1d3887b1196cf4e65" }, "@std/fs@1.0.1": { "integrity": "d6914ca2c21abe591f733b31dbe6331e446815e513e2451b3b9e472daddfefcb", @@ -52,15 +50,22 @@ "@std/internal@1.0.1": { "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" }, + "@std/json@1.0.0": { + "integrity": "985c1e544918d42e4e84072fc739ac4a19c3a5093292c99742ffcdd03fb6a268", + "dependencies": [ + "jsr:@std/streams@^1.0.0" + ] + }, "@std/path@1.0.2": { "integrity": "a452174603f8c620bd278a380c596437a9eef50c891c64b85812f735245d9ec7" }, + "@std/streams@1.0.0": { + "integrity": "350242b8fad9874ed45f3c42df3d132bd0a958f8a8bae9bbfa1ff039716aa6fb" + }, "@std/testing@1.0.0": { "integrity": "27cfc06392c69c2acffe54e6d0bcb5f961cf193f519255372bd4fff1481bfef8", "dependencies": [ "jsr:@std/assert@^1.0.2", - "jsr:@std/async@^1.0.2", - "jsr:@std/data-structures@^1.0.1", "jsr:@std/fs@^1.0.1", "jsr:@std/internal@^1.0.1", "jsr:@std/path@^1.0.2" @@ -71,9 +76,61 @@ } }, "npm": { + "@socket.io/component-emitter@3.1.2": { + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dependencies": {} + }, + "debug@4.3.6": { + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "ms@2.1.2" + } + }, + "engine.io-client@6.5.4": { + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dependencies": { + "@socket.io/component-emitter": "@socket.io/component-emitter@3.1.2", + "debug": "debug@4.3.6", + "engine.io-parser": "engine.io-parser@5.2.3", + "ws": "ws@8.17.1", + "xmlhttprequest-ssl": "xmlhttprequest-ssl@2.0.0" + } + }, + "engine.io-parser@5.2.3": { + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dependencies": {} + }, + "ms@2.1.2": { + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dependencies": {} + }, "option-t@49.1.0": { "integrity": "sha512-K5o4+D8rSE1VcmfwrieRZWyShPxX27NEGuZPT11S4CbvjE73YUDR/sanKAYnhVRj6Hn1mKcjfhjhbOCD2ef3qg==", "dependencies": {} + }, + "socket.io-client@4.7.5": { + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dependencies": { + "@socket.io/component-emitter": "@socket.io/component-emitter@3.1.2", + "debug": "debug@4.3.6", + "engine.io-client": "engine.io-client@6.5.4", + "socket.io-parser": "socket.io-parser@4.2.4" + } + }, + "socket.io-parser@4.2.4": { + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "@socket.io/component-emitter@3.1.2", + "debug": "debug@4.3.6" + } + }, + "ws@8.17.1": { + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dependencies": {} + }, + "xmlhttprequest-ssl@2.0.0": { + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dependencies": {} } } }, @@ -86,9 +143,11 @@ "jsr:@std/assert@1", "jsr:@std/async@1", "jsr:@std/encoding@1", + "jsr:@std/json@^1.0.0", "jsr:@std/testing@1", "jsr:@takker/md5@0.1", - "npm:option-t@^49.1.0" + "npm:option-t@^49.1.0", + "npm:socket.io-client@^4.7.5" ] } } From b1d9a2e16765a7cddce4443f8301acfeccd68b69 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Sun, 15 Sep 2024 16:45:38 +0900 Subject: [PATCH 2/3] refactor(BREAKING): remove `listenStream` and add `listen` that add event listners for socket.io --- browser/websocket/applyCommit.ts | 2 +- browser/websocket/change.ts | 68 ++++++++ browser/websocket/diffToChanges.ts | 6 +- browser/websocket/emit-events.ts | 75 +++++++++ browser/websocket/emit.ts | 10 +- browser/websocket/listen-events.ts | 106 ++++++++++++ browser/websocket/listen.ts | 70 +++----- browser/websocket/makeChanges.ts | 2 +- browser/websocket/mod.ts | 3 + browser/websocket/patch.ts | 2 +- browser/websocket/pin.ts | 2 +- browser/websocket/push.ts | 8 +- browser/websocket/socket.ts | 14 +- browser/websocket/updateCodeBlock.ts | 6 +- browser/websocket/updateCodeFile.ts | 6 +- browser/websocket/websocket-types.ts | 230 --------------------------- 16 files changed, 294 insertions(+), 316 deletions(-) create mode 100644 browser/websocket/change.ts create mode 100644 browser/websocket/emit-events.ts create mode 100644 browser/websocket/listen-events.ts delete mode 100644 browser/websocket/websocket-types.ts diff --git a/browser/websocket/applyCommit.ts b/browser/websocket/applyCommit.ts index 2d40cef..f091721 100644 --- a/browser/websocket/applyCommit.ts +++ b/browser/websocket/applyCommit.ts @@ -1,4 +1,4 @@ -import type { CommitNotification } from "./websocket-types.ts"; +import type { CommitNotification } from "./listen-events.ts"; import type { BaseLine } from "@cosense/types/rest"; import { getUnixTimeFromId } from "./id.ts"; diff --git a/browser/websocket/change.ts b/browser/websocket/change.ts new file mode 100644 index 0000000..7ffe7c4 --- /dev/null +++ b/browser/websocket/change.ts @@ -0,0 +1,68 @@ +export type Change = + | InsertChange + | UpdateChange + | DeleteChange + | LinksChange + | ProjectLinksChange + | IconsChange + | DescriptionsChange + | ImageChange + | FilesChange + | HelpFeelsChange + | infoboxDefinitionChange + | TitleChange; +export interface InsertChange { + _insert: string; + lines: { + id: string; + text: string; + }; +} +export interface UpdateChange { + _update: string; + lines: { + text: string; + }; + noTimestampUpdate?: unknown; +} +export interface DeleteChange { + _delete: string; + lines: -1; +} +export interface LinksChange { + links: string[]; +} +export interface ProjectLinksChange { + projectLinks: string[]; +} +export interface IconsChange { + icons: string[]; +} +export interface DescriptionsChange { + descriptions: string[]; +} +export interface ImageChange { + image: string | null; +} +export interface TitleChange { + title: string; +} +export interface FilesChange { + /** file id */ + files: string[]; +} +export interface HelpFeelsChange { + /** Helpfeel記法の先頭の`? `をとったもの */ + helpfeels: string[]; +} +export interface infoboxDefinitionChange { + /** `table:infobox`または`table:cosense`の各行をtrimしたもの */ + infoboxDefinition: string[]; +} +export interface PinChange { + pin: number; +} +export interface DeletePageChange { + deleted: true; + merged?: true; +} diff --git a/browser/websocket/diffToChanges.ts b/browser/websocket/diffToChanges.ts index 0133495..dffa82b 100644 --- a/browser/websocket/diffToChanges.ts +++ b/browser/websocket/diffToChanges.ts @@ -1,10 +1,6 @@ import { diff, toExtendedChanges } from "../../deps/onp.ts"; import type { Line } from "@cosense/types/userscript"; -import type { - DeleteChange, - InsertChange, - UpdateChange, -} from "./websocket-types.ts"; +import type { DeleteChange, InsertChange, UpdateChange } from "./change.ts"; import { createNewLineId } from "./id.ts"; type Options = { diff --git a/browser/websocket/emit-events.ts b/browser/websocket/emit-events.ts new file mode 100644 index 0000000..a54da90 --- /dev/null +++ b/browser/websocket/emit-events.ts @@ -0,0 +1,75 @@ +import type { Change, DeletePageChange, PinChange } from "./change.ts"; + +export interface EmitEvents { + "socket.io-request": ( + req: { method: "commit"; data: PageCommit } | { + method: "room:join"; + data: JoinRoomRequest; + }, + callback: ( + res: + | { data: PageCommitResponse | JoinRoomResponse } + | { error: { name: string; message?: string } }, + ) => void, + ) => void; + cursor: (req: Omit) => void; +} + +export interface PageCommit { + kind: "page"; + parentId: string; + projectId: string; + pageId: string; + userId: string; + changes: Change[] | [PinChange] | [DeletePageChange]; + cursor?: null; + freeze: true; +} + +export interface PageCommitResponse { + commitId: string; +} + +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 { + success: true; + pageId: string | null; + projectId: string; +} + +export interface MoveCursorData { + user: { + id: string; + name: string; + displayName: string; + }; + pageId: string; + position: { + line: number; + char: number; + }; + visible: boolean; + socketId: string; +} diff --git a/browser/websocket/emit.ts b/browser/websocket/emit.ts index 920377f..bc5c142 100644 --- a/browser/websocket/emit.ts +++ b/browser/websocket/emit.ts @@ -1,14 +1,11 @@ import { createErr, createOk, type Result } from "option-t/plain_result"; import type { Socket } from "socket.io-client"; import type { - EmitEvents, JoinRoomRequest, - ListenEvents, MoveCursorData, PageCommit, PageCommitResponse, -} from "./websocket-types.ts"; -export * from "./websocket-types.ts"; +} from "./emit-events.ts"; import { isPageCommitError, type PageCommitError, @@ -16,7 +13,8 @@ import { type TimeoutError, type UnexpectedRequestError, } from "./error.ts"; -import type { JoinRoomResponse } from "./websocket-types.ts"; +import type { JoinRoomResponse } from "./emit-events.ts"; +import type { ScrapboxSocket } from "./socket.ts"; export interface WrapperdEmitEvents { commit: { req: PageCommit; res: PageCommitResponse; err: PageCommitError }; @@ -47,7 +45,7 @@ export interface EmitOptions { * @returns A promise that resolves with the result of the emit operation. */ export const emit = ( - socket: Socket, + socket: ScrapboxSocket, event: EventName, data: WrapperdEmitEvents[EventName]["req"], options?: EmitOptions, diff --git a/browser/websocket/listen-events.ts b/browser/websocket/listen-events.ts new file mode 100644 index 0000000..c2d60f2 --- /dev/null +++ b/browser/websocket/listen-events.ts @@ -0,0 +1,106 @@ +import type { MoveCursorData, PageCommit } from "./emit-events.ts"; +import type { + DeleteChange, + DeletePageChange, + DescriptionsChange, + IconsChange, + ImageChange, + InsertChange, + LinksChange, + TitleChange, + UpdateChange, +} from "./change.ts"; + +export interface ListenEvents { + "projectUpdatesStream:commit": (event: ProjectUpdatesStreamCommit) => void; + "projectUpdatesStream:event": (event: ProjectUpdatesStreamEvent) => void; + commit: (event: CommitNotification) => void; + cursor: (event: MoveCursorData) => void; + "quick-search:commit": (event: QuickSearchCommit) => void; + "quick-search:replace-link": QuickSearchReplaceLink; + "infobox:updating": boolean; + "infobox:reload": void; + "literal-database:reload": void; +} + +export interface ProjectUpdatesStreamCommit { + kind: "page"; + id: string; + parentId: string; + projectId: string; + pageId: string; + userId: string; + changes: + | ( + | InsertChange + | UpdateChange + | DeleteChange + | TitleChange + | LinksChange + | IconsChange + )[] + | [DeletePageChange]; + cursor: null; + freeze: true; +} + +export type ProjectUpdatesStreamEvent = + | PageDeleteEvent + | MemberJoinEvent + | MemberAddEvent + | AdminAddEvent + | AdminDeleteEvent + | OwnerSetEvent + | InvitationResetEvent; + +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 MemberAddEvent extends ProjectEvent { + type: "member.add"; +} +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 QuickSearchCommit extends Omit { + changes: + | (TitleChange | LinksChange | DescriptionsChange | ImageChange)[] + | [DeletePageChange]; +} + +export interface QuickSearchReplaceLink { + from: string; + to: string; +} diff --git a/browser/websocket/listen.ts b/browser/websocket/listen.ts index cf1640a..7e8da89 100644 --- a/browser/websocket/listen.ts +++ b/browser/websocket/listen.ts @@ -1,26 +1,21 @@ -import { createOk, isErr, type Result, unwrapOk } from "option-t/plain_result"; import type { NotFoundError, NotLoggedInError, NotMemberError, } from "@cosense/types/rest"; -import type { - ProjectUpdatesStreamCommit, - ProjectUpdatesStreamEvent, -} from "./emit.ts"; import type { HTTPError } from "../../rest/responseIntoResult.ts"; import type { AbortError, NetworkError } from "../../rest/robustFetch.ts"; -import { getProjectId } from "./pull.ts"; -import { connect, disconnect } from "./socket.ts"; -import type { Socket } from "socket.io-client"; +import type { ScrapboxSocket } from "./socket.ts"; +import type { ListenEvents } from "./listen-events.ts"; export type { ProjectUpdatesStreamCommit, ProjectUpdatesStreamEvent, -} from "./websocket-types.ts"; +} from "./listen-events.ts"; export interface ListenStreamOptions { - socket?: Socket; + signal?: AbortSignal; + once?: boolean; } export type ListenStreamError = @@ -37,46 +32,21 @@ export type ListenStreamError = * @param events 購読したいevent。配列で指定する * @param options 使用したいSocketがあれば指定する */ -export async function* listenStream( - project: string, - events: ["commit" | "event", ...("commit" | "event")[]], +export const listen = ( + socket: ScrapboxSocket, + event: EventName, + listener: ListenEvents[EventName], options?: ListenStreamOptions, -): AsyncGenerator< - Result< - ProjectUpdatesStreamEvent | ProjectUpdatesStreamCommit, - ListenStreamError - >, - void, - unknown -> { - const result = await getProjectId(project); - if (isErr(result)) { - yield result; - return; - } - const projectId = unwrapOk(result); +): void => { + if (options?.signal?.aborted) return; - const injectedSocket = options?.socket; - const result2 = await connect(injectedSocket); - if (isErr(result2)) throw new Error("Failed to connect to websocket"); - const socket = unwrapOk(result2); - const { request, response } = wrap(socket); + // deno-lint-ignore no-explicit-any + (options?.once ? socket.once : socket.on)(event, listener as any); - try { - // 部屋に入って購読し始める - await request("socket.io-request", { - method: "room:join", - data: { projectId, pageId: null, projectUpdatesStream: true }, - }); - - for await ( - const streamEvent of response( - ...events.map((event) => `projectUpdatesStream:${event}` as const), - ) - ) { - yield createOk(streamEvent); - } - } finally { - if (!injectedSocket) await disconnect(socket); - } -} + options?.signal?.addEventListener?.( + "abort", + // deno-lint-ignore no-explicit-any + () => socket.off(event, listener as any), + { once: true }, + ); +}; diff --git a/browser/websocket/makeChanges.ts b/browser/websocket/makeChanges.ts index ae748b6..51f0ce4 100644 --- a/browser/websocket/makeChanges.ts +++ b/browser/websocket/makeChanges.ts @@ -1,6 +1,6 @@ import { diffToChanges } from "./diffToChanges.ts"; import type { Page } from "@cosense/types/rest"; -import type { Change } from "./websocket-types.ts"; +import type { Change } from "./change.ts"; import { findMetadata, getHelpfeels } from "./findMetadata.ts"; import { isSameArray } from "./isSameArray.ts"; diff --git a/browser/websocket/mod.ts b/browser/websocket/mod.ts index 01c1cf2..3b84d01 100644 --- a/browser/websocket/mod.ts +++ b/browser/websocket/mod.ts @@ -5,3 +5,6 @@ export * from "./pin.ts"; export * from "./listen.ts"; export * from "./updateCodeBlock.ts"; export * from "./updateCodeFile.ts"; +export * from "./listen-events.ts"; +export * from "./emit-events.ts"; +export * from "./change.ts"; diff --git a/browser/websocket/patch.ts b/browser/websocket/patch.ts index ace5de7..8645680 100644 --- a/browser/websocket/patch.ts +++ b/browser/websocket/patch.ts @@ -1,4 +1,4 @@ -import type { Change, DeletePageChange, PinChange } from "./emit.ts"; +import type { Change, DeletePageChange, PinChange } from "./change.ts"; import { makeChanges } from "./makeChanges.ts"; import type { BaseLine, Page } from "@cosense/types/rest"; import { push, type PushError, type PushOptions } from "./push.ts"; diff --git a/browser/websocket/pin.ts b/browser/websocket/pin.ts index 7fc19b7..4c83029 100644 --- a/browser/websocket/pin.ts +++ b/browser/websocket/pin.ts @@ -1,5 +1,5 @@ import type { Result } from "option-t/plain_result"; -import type { Change } from "./emit.ts"; +import type { Change } from "./change.ts"; import { push, type PushError, type PushOptions } from "./push.ts"; export interface PinOptions extends PushOptions { diff --git a/browser/websocket/push.ts b/browser/websocket/push.ts index 258aaf0..2857133 100644 --- a/browser/websocket/push.ts +++ b/browser/websocket/push.ts @@ -1,9 +1,5 @@ -import type { - Change, - DeletePageChange, - PageCommit, - PinChange, -} from "./websocket-types.ts"; +import type { Change, DeletePageChange, PinChange } from "./change.ts"; +import type { PageCommit } from "./emit-events.ts"; import { connect, disconnect } from "./socket.ts"; import type { Socket } from "socket.io-client"; import { emit } from "./emit.ts"; diff --git a/browser/websocket/socket.ts b/browser/websocket/socket.ts index 790dd57..c4c38c4 100644 --- a/browser/websocket/socket.ts +++ b/browser/websocket/socket.ts @@ -1,14 +1,18 @@ import { io, type Socket } from "socket.io-client"; import { createErr, createOk, type Result } from "option-t/plain_result"; -import type { EmitEvents, ListenEvents } from "./websocket-types.ts"; +import type { ListenEvents } from "./listen-events.ts"; +import type { EmitEvents } from "./emit-events.ts"; + +/** A pre-configured {@linkcode Socket} type for Scrapbox */ +export type ScrapboxSocket = Socket; /** connect to websocket * * @param socket - The socket to be connected. If not provided, a new socket will be created * @returns A promise that resolves to a socket if connected successfully, or an error if failed */ -export const connect = (socket?: Socket): Promise< - Result, Socket.DisconnectReason> +export const connect = (socket?: ScrapboxSocket): Promise< + Result > => { if (socket?.connected) return Promise.resolve(createOk(socket)); socket ??= io("https://scrapbox.io", { @@ -17,7 +21,7 @@ export const connect = (socket?: Socket): Promise< }); const promise = new Promise< - Result, Socket.DisconnectReason> + Result >( (resolve) => { const onDisconnect = (reason: Socket.DisconnectReason) => @@ -38,7 +42,7 @@ export const connect = (socket?: Socket): Promise< * @param socket - The socket to be disconnected */ export const disconnect = ( - socket: Socket, + socket: ScrapboxSocket, ): Promise< Result< void, diff --git a/browser/websocket/updateCodeBlock.ts b/browser/websocket/updateCodeBlock.ts index ca3b772..c788a94 100644 --- a/browser/websocket/updateCodeBlock.ts +++ b/browser/websocket/updateCodeBlock.ts @@ -1,9 +1,5 @@ import type { BaseLine } from "@cosense/types/rest"; -import type { - DeleteChange, - InsertChange, - UpdateChange, -} from "./websocket-types.ts"; +import type { DeleteChange, InsertChange, UpdateChange } from "./change.ts"; import type { TinyCodeBlock } from "../../rest/getCodeBlocks.ts"; import { diffToChanges } from "./diffToChanges.ts"; import { isSimpleCodeFile } from "./isSimpleCodeFile.ts"; diff --git a/browser/websocket/updateCodeFile.ts b/browser/websocket/updateCodeFile.ts index 87b7e24..0d55346 100644 --- a/browser/websocket/updateCodeFile.ts +++ b/browser/websocket/updateCodeFile.ts @@ -1,9 +1,5 @@ import type { BaseLine } from "@cosense/types/rest"; -import type { - DeleteChange, - InsertChange, - UpdateChange, -} from "./websocket-types.ts"; +import type { DeleteChange, InsertChange, UpdateChange } from "./change.ts"; import { getCodeBlocks, type TinyCodeBlock } from "../../rest/getCodeBlocks.ts"; import { createNewLineId } from "./id.ts"; import { diff, toExtendedChanges } from "../../deps/onp.ts"; diff --git a/browser/websocket/websocket-types.ts b/browser/websocket/websocket-types.ts deleted file mode 100644 index 211851a..0000000 --- a/browser/websocket/websocket-types.ts +++ /dev/null @@ -1,230 +0,0 @@ -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 { - success: true; - pageId: string | null; - projectId: string; -} - -export interface ProjectUpdatesStreamCommit { - kind: "page"; - id: string; - parentId: string; - projectId: string; - pageId: string; - userId: string; - changes: - | ( - | InsertChange - | UpdateChange - | DeleteChange - | TitleChange - | LinksChange - | IconsChange - )[] - | [DeletePageChange]; - cursor: null; - freeze: true; -} - -export type ProjectUpdatesStreamEvent = - | 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[] | [PinChange] | [DeletePageChange]; - cursor?: null; - freeze: true; -} -export interface PageCommitResponse { - commitId: string; -} -export interface EmitEvents { - "socket.io-request": ( - req: { method: "commit"; data: PageCommit } | { - method: "room:join"; - data: JoinRoomRequest; - }, - callback: ( - res: - | { data: PageCommitResponse | JoinRoomResponse } - | { error: { name: string; message?: string } }, - ) => void, - ) => void; - cursor: (req: Omit) => void; -} -export interface ListenEvents { - "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 interface MoveCursorData { - user: { - id: string; - name: string; - displayName: string; - }; - pageId: string; - position: { - line: number; - char: number; - }; - visible: boolean; - socketId: string; -} - -export type Change = - | InsertChange - | UpdateChange - | DeleteChange - | LinksChange - | ProjectLinksChange - | IconsChange - | DescriptionsChange - | ImageChange - | FilesChange - | HelpFeelsChange - | infoboxDefinitionChange - | TitleChange; -export interface InsertChange { - _insert: string; - lines: { - id: string; - text: string; - }; -} -export interface UpdateChange { - _update: string; - lines: { - text: string; - }; - noTimestampUpdate?: unknown; -} -export interface DeleteChange { - _delete: string; - lines: -1; -} -export interface LinksChange { - links: string[]; -} -export interface ProjectLinksChange { - projectLinks: string[]; -} -export interface IconsChange { - icons: string[]; -} -export interface DescriptionsChange { - descriptions: string[]; -} -export interface ImageChange { - image: string | null; -} -export interface TitleChange { - title: string; -} -export interface FilesChange { - /** file id */ - files: string[]; -} -export interface HelpFeelsChange { - /** Helpfeel記法の先頭の`? `をとったもの */ - helpfeels: string[]; -} -export interface infoboxDefinitionChange { - /** `table:infobox`または`table:cosense`の各行をtrimしたもの */ - infoboxDefinition: string[]; -} -export interface PinChange { - pin: number; -} -export interface DeletePageChange { - deleted: true; - merged?: true; -} From 397e8590415b6edee5dae6b555320b20e6b6bac4 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Sun, 15 Sep 2024 16:46:40 +0900 Subject: [PATCH 3/3] fix: mistake `createOk` for `isOk` --- browser/websocket/push.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/browser/websocket/push.ts b/browser/websocket/push.ts index 2857133..714adad 100644 --- a/browser/websocket/push.ts +++ b/browser/websocket/push.ts @@ -17,6 +17,7 @@ import { createOk, isErr, type Result, + isOk, unwrapErr, unwrapOk, } from "option-t/plain_result"; @@ -132,7 +133,7 @@ export const push = async ( // loop for push changes while (true) { const result = await emit(socket, "commit", data); - if (createOk(result)) { + if (isOk(result)) { metadata.commitId = unwrapOk(result).commitId; // success return createOk(metadata.commitId);