statewire 0.15.0 → 0.17.0

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.
Files changed (90) hide show
  1. package/README.md +14 -6
  2. package/dist/client/connection.d.ts +5 -1
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +19 -4
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts.map +1 -1
  7. package/dist/client/core.js +2 -0
  8. package/dist/client/core.js.map +1 -1
  9. package/dist/client/session.d.ts +15 -4
  10. package/dist/client/session.d.ts.map +1 -1
  11. package/dist/client/session.js +233 -52
  12. package/dist/client/session.js.map +1 -1
  13. package/dist/client/sessionStore.d.ts +5 -1
  14. package/dist/client/sessionStore.d.ts.map +1 -1
  15. package/dist/client/sessionStore.js +3 -1
  16. package/dist/client/sessionStore.js.map +1 -1
  17. package/dist/host/host.d.ts +1 -0
  18. package/dist/host/host.d.ts.map +1 -1
  19. package/dist/host/host.js.map +1 -1
  20. package/dist/host/http.d.ts.map +1 -1
  21. package/dist/host/http.js +27 -9
  22. package/dist/host/http.js.map +1 -1
  23. package/dist/host/socket.d.ts +10 -3
  24. package/dist/host/socket.d.ts.map +1 -1
  25. package/dist/host/socket.js +103 -39
  26. package/dist/host/socket.js.map +1 -1
  27. package/dist/host-internal.d.ts +9 -1
  28. package/dist/host-internal.d.ts.map +1 -1
  29. package/dist/host-internal.js +1 -1
  30. package/dist/index.d.ts +5 -3
  31. package/dist/index.js +3 -3
  32. package/dist/statewire.d.ts +33 -2
  33. package/dist/statewire.d.ts.map +1 -1
  34. package/dist/statewire.js +34 -3
  35. package/dist/statewire.js.map +1 -1
  36. package/dist/transports/fixtures.js +1 -0
  37. package/dist/transports/fixtures.js.map +1 -1
  38. package/dist/transports/remote.d.ts.map +1 -1
  39. package/dist/transports/remote.js +15 -8
  40. package/dist/transports/remote.js.map +1 -1
  41. package/dist/transports/sse.d.ts +2 -2
  42. package/dist/transports/sse.d.ts.map +1 -1
  43. package/dist/transports/sse.js +11 -11
  44. package/dist/transports/sse.js.map +1 -1
  45. package/dist/transports/testing.d.ts +1 -1
  46. package/dist/transports/testing.d.ts.map +1 -1
  47. package/dist/transports/testing.js +14 -6
  48. package/dist/transports/testing.js.map +1 -1
  49. package/dist/transports/ws.d.ts +2 -2
  50. package/dist/transports/ws.d.ts.map +1 -1
  51. package/dist/transports/ws.js +5 -5
  52. package/dist/transports/ws.js.map +1 -1
  53. package/dist/types.d.ts +7 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js.map +1 -1
  56. package/dist/util/randomId.js +1 -1
  57. package/dist/util/randomId.js.map +1 -1
  58. package/dist/wire/documents.d.ts +25 -2
  59. package/dist/wire/documents.d.ts.map +1 -1
  60. package/dist/wire/documents.js +27 -7
  61. package/dist/wire/documents.js.map +1 -1
  62. package/dist/wire/protocol.d.ts +55 -0
  63. package/dist/wire/protocol.d.ts.map +1 -0
  64. package/dist/wire/protocol.js +221 -0
  65. package/dist/wire/protocol.js.map +1 -0
  66. package/dist/wire/version.d.ts +1 -3
  67. package/dist/wire/version.d.ts.map +1 -1
  68. package/dist/wire/version.js +1 -3
  69. package/dist/wire/version.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/client/connection.ts +43 -13
  72. package/src/client/core.ts +6 -0
  73. package/src/client/session.ts +339 -42
  74. package/src/client/sessionStore.ts +8 -0
  75. package/src/host/host.ts +1 -0
  76. package/src/host/http.ts +36 -7
  77. package/src/host/socket.ts +141 -50
  78. package/src/host-internal.ts +12 -1
  79. package/src/index.ts +5 -2
  80. package/src/statewire.ts +109 -6
  81. package/src/transports/fixtures.ts +1 -1
  82. package/src/transports/remote.ts +16 -7
  83. package/src/transports/sse.ts +28 -18
  84. package/src/transports/testing.ts +12 -4
  85. package/src/transports/ws.ts +8 -8
  86. package/src/types.ts +11 -0
  87. package/src/util/randomId.ts +3 -3
  88. package/src/wire/documents.ts +126 -8
  89. package/src/wire/protocol.ts +327 -0
  90. package/src/wire/version.ts +0 -2
