akanjs 3.0.0-alpha.44 → 3.0.0-alpha.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -81,6 +81,7 @@ export class FetchClient {
81
81
  readonly handler: Record<string, FetchHandler>;
82
82
  readonly slice: Record<string, SliceMeta> = {};
83
83
  readonly sortKeyMap = new Map<string, string[]>();
84
+ readonly #originWs = new Map<string, WsClient>();
84
85
  readonly #handlerStore: Record<string, FetchHandler> = {};
85
86
  readonly #handlerFactory = new Map<string, FetchHandlerFactory>();
86
87
  #sharedRegistryAppliedVersion = 0;
@@ -95,8 +96,7 @@ export class FetchClient {
95
96
  ) {
96
97
  this.origin = origin;
97
98
  this.http = new HttpClient(origin, ErrorCls);
98
- const wsUri = `${origin.replace("http://", "ws://").replace("https://", "wss://")}/ws`;
99
- this.ws = new WsClient(wsUri, ErrorCls);
99
+ this.ws = new WsClient(FetchClient.#makeWsUri(origin), ErrorCls);
100
100
  Object.assign(this.#handlerStore, handler);
101
101
  this.handler = this.#makeHandlerProxy();
102
102
  this.applySignal(serializedSignal);
@@ -158,6 +158,7 @@ export class FetchClient {
158
158
  this.ErrorCls = ErrorCls;
159
159
  this.http.setErrorConstructor(ErrorCls);
160
160
  this.ws.setErrorConstructor(ErrorCls);
161
+ for (const ws of this.#originWs.values()) ws.setErrorConstructor(ErrorCls);
161
162
  }
162
163
  applySignal(serializedSignal: { [key: string]: SerializedSignal }, { share = true }: { share?: boolean } = {}) {
163
164
  if (share && Object.keys(serializedSignal).length > 0) {
@@ -232,6 +233,8 @@ export class FetchClient {
232
233
  }
233
234
  disconnect() {
234
235
  this.ws.destroy();
236
+ for (const ws of this.#originWs.values()) ws.destroy();
237
+ this.#originWs.clear();
235
238
  }
236
239
  clone({ origin, connect = true, jwt }: { origin?: string; connect?: boolean; jwt?: string } = {}) {
237
240
  const instance = new FetchClient(origin ?? this.origin, {}, this.serializedSignal, this.ErrorCls);
@@ -245,6 +248,20 @@ export class FetchClient {
245
248
  setJwt(jwt: string | null) {
246
249
  this.jwt = jwt;
247
250
  this.ws.setJwt(jwt);
251
+ for (const ws of this.#originWs.values()) ws.setJwt(jwt);
252
+ }
253
+
254
+ #resolveWs(origin?: string) {
255
+ if (!origin) return this.ws;
256
+ const target = origin.replace(/\/+$/, "");
257
+ if (target === this.origin.replace(/\/+$/, "")) return this.ws;
258
+ const cached = this.#originWs.get(target);
259
+ if (cached) return cached;
260
+ const ws = new WsClient(FetchClient.#makeWsUri(target), this.ErrorCls);
261
+ this.#originWs.set(target, ws);
262
+ ws.setJwt(this.jwt);
263
+ ws.connect();
264
+ return ws;
248
265
  }
249
266
  #makeAuthHeaders(option?: FetchPolicy): Record<string, string> {
250
267
  if (option?.token) return { Authorization: `Bearer ${option.token}` };
@@ -338,13 +355,13 @@ export class FetchClient {
338
355
  handleEvent(parsedReturn);
339
356
  };
340
357
  wrappedListeners.set(handleEvent, wrapped);
341
- this.ws.subscribe({
358
+ const ws = this.#resolveWs(fetchPolicy?.origin);
359
+ ws.subscribe({
342
360
  key,
343
361
  data,
344
362
  handleEvent: wrapped,
345
363
  });
346
- return () =>
347
- this.ws.unsubscribe({ key, data, handleEvent: wrappedListeners.get(handleEvent) ?? handleEvent });
364
+ return () => ws.unsubscribe({ key, data, handleEvent: wrappedListeners.get(handleEvent) ?? handleEvent });
348
365
  };
349
366
  });
350
367
  return;
@@ -356,8 +373,9 @@ export class FetchClient {
356
373
  const serializerMap = this.#makeArgSerializer(endpoint.args);
357
374
  return (...argData: unknown[]) => {
358
375
  const args = argData.slice(0, msgArgLength);
376
+ const fetchPolicy = argData[msgArgLength] as FetchPolicy | undefined;
359
377
  const data = msgArgs.map((arg, idx) => serializerMap.get(arg.name)?.(args[idx]) ?? null);
360
- this.ws.emit(key, data);
378
+ this.#resolveWs(fetchPolicy?.origin).emit(key, data);
361
379
  };
362
380
  });
363
381
  this.#setHandlerFactory(`listen${capitalize(key)}`, () => {
@@ -369,8 +387,9 @@ export class FetchClient {
369
387
  handleEvent(parsedReturn);
370
388
  };
371
389
  wrappedListeners.set(handleEvent, wrapped);
372
- this.ws.on(key, wrapped);
373
- return () => this.ws.off(key, wrappedListeners.get(handleEvent) ?? handleEvent);
390
+ const ws = this.#resolveWs(fetchPolicy.origin);
391
+ ws.on(key, wrapped);
392
+ return () => ws.off(key, wrappedListeners.get(handleEvent) ?? handleEvent);
374
393
  }) as FetchHandler;
375
394
  });
376
395
  return;
@@ -380,6 +399,10 @@ export class FetchClient {
380
399
  break;
381
400
  }
382
401
  }
402
+ static #makeWsUri(origin: string) {
403
+ return `${origin.replace("http://", "ws://").replace("https://", "wss://")}/ws`;
404
+ }
405
+
383
406
  static paginationArgs: SerializedArg[] = [
384
407
  { type: "search", name: "skip", refName: "Int" },
385
408
  { type: "search", name: "limit", refName: "Int" },
@@ -41,6 +41,7 @@ export class WsClient {
41
41
  #listenerMap = new Map<string, Set<Listener>>();
42
42
  #destroyed = false;
43
43
  #connectRequested = false;
44
+ #outbox: string[] = [];
44
45
  #unconnectedWarnTimers = new Map<string, ReturnType<typeof setTimeout>>();
45
46
  #jwt: string | null = null;
46
47
  connected = false;
@@ -94,6 +95,9 @@ export class WsClient {
94
95
  const data: WebsocketReqData = { key: option.key, data: option.data, subscribe: true };
95
96
  this.#ws?.send(JSON.stringify(data));
96
97
  });
98
+ const queued = this.#outbox;
99
+ this.#outbox = [];
100
+ for (const frame of queued) this.#ws?.send(frame);
97
101
  };
98
102
  this.#ws.onmessage = (e) => {
99
103
  try {
@@ -197,6 +201,7 @@ export class WsClient {
197
201
  }
198
202
  for (const timer of this.#unconnectedWarnTimers.values()) clearTimeout(timer);
199
203
  this.#unconnectedWarnTimers.clear();
204
+ this.#outbox = [];
200
205
  this.#ws?.close();
201
206
  this.#ws = null;
202
207
  }
@@ -240,28 +245,31 @@ export class WsClient {
240
245
  `[akanjs] WebSocket is not connected. Call fetch.instance.connect(), or drop the root layout "wsConnect = false", before ${action} "${key}".`,
241
246
  );
242
247
  }
243
- #warnUnconnectedSubscribe(key: string) {
244
- if (this.#connectRequested || this.#unconnectedWarnTimers.has(key)) return;
248
+ #warnUnconnected(action: "emit" | "subscribe", key: string) {
249
+ const timerKey = `${action}:${key}`;
250
+ if (this.#connectRequested || this.#unconnectedWarnTimers.has(timerKey)) return;
245
251
  const timer = setTimeout(() => {
246
- this.#unconnectedWarnTimers.delete(key);
252
+ this.#unconnectedWarnTimers.delete(timerKey);
247
253
  if (this.#connectRequested || this.#destroyed) return;
248
- this.#warnNotConnected("subscribe", key);
254
+ this.#warnNotConnected(action, key);
249
255
  }, 0);
250
- this.#unconnectedWarnTimers.set(key, timer);
256
+ this.#unconnectedWarnTimers.set(timerKey, timer);
251
257
  }
252
258
  emit(key: string, data: WsRequestPayload) {
259
+ const payload: WebsocketReqData = { key, data: Array.isArray(data) ? data : [data] };
260
+ const frame = JSON.stringify(payload);
261
+
253
262
  if (this.#ws?.readyState !== WebSocket.OPEN) {
254
- this.logger.warn("WebSocket not connected");
255
- this.#warnNotConnected("emit", key);
263
+ this.#outbox.push(frame);
264
+ this.#warnUnconnected("emit", key);
256
265
  return this;
257
266
  }
258
- const payload: WebsocketReqData = { key, data: Array.isArray(data) ? data : [data] };
259
- this.#ws.send(JSON.stringify(payload));
267
+ this.#ws.send(frame);
260
268
  return this;
261
269
  }
262
270
  subscribe(option: { key: string; data: unknown[]; handleEvent: (data: unknown) => void }) {
263
271
  const roomId = WsClient.makeRoomId(option.key, option.data);
264
- if (!this.#ws) this.#warnUnconnectedSubscribe(option.key);
272
+ if (!this.#ws) this.#warnUnconnected("subscribe", option.key);
265
273
  if (!this.#roomSubscribeMap.has(roomId)) {
266
274
  this.#roomSubscribeMap.set(roomId, { key: option.key, data: option.data, listener: new Set() });
267
275
  if (this.#ws?.readyState === WebSocket.OPEN) {
@@ -44,7 +44,7 @@ type QueryOrMutationFetchFn<E, SlceCls extends SliceCls | never> = (
44
44
  /** Typed off `PromptResult` rather than the endpoint's return ref, which is the `Any` carrier a prompt rides on. */
45
45
  type PromptFetchFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<PromptMessage[]>;
46
46
 
47
- type MessageEmitFn<E> = (...args: EndpInfoArgs<E>) => void;
47
+ type MessageEmitFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => void;
48
48
 
49
49
  type MessageListenFn<E, SlceCls extends SliceCls | never> = (
50
50
  handleEvent: (data: EndpInfoReturns<E, SlceCls>) => PromiseOrObject<void>,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.44",
3
+ "version": "3.0.0-alpha.45",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -18,6 +18,7 @@ import {
18
18
  type DocumentUpdateOptions,
19
19
  documentQueryHelper,
20
20
  encodeDocumentValue,
21
+ isDocumentId,
21
22
  isDocumentUpdateNode,
22
23
  NoDocumentError,
23
24
  resolveDocumentUpdate,
@@ -1033,6 +1034,7 @@ export class SqlDocumentStore {
1033
1034
  async create(data: DocumentRecord, { runSaveHooks = true }: WriteHookOptions = {}) {
1034
1035
  const now = Date.now();
1035
1036
  const id = data.id ?? createDocumentId(now);
1037
+ if (!isDocumentId(id)) throw new Error(`Invalid ID value: ${id}`);
1036
1038
  const doc = this.hydrate(
1037
1039
  this.prepareDocument({
1038
1040
  ...data,
@@ -7,7 +7,7 @@ type EndpInfoReturns<E, SlceCls extends SliceCls | never> = EndpointClientReturn
7
7
  type QueryOrMutationFetchFn<E, SlceCls extends SliceCls | never> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<EndpInfoReturns<E, SlceCls>>;
8
8
  /** Typed off `PromptResult` rather than the endpoint's return ref, which is the `Any` carrier a prompt rides on. */
9
9
  type PromptFetchFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<PromptMessage[]>;
10
- type MessageEmitFn<E> = (...args: EndpInfoArgs<E>) => void;
10
+ type MessageEmitFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => void;
11
11
  type MessageListenFn<E, SlceCls extends SliceCls | never> = (handleEvent: (data: EndpInfoReturns<E, SlceCls>) => PromiseOrObject<void>, options?: FetchPolicy) => () => void;
12
12
  type PubsubSubscribeFn<E, SlceCls extends SliceCls | never> = (...args: [
13
13
  ...EndpInfoArgs<E>,