package/src/statewire.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { StatewireDocuments } from "./wire/documents";
2
+ import type { StatewireProtocol } from "./wire/protocol";
1
3
  import type { StatewireHost } from "./host";
2
4
  import { useMemo, useRef, useState, useSyncExternalStore } from "react";
3
5
  import {
@@ -13,18 +15,32 @@ import type { ResourceElement } from "@assistant-ui/tap";
13
15
  import { CONNECTED_CONNECTION, useStatewireCore } from "./client/core";
14
16
  import type { Statewire } from "./types";
15
17
  import type { TERMINAL } from "./client/connection";
16
- import { SessionStorageContext } from "./client/sessionStore";
18
+ import {
19
+ DocumentStorageContext,
20
+ ProtocolContext,
21
+ SessionStorageContext,
22
+ } from "./client/sessionStore";
17
23
 
18
24
  const useSessionTransport = ({
19
25
  transport,
20
26
  session,
27
+ documents,
28
+ protocol,
21
29
  }: {
22
30
  transport: ResourceElement<Statewire<any, any>>;
23
31
  session: StatewireClient.SessionStorage | undefined;
32
+ documents: StatewireDocuments.Storage | undefined;
33
+ protocol: StatewireProtocol.Options;
24
34
  }): Statewire<unknown> =>
25
35
  useContextProvider(SessionStorageContext, session, () =>
26
36
  // oxlint-disable-next-line react-hooks/rules-of-hooks
27
- useResource(transport),
37
+ useContextProvider(DocumentStorageContext, documents, () =>
38
+ // oxlint-disable-next-line react-hooks/rules-of-hooks
39
+ useContextProvider(ProtocolContext, protocol, () =>
40
+ // oxlint-disable-next-line react-hooks/rules-of-hooks
41
+ useResource(transport),
42
+ ),
43
+ ),
28
44
  );
29
45
 
30
46
  // The transport owns the session; it reads the storage off the context this
@@ -45,8 +61,30 @@ export const useStatewire = <
45
61
  ? [withKey("session", options.storage.session)]
46
62
  : [],
47
63
  )[0];
64
+ const documents = useResources(
65
+ options.storage?.documents === undefined
66
+ ? []
67
+ : [withKey("documents", options.storage.documents)],
68
+ )[0];
69
+ const protocol: StatewireProtocol.Options = {
70
+ ...(options.protocol !== undefined && { protocol: options.protocol }),
71
+ ...(options.wireVersion !== undefined && {
72
+ wireVersion: options.wireVersion,
73
+ }),
74
+ ...(options.applicationProtocols !== undefined && {
75
+ applicationProtocols: options.applicationProtocols,
76
+ }),
77
+ ...(options.keyPrefixes !== undefined && {
78
+ keyPrefixes: options.keyPrefixes,
79
+ }),
80
+ };
48
81
  const value = useResource(
49
- SessionTransport({ transport: options.transport, session }),
82
+ SessionTransport({
83
+ transport: options.transport,
84
+ session,
85
+ documents,
86
+ protocol,
87
+ }),
50
88
  );
51
89
  const core = useStatewireCore(() => latest.current, value);
52
90
  const [subscribe] = useState(
@@ -76,6 +114,26 @@ export const useStatewire = <
76
114
  };
77
115
  return {
78
116
  state,
117
+ documents: value.documents,
118
+ selection: value.selection,
119
+ invoke: (
120
+ protocol: string,
121
+ method: string,
122
+ params: readonly unknown[],
123
+ ) => {
124
+ if (!value.invoke)
125
+ throw new Error(
126
+ "statewire: transport does not support protocol commands",
127
+ );
128
+ return value.invoke(protocol, method, params);
129
+ },
130
+ updateContext: (ops: readonly unknown[]) => {
131
+ if (!value.updateContext)
132
+ throw new Error(
133
+ "statewire: transport does not support context documents",
134
+ );
135
+ value.updateContext(ops);
136
+ },
79
137
  getState: core.getState as () => TState,
80
138
  connection,
81
139
  commands: core.commands as Statewire.CommandsProxy<TCommands>,
@@ -129,6 +187,23 @@ export class StatewireClient<
129
187
  return this.root.getValue().commands;
130
188
  }
131
189
 
190
+ get documents(): StatewireDocuments.Source | undefined {
191
+ return this.root.getValue().documents;
192
+ }
193
+ get selection(): StatewireProtocol.Selection | undefined {
194
+ return this.root.getValue().selection;
195
+ }
196
+ invoke(
197
+ protocol: string,
198
+ method: string,
199
+ params: readonly unknown[],
200
+ ): Promise<unknown> {
201
+ return this.root.getValue().invoke(protocol, method, params);
202
+ }
203
+ updateContext(ops: readonly unknown[]): void {
204
+ this.root.getValue().updateContext(ops);
205
+ }
206
+
132
207
  get isBusy(): boolean {
133
208
  return this.root.getValue().isBusy;
134
209
  }
@@ -165,10 +240,17 @@ export namespace StatewireClient {
165
240
  TState,
166
241
  TCommands extends Statewire.Commands = Statewire.Commands,
167
242
  > = {
243
+ protocol?: string;
244
+ keyPrefixes?: Readonly<Record<string, readonly string[]>>;
245
+ wireVersion?: StatewireProtocol.Range;
246
+ applicationProtocols?: readonly StatewireProtocol.Offer[] | "*";
168
247
  transport: ResourceElement<Statewire<TState, TCommands>>;
169
248
  /** Durable session: the mounted storage holds this client's lane identity and its un-durable commands, which resend on the next attach. */
170
249
  storage?:
171
- | { session?: ResourceElement<SessionStorage> | undefined }
250
+ | {
251
+ session?: ResourceElement<SessionStorage> | undefined;
252
+ documents?: ResourceElement<StatewireDocuments.Storage> | undefined;
253
+ }
172
254
  | undefined;
173
255
  /** Served as the canonical state until the attach snapshot; own commands fold over it. */
174
256
  initialState?: TState;
@@ -197,6 +279,14 @@ export namespace StatewireClient {
197
279
  > = {
198
280
  /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */
199
281
  state: TState;
282
+ documents: StatewireDocuments.Source | undefined;
283
+ selection: StatewireProtocol.Selection | undefined;
284
+ invoke: (
285
+ protocol: string,
286
+ method: string,
287
+ params: readonly unknown[],
288
+ ) => Promise<unknown>;
289
+ updateContext: (ops: readonly unknown[]) => void;
200
290
  /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */
201
291
  getState: () => TState;
202
292
  connection: Connection;
@@ -318,6 +408,8 @@ export namespace StatewireClient {
318
408
 
319
409
  /** A non-accepted send retained for the user: the wire statement, its failure, and its original clocks. */
320
410
  export type FailedCommand = {
411
+ readonly protocol: string;
412
+ readonly produced?: SessionCommand["produced"];
321
413
  readonly command: string;
322
414
  readonly params: readonly unknown[];
323
415
  readonly meta?: unknown;
@@ -329,6 +421,11 @@ export namespace StatewireClient {
329
421
  /** A command as persisted while un-durable: its lane seq plus the wire statement. */
330
422
  export type SessionCommand = {
331
423
  seq: number;
424
+ protocol: string;
425
+ produced?: {
426
+ wire: StatewireProtocol.Range;
427
+ protocols: readonly StatewireProtocol.Offer[] | "*";
428
+ };
332
429
  method: string;
333
430
  params: unknown[];
334
431
  meta?: unknown;
@@ -339,6 +436,7 @@ export namespace StatewireClient {
339
436
  clientId: string;
340
437
  nextSeq: number;
341
438
  ctxNextSeq: number;
439
+ context?: { ops: unknown[]; pending: { seq: number; ops: unknown[] }[] };
342
440
  commands: readonly SessionCommand[];
343
441
  /** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */
344
442
  verdicts?: readonly SessionVerdict[];
@@ -360,6 +458,11 @@ export namespace StatewireClient {
360
458
  export type SessionStorage = {
361
459
  load: () => SessionRecord | null;
362
460
  save: (record: SessionRecord) => void;
461
+ /** Upgrade incompatible stored commands; null discards the old record and starts a new client identity. */
462
+ migrate?: (
463
+ record: SessionRecord,
464
+ current: NonNullable<SessionCommand["produced"]>,
465
+ ) => SessionRecord | null;
363
466
  /** Serializes record mutations across sharers; presence marks the storage shared. */
364
467
  lock?: <T>(fn: () => T | Promise<T>) => Promise<T>;
365
468
  /** Notifies of record saves by other sharers; returns unsubscribe. Required alongside `lock`. */
@@ -373,7 +476,7 @@ export namespace StatewireClient {
373
476
  | HeadersInit
374
477
  | ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);
375
478
 
376
- export type SSEOptions = {
479
+ export type HttpOptions = StatewireProtocol.Options & {
377
480
  sessionStore?: SessionStorage | undefined;
378
481
  url: string;
379
482
  /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */
@@ -387,7 +490,7 @@ export namespace StatewireClient {
387
490
  onDone?: () => void;
388
491
  };
389
492
 
390
- export type WSOptions = {
493
+ export type WebsocketOptions = StatewireProtocol.Options & {
391
494
  sessionStore?: SessionStorage | undefined;
392
495
  url: string;
393
496
  /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */
@@ -24,7 +24,7 @@ export const commandFrame = (
24
24
  method: string,
25
25
  seq: number,
26
26
  params: unknown[] = [],
27
- ) => JSON.stringify({ cmd: [{ method, params, seq }] });
27
+ ) => JSON.stringify({ cmd: [{ protocol: "default", method, params, seq }] });
28
28
 
29
29
  export type SocketRecorder = {
30
30
  sent: string[];
@@ -1,6 +1,10 @@
1
- import { useEffect } from "react";
1
+ import { useContext, useEffect } from "react";
2
+ import { ProtocolContext } from "../client/sessionStore";
2
3
  import type { Statewire } from "../types";
3
4
  import {
5
+ documentSource,
6
+ invokeProtocol,
7
+ updateContext,
4
8
  discardFailed,
5
9
  failedCommands,
6
10
  retryFailed,
@@ -22,16 +26,21 @@ export const useStatewireRemote = <
22
26
  options: TOptions,
23
27
  useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,
24
28
  ): Statewire<any, any> => {
25
- const s = useStatewireSession({
26
- sessionStore: options.sessionStore,
27
- onSessionLost: options.onSessionLost,
28
- onDone: options.onDone,
29
- });
30
- const conn = useLink(options, s.ctx);
29
+ const inherited = useContext(ProtocolContext);
30
+ const configured = { ...inherited, ...options };
31
+ const s = useStatewireSession(configured);
32
+ const conn = useLink(configured, s.ctx);
31
33
  useEffect(() => setTransport(s, conn), [s, conn]);
32
34
 
33
35
  return {
34
36
  state: s.canonical,
37
+ documents: documentSource(s),
38
+ get selection() {
39
+ return s.selection;
40
+ },
41
+ invoke: (protocol, method, params) =>
42
+ invokeProtocol(s, protocol, method, params),
43
+ updateContext: (ops) => updateContext(s, ops),
35
44
  commands: s.commands,
36
45
  connection: conn.connection,
37
46
  source: s.source,
@@ -7,7 +7,6 @@ import {
7
7
  classify,
8
8
  handleEnvelope,
9
9
  resolveHeaders,
10
- unsupportedVersion,
11
10
  useLink,
12
11
  validateRefusalSyn,
13
12
  } from "../client/connection";
@@ -22,7 +21,7 @@ import type { Statewire } from "../types";
22
21
  import type { StatewireClient } from "../statewire";
23
22
  import type { StatewireHost } from "../host";
24
23
  import { useStatewireRemote } from "./remote";
25
- import { PROTOCOL_VERSION, VERSION_HEADER } from "../wire/version";
24
+ import { negotiationHeaders, parseNegotiation } from "../wire/protocol";
26
25
 
27
26
  /** A client frame's lanes: stamped statements, strictly consecutive per lane. */
28
27
  export type StatewireFrame = {
@@ -61,7 +60,7 @@ export type RemoteLink = {
61
60
 
62
61
  const LEASE_HEADER = "Statewire-Lease";
63
62
 
64
- const baseUrl = (options: StatewireClient.SSEOptions) =>
63
+ const baseUrl = (options: StatewireClient.HttpOptions) =>
65
64
  options.url.replace(/\/$/, "");
66
65
 
67
66
  const withExtra = (headers: Headers, extra: Record<string, string>) => {
@@ -71,7 +70,7 @@ const withExtra = (headers: Headers, extra: Record<string, string>) => {
71
70
  };
72
71
 
73
72
  const timedFetch = async (
74
- options: StatewireClient.SSEOptions,
73
+ options: StatewireClient.HttpOptions,
75
74
  input: string,
76
75
  init: RequestInit & { signal: AbortSignal },
77
76
  timeout: AbortSignal,
@@ -116,11 +115,15 @@ const consume = async (
116
115
 
117
116
  const attemptStream = async (
118
117
  c: Conn,
119
- options: StatewireClient.SSEOptions,
118
+ options: StatewireClient.HttpOptions,
120
119
  abort: AbortController,
121
120
  ): Promise<AttemptOutcome> => {
122
121
  c.lease = null;
123
122
  const headers = await resolveHeaders(options.headers, c.authStale);
123
+ for (const [name, value] of Object.entries(negotiationHeaders(options))) {
124
+ if (!headers.has(name)) headers.set(name, value);
125
+ }
126
+ c.negotiation = parseNegotiation(headers);
124
127
  c.authStale = false;
125
128
  const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
126
129
  const body = lanes === null ? undefined : JSON.stringify(lanes);
@@ -150,14 +153,21 @@ const attemptStream = async (
150
153
  } finally {
151
154
  clearTimeout(headerTimer);
152
155
  }
153
- if (!res.ok) return flushTapSync(() => settleFin(c, httpFin(res)));
156
+ if (!res.ok) {
157
+ const body = await res
158
+ .clone()
159
+ .json()
160
+ .catch(() => null);
161
+ return (
162
+ flushTapSync(() =>
163
+ body?.fin === undefined
164
+ ? settleFin(c, httpFin(res))
165
+ : handleEnvelope(c, body),
166
+ ) ?? "stop"
167
+ );
168
+ }
154
169
  if (!res.body)
155
170
  throw new Error("statewire: protocol error: stream response has no body");
156
- const version = res.headers.get(VERSION_HEADER)?.trim();
157
- if (version !== undefined && version !== PROTOCOL_VERSION) {
158
- res.body.cancel().catch(() => {});
159
- return unsupportedVersion(c, version);
160
- }
161
171
  c.lease = res.headers.get(LEASE_HEADER);
162
172
  const reader = res.body.getReader();
163
173
  try {
@@ -169,7 +179,7 @@ const attemptStream = async (
169
179
 
170
180
  const postFrame = async (
171
181
  c: Conn,
172
- options: StatewireClient.SSEOptions,
182
+ options: StatewireClient.HttpOptions,
173
183
  lease: string,
174
184
  frame: StatewireFrame,
175
185
  timeout: AbortSignal,
@@ -230,7 +240,7 @@ const refusalSyn = async (res: Response): Promise<StatewireSyn> => {
230
240
 
231
241
  const sendFrame = async (
232
242
  c: Conn,
233
- options: StatewireClient.SSEOptions,
243
+ options: StatewireClient.HttpOptions,
234
244
  frame: StatewireFrame,
235
245
  ): Promise<StatewireSubmitResult> => {
236
246
  const lease = c.lease;
@@ -256,7 +266,7 @@ const sendFrame = async (
256
266
  };
257
267
 
258
268
  const useSseLink = (
259
- options: StatewireClient.SSEOptions,
269
+ options: StatewireClient.HttpOptions,
260
270
  ctx: StatewireRemoteProps,
261
271
  ): RemoteLink =>
262
272
  useLink({
@@ -282,13 +292,13 @@ const useSseLink = (
282
292
  },
283
293
  });
284
294
 
285
- const useStatewireSSE = (
286
- options: StatewireClient.SSEOptions,
295
+ const useStatewireHttp = (
296
+ options: StatewireClient.HttpOptions,
287
297
  ): Statewire<any, any> => useStatewireRemote(options, useSseLink);
288
298
 
289
- export const StatewireSSE = resource(useStatewireSSE) as <
299
+ export const StatewireHttp = resource(useStatewireHttp) as <
290
300
  TState = any,
291
301
  TCommands extends Statewire.Commands = Statewire.Commands,
292
302
  >(
293
- options: StatewireClient.SSEOptions,
303
+ options: StatewireClient.HttpOptions,
294
304
  ) => ResourceElement<Statewire<TState | undefined, TCommands>>;
@@ -1,6 +1,6 @@
1
1
  import type { ResourceElement } from "@assistant-ui/tap";
2
2
  import type { StatewireEnvelope } from "../client/connection";
3
- import { StatewireSSE } from "./sse";
3
+ import { StatewireHttp } from "./sse";
4
4
  import type { StatewireFrame } from "./sse";
5
5
  import type { Statewire } from "../types";
6
6
  import { openSSEStream, sseFrame } from "./fake-sse";
@@ -41,6 +41,7 @@ let mockCount = 0;
41
41
 
42
42
  export const createMockTransport = <TState>(
43
43
  initialState: TState,
44
+ protocol = "default",
44
45
  ): MockTransport<TState> => {
45
46
  let state: unknown = initialState;
46
47
  let clientId = "";
@@ -57,11 +58,13 @@ export const createMockTransport = <TState>(
57
58
  const admit = (id: string, frame: StatewireFrame) => {
58
59
  const rec = clients.get(id)!;
59
60
  for (const statement of frame.ctx ?? []) {
61
+ if (statement.seq <= rec.ctxSeq) continue;
60
62
  rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);
61
63
  const root = statement.ops[0] as { value?: unknown } | undefined;
62
64
  commands.push({ context: root?.value });
63
65
  }
64
66
  for (const statement of frame.cmd ?? []) {
67
+ if (statement.seq <= rec.seq) continue;
65
68
  rec.seq = Math.max(rec.seq, statement.seq);
66
69
  commands.push({ method: statement.method, params: statement.params });
67
70
  }
@@ -87,8 +90,13 @@ export const createMockTransport = <TState>(
87
90
  (handle) => {
88
91
  handle.send(
89
92
  sseFrame({
90
- ops: [{ op: "replace", path: [], value: state }],
91
- syn: { seq: rec.seq, ctxSeq: rec.ctxSeq },
93
+ ops: [{ op: "mount", protocol, main: true, value: state }],
94
+ syn: {
95
+ seq: rec.seq,
96
+ ctxSeq: rec.ctxSeq,
97
+ version: "2026-09-07",
98
+ protocols: [{ name: protocol, version: "2026-09-07" }],
99
+ },
92
100
  }),
93
101
  );
94
102
  },
@@ -104,7 +112,7 @@ export const createMockTransport = <TState>(
104
112
  }) as typeof fetch;
105
113
 
106
114
  return {
107
- transport: StatewireSSE({
115
+ transport: StatewireHttp({
108
116
  url: `http://mock-${++mockCount}/api`,
109
117
  fetch: fakeFetch,
110
118
  }),
@@ -34,7 +34,7 @@ const OPEN = 1;
34
34
  export { WS_SUBPROTOCOL };
35
35
 
36
36
  const socketUrl = async (
37
- options: StatewireClient.WSOptions,
37
+ options: StatewireClient.WebsocketOptions,
38
38
  clientId: string,
39
39
  ) => {
40
40
  const { url, query } = options;
@@ -72,7 +72,7 @@ const runSocket = (
72
72
  ws.onerror = () => {};
73
73
  ws.onopen = () => {
74
74
  const negotiated = ws.protocol.trim();
75
- if (negotiated !== "" && negotiated !== WS_SUBPROTOCOL) {
75
+ if (negotiated !== WS_SUBPROTOCOL) {
76
76
  outcome = unsupportedVersion(c, negotiated);
77
77
  ws.close();
78
78
  return;
@@ -124,7 +124,7 @@ const runSocket = (
124
124
  const attemptSocket = async (
125
125
  c: Conn,
126
126
  w: WsChannel,
127
- options: StatewireClient.WSOptions,
127
+ options: StatewireClient.WebsocketOptions,
128
128
  abort: AbortController,
129
129
  ): Promise<AttemptOutcome> => {
130
130
  let ws: WebSocket | null = null;
@@ -166,7 +166,7 @@ const sendFrame = (
166
166
  };
167
167
 
168
168
  const useWsLink = (
169
- options: StatewireClient.WSOptions,
169
+ options: StatewireClient.WebsocketOptions,
170
170
  ctx: StatewireRemoteProps,
171
171
  ): RemoteLink => {
172
172
  const [w] = useState((): WsChannel => ({ socket: null }));
@@ -178,13 +178,13 @@ const useWsLink = (
178
178
  });
179
179
  };
180
180
 
181
- const useStatewireWS = (
182
- options: StatewireClient.WSOptions,
181
+ const useStatewireWebsocket = (
182
+ options: StatewireClient.WebsocketOptions,
183
183
  ): Statewire<any, any> => useStatewireRemote(options, useWsLink);
184
184
 
185
- export const StatewireWS = resource(useStatewireWS) as <
185
+ export const StatewireWebsocket = resource(useStatewireWebsocket) as <
186
186
  TState = any,
187
187
  TCommands extends Statewire.Commands = Statewire.Commands,
188
188
  >(
189
- options: StatewireClient.WSOptions,
189
+ options: StatewireClient.WebsocketOptions,
190
190
  ) => ResourceElement<Statewire<TState | undefined, TCommands>>;
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { StatewireDocuments } from "./wire/documents";
2
+ import type { StatewireProtocol } from "./wire/protocol";
1
3
  import type { StatewireClient } from "./statewire";
2
4
  import type { StatewirePath } from "./wire/ops";
3
5
 
@@ -16,6 +18,7 @@ export namespace Statewire {
16
18
 
17
19
  export type CommandUpdate<TCommands extends Commands = Commands> = {
18
20
  [K in keyof TCommands]: {
21
+ readonly protocol?: string;
19
22
  method: K;
20
23
  params: Parameters<TCommands[K]>;
21
24
  seq: number;
@@ -50,6 +53,14 @@ export interface Statewire<
50
53
  TCommands extends Statewire.Commands = Statewire.Commands,
51
54
  > {
52
55
  state: TState;
56
+ documents?: StatewireDocuments.Source;
57
+ selection?: StatewireProtocol.Selection | undefined;
58
+ invoke?: (
59
+ protocol: string,
60
+ method: string,
61
+ params: readonly unknown[],
62
+ ) => Promise<unknown>;
63
+ updateContext?: (ops: readonly unknown[]) => void;
53
64
  commands: Statewire.CommandsProxy<TCommands>;
54
65
  connection?: StatewireClient.Connection;
55
66
  source?: Statewire.StateSource;
@@ -1,4 +1,4 @@
1
1
  export const randomId = (): string =>
2
- typeof crypto !== "undefined" && "randomUUID" in crypto
3
- ? crypto.randomUUID()
4
- : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
2
+ Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) =>
3
+ byte.toString(16).padStart(2, "0"),
4
+ ).join("");