experimental-a2 0.9.0 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ed921d5: Make WebSocket multiplexing opt-in. A `ws` client now opens one socket per
8
+ session by default, and `server.fetch` binds that socket to the session in its
9
+ upgrade URL. Set `multiplex: true` on the client API and
10
+ `multiplexWebSocket: true` on the server fetch options to share one socket
11
+ across sessions.
12
+
13
+ Client route URLs may also be functions of `sessionId`. A2 resolves them for
14
+ each HTTP request or WebSocket reconnect. Multiplexed WebSockets remain
15
+ string-only because their connection does not belong to one session.
16
+
3
17
  ## 0.9.0
4
18
 
5
19
  ### Minor Changes
@@ -1,4 +1,4 @@
1
- import { c as ActorRefusedError } from "./actor-shared-BACubf4x.js";
1
+ import { c as ActorRefusedError } from "./actor-shared-USo5MyuF.js";
2
2
  //#region src/actor-client.d.ts
3
3
  /** The loose shape of an `actor()` definition — carried type-only. */
4
4
  type AnyActorDefinition = {
@@ -1,5 +1,5 @@
1
1
  import { a as ContractEvent, s as EventDefs } from "./reducer-DJKWm3cp.js";
2
- import { i as ActorPresenceMap, o as ActorPresenceValues } from "./actor-shared-BACubf4x.js";
2
+ import { i as ActorPresenceMap, o as ActorPresenceValues } from "./actor-shared-USo5MyuF.js";
3
3
  import { ActorClientCall, ActorStateOf, AnyActorDefinition } from "./actor-client.js";
4
4
  import { Connection } from "./client.js";
5
5
  //#region src/actor-react.d.ts
@@ -1,5 +1,5 @@
1
1
  import { h as StandardSchemaV1 } from "./reducer-DJKWm3cp.js";
2
- import { f as ScheduleTiming } from "./server-CKY3_lbw.js";
2
+ import { f as ScheduleTiming } from "./server-DgCrSuhB.js";
3
3
  //#region src/actor-shared.d.ts
4
4
  /**
5
5
  * A handler answered by throwing `NonRetriableError`: the event
@@ -133,4 +133,4 @@ type ActorClientEventDefs<S> = {
133
133
  };
134
134
  //#endregion
135
135
  export { ActorPresenceOf as a, ActorRefusedError as c, ActorSend as d, ActorPresenceMap as i, ActorScheduleOptions as l, ActorConcurrentContext as n, ActorPresenceValues as o, ActorContext as r, ActorProtocol as s, ActorClientEventDefs as t, ActorScheduleSend as u };
136
- //# sourceMappingURL=actor-shared-BACubf4x.d.ts.map
136
+ //# sourceMappingURL=actor-shared-USo5MyuF.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"actor-shared-BACubf4x.d.ts","names":[],"sources":["../src/actor-shared.ts"],"mappings":";;;;;;;;;cAgCa,0BAA0B;;WAE5B;;WAEA;EACT,YAAY,eAAe,mBAAmB;;;;;;;;;KAepC;EACV;EACA;;;;;;;EAOA;;;KAIU,gBAAgB,UAAU,iBAAiB;EACrD,gBAAgB;IAEd;;KAIQ,oBAAoB,QAC7B,WAAW,cAAc,EAAE;;;;;;KAQlB,iBAAiB;GAExB,yBAAA,WAAW;IAAgB,OAAO,EAAE;IAAI;IAAc,IAAI;;;;;;;;;;;;KAanD,UAAU,iBACV,WAAW,eAAe,EAAE,MACjC,QAAQ,EAAE,eACV,OAAO,EAAE;;;;;;;;;KAWJ,uBAAuB;EAAmB;;;;;;;;;;;;KAY1C,kBAAkB,iBAClB,WAAW,eAAe,EAAE,MACjC,OAAO,EAAE,gBAAgB,SAAS,yBAAyB,iBAC3D,OAAO,EAAE,IAAI,SAAS,yBAAyB;;KAI1C,aAAa,UAAU;;EAEjC,OAAO;;EAEP;;EAEA;;EAEA,QAAQ;;EAER,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;;;;;;;KASlB,uBAAuB,UAAU;;EAE3C;;EAEA;;EAEA,QAAQ;;EAER,SAAS;IAAU,OAAO;IAAY;;;EAEtC,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;KAGlB,kBAAkB;EAC5B,OAAO;;EAEP;;EAEA;;;KAWU,qBAAqB;EAC/B,kBAAkB,iBAAiB,kBAAkB"}
1
+ {"version":3,"file":"actor-shared-USo5MyuF.d.ts","names":[],"sources":["../src/actor-shared.ts"],"mappings":";;;;;;;;;cAgCa,0BAA0B;;WAE5B;;WAEA;EACT,YAAY,eAAe,mBAAmB;;;;;;;;;KAepC;EACV;EACA;;;;;;;EAOA;;;KAIU,gBAAgB,UAAU,iBAAiB;EACrD,gBAAgB;IAEd;;KAIQ,oBAAoB,QAC7B,WAAW,cAAc,EAAE;;;;;;KAQlB,iBAAiB;GAExB,yBAAA,WAAW;IAAgB,OAAO,EAAE;IAAI;IAAc,IAAI;;;;;;;;;;;;KAanD,UAAU,iBACV,WAAW,eAAe,EAAE,MACjC,QAAQ,EAAE,eACV,OAAO,EAAE;;;;;;;;;KAWJ,uBAAuB;EAAmB;;;;;;;;;;;;KAY1C,kBAAkB,iBAClB,WAAW,eAAe,EAAE,MACjC,OAAO,EAAE,gBAAgB,SAAS,yBAAyB,iBAC3D,OAAO,EAAE,IAAI,SAAS,yBAAyB;;KAI1C,aAAa,UAAU;;EAEjC,OAAO;;EAEP;;EAEA;;EAEA,QAAQ;;EAER,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;;;;;;;KASlB,uBAAuB,UAAU;;EAE3C;;EAEA;;EAEA,QAAQ;;EAER,SAAS;IAAU,OAAO;IAAY;;;EAEtC,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;KAGlB,kBAAkB;EAC5B,OAAO;;EAEP;;EAEA;;;KAWU,qBAAqB;EAC/B,kBAAkB,iBAAiB,kBAAkB"}
package/dist/actor.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { i as Contract, s as EventDefs, t as Reducer } from "./reducer-DJKWm3cp.js";
2
2
  import { t as A2Store } from "./store-DGHeBtIQ.js";
3
- import { m as Session, n as A2Server, t as A2Scheduler } from "./server-CKY3_lbw.js";
3
+ import { m as Session, n as A2Server, t as A2Scheduler } from "./server-DgCrSuhB.js";
4
4
  import { i as A2Telemetry } from "./telemetry-CpeclqB2.js";
5
- import { a as ActorPresenceOf, c as ActorRefusedError, d as ActorSend, i as ActorPresenceMap, l as ActorScheduleOptions, n as ActorConcurrentContext, o as ActorPresenceValues, r as ActorContext, s as ActorProtocol, t as ActorClientEventDefs, u as ActorScheduleSend } from "./actor-shared-BACubf4x.js";
5
+ import { a as ActorPresenceOf, c as ActorRefusedError, d as ActorSend, i as ActorPresenceMap, l as ActorScheduleOptions, n as ActorConcurrentContext, o as ActorPresenceValues, r as ActorContext, s as ActorProtocol, t as ActorClientEventDefs, u as ActorScheduleSend } from "./actor-shared-USo5MyuF.js";
6
6
  //#region src/actor.d.ts
7
7
  /**
8
8
  * One event's handler: ordinary server code. Serial by default — read
package/dist/actor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as contract } from "./contract-48bUMgcL.js";
2
2
  import { n as NonRetriableError, t as A2Error } from "./errors-DCk6ch5n.js";
3
- import { a as parsePresenceSibling, i as sseResponse, r as setServerFetchHooks, t as createServer } from "./server-CBET-jSz.js";
3
+ import { a as parsePresenceSibling, i as sseResponse, r as setServerFetchHooks, t as createServer } from "./server-DlLyvaSH.js";
4
4
  import { a as actorFailedSchema, c as isJsonTree, i as ActorRefusedError, l as openPresenceDefs, n as ACTOR_LANE, o as actorReducer, r as ACTOR_STATE_EVENT, s as actorStateSchema, t as ACTOR_FAILED_EVENT } from "./actor-shared-DI7J5upy.js";
5
5
  //#region src/actor.ts
6
6
  /**
@@ -1,5 +1,5 @@
1
1
  import { a as ContractEvent, s as EventDefs } from "./reducer-DJKWm3cp.js";
2
- import { n as A2Server, p as ServerOptions } from "./server-CKY3_lbw.js";
2
+ import { n as A2Server, p as ServerOptions } from "./server-DgCrSuhB.js";
3
3
  import { AIEventDefs, AIState, AgentDefinition, AgentToolContext, AgentToolEventDefs } from "./ai.js";
4
4
  import { Instructions, LanguageModel, TextStreamPart, ToolSet, ToolSet as ToolSet$1, UIMessage, UIMessageChunk, streamText } from "ai";
5
5
  //#region src/ai-sdk-step.d.ts
package/dist/ai-server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as A2Error } from "./errors-DCk6ch5n.js";
2
2
  import { f as consumeSchedulerSendFailure, p as installAmbientToolScopeStorage } from "./internal-DRXJ56EI.js";
3
- import { r as setServerFetchHooks, t as createServer } from "./server-CBET-jSz.js";
3
+ import { r as setServerFetchHooks, t as createServer } from "./server-DlLyvaSH.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { convertToModelMessages, stepCountIs, streamText, toUIMessageStream } from "ai";
6
6
  //#region src/ai-sdk-step.ts
package/dist/ai.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { h as StandardSchemaV1, i as Contract, r as AppendInput, s as EventDefs, t as Reducer } from "./reducer-DJKWm3cp.js";
2
- import { s as HandlerContext } from "./server-CKY3_lbw.js";
2
+ import { s as HandlerContext } from "./server-DgCrSuhB.js";
3
3
  import { FinishReason, JSONValue, LanguageModelUsage, UIMessage, UIMessageChunk } from "ai";
4
4
  //#region src/ai.d.ts
5
5
  type AgentStatus = "idle" | "generating" | "waiting" | "failed" | "closed";
package/dist/client.d.ts CHANGED
@@ -141,19 +141,25 @@ type A2Client<D extends EventDefs, S, P extends PresenceDefs = Record<never, nev
141
141
  session(sessionId: string, options?: SessionOptions<D, S>): SessionClient<D, S, P>;
142
142
  };
143
143
  /**
144
- * The wire the client rides. The string form is one route serving GET
144
+ * The wire the client rides. The first form is one route serving GET
145
145
  * (SSE stream) + POST (push). `http` splits the verbs across two
146
146
  * routes, for when platform duration limits differ per verb. `ws`
147
- * rides everything every session of this client over one
148
- * multiplexed WebSocket, served by `sessionsSocket`.
147
+ * rides everything over WebSocket, one socket per session by default.
148
+ * Multiplexing every session of the client is an explicit opt-in.
149
149
  */
150
- type ClientApi = string | {
150
+ type SessionUrl = string | ((sessionId: string) => string);
151
+ type ClientApi = SessionUrl | {
151
152
  type: "http";
152
- push: string;
153
- stream: string;
153
+ push: SessionUrl;
154
+ stream: SessionUrl;
155
+ } | {
156
+ type: "ws";
157
+ url: SessionUrl;
158
+ multiplex?: false;
154
159
  } | {
155
160
  type: "ws";
156
161
  url: string;
162
+ multiplex: true;
157
163
  };
158
164
  /**
159
165
  * The socket surface the `ws` transport drives — satisfied structurally
@@ -198,5 +204,5 @@ type CreateClientOptions<D extends EventDefs, S, P extends PresenceDefs = Record
198
204
  };
199
205
  declare function createClient<D extends EventDefs, S, P extends PresenceDefs = Record<never, never>>(options: CreateClientOptions<D, S, P>): A2Client<D, S, P>;
200
206
  //#endregion
201
- export { A2Client, ClientApi, ClientWebSocket, ClientWebSocketConstructor, Connection, ConnectionStatus, CreateClientOptions, HistoryState, LoadHistoryOptions, PushResult, SessionClient, SessionClientPresence, SessionOptions, SessionSnapshot, createClient };
207
+ export { A2Client, ClientApi, ClientWebSocket, ClientWebSocketConstructor, Connection, ConnectionStatus, CreateClientOptions, HistoryState, LoadHistoryOptions, PushResult, SessionClient, SessionClientPresence, SessionOptions, SessionSnapshot, SessionUrl, createClient };
202
208
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;KAqDY;;;;;;;KAQA;EACN;;EAEA;;EAEA;;EAEA,OAAO;;EAEP;EAAgB;;EAChB;;;;;;;;;;KAUM,WAAW,UAAU,aAAa,QAAQ,cAAc;WACzD,WAAW,QAAQ,cAAc;;KAGhC;;;;;;EAMV;;EAEA;;;KAIU;;EAEV;;EAEA;;EAEA;;;KAIU,gBACV,UAAU,WACV,GACA,UAAU,eAAe,wBACvB,aACF;;EAGE,UAAU,YAAY;;;EAIxB,OAAO;;;;;EAKP,QAAQ,cAAc;;;EAGtB;;EAEA,SAAS;EACT,YAAY;;;;;;;KAQF,sBAAsB,UAAU;;;;;;;;;;EAU1C,YAAY,QAAQ,cAAc;;KAGxB,cACV,UAAU,WACV,GACA,UAAU,eAAe,wBACvB,aAAa,GAAG,sBAAsB;WAC/B;EACT,UAAU;EACV,eAAe,gBAAgB,GAAG,GAAG;;;;;;;;;;EAUrC,QAAQ,QAAQ,YAAY,OAAO,WAAW;;;;;;;;;;;EAW9C,YAAY,UAAU,qBAAqB,QAAQ,cAAc;;;;;;;;;EASjE;;;;;;EAMA;;KAGU,eAAe,UAAU,WAAW;EAC9C,eAAe;EACf;;EAEA,gBAAgB,cAAc;;;EAG9B;;KAGU,SACV,UAAU,WACV,GACA,UAAU,eAAe;EAEzB,QACE,mBACA,UAAU,eAAe,GAAG,KAC3B,cAAc,GAAG,GAAG;;;;;;;;;KAUb;EAEN;EAAc;EAAc;;EAC5B;EAAY;;;;;;;KAON;WACD;EACT,KAAK;EACL,MAAM,eAAe;EACrB,iBAAiB,cAAc;EAC/B,iBACE,iBACA,WAAW;IAAS;;EAEtB,iBACE,eACA,WAAW;IAAS;IAAc;;EAEpC,iBAAiB,eAAe;;KAGtB,kCAAkC,gBAAgB;KAElD,oBACV,UAAU,WACV,GACA,UAAU,eAAe;EAEzB,SAAS,QAAQ,GAAG,GAAG;;EAEvB,KAAK;;EAEL,eAAe,WAAW;;;EAG1B,YAAY;;;EAGZ;;;;;;;;EAQA;;iBAirBc,aACd,UAAU,WACV,GACA,UAAU,eAAe,sBACzB,SAAS,oBAAoB,GAAG,GAAG,KAAK,SAAS,GAAG,GAAG"}
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;KAqDY;;;;;;;KAQA;EACN;;EAEA;;EAEA;;EAEA,OAAO;;EAEP;EAAgB;;EAChB;;;;;;;;;;KAUM,WAAW,UAAU,aAAa,QAAQ,cAAc;WACzD,WAAW,QAAQ,cAAc;;KAGhC;;;;;;EAMV;;EAEA;;;KAIU;;EAEV;;EAEA;;EAEA;;;KAIU,gBACV,UAAU,WACV,GACA,UAAU,eAAe,wBACvB,aACF;;EAGE,UAAU,YAAY;;;EAIxB,OAAO;;;;;EAKP,QAAQ,cAAc;;;EAGtB;;EAEA,SAAS;EACT,YAAY;;;;;;;KAQF,sBAAsB,UAAU;;;;;;;;;;EAU1C,YAAY,QAAQ,cAAc;;KAGxB,cACV,UAAU,WACV,GACA,UAAU,eAAe,wBACvB,aAAa,GAAG,sBAAsB;WAC/B;EACT,UAAU;EACV,eAAe,gBAAgB,GAAG,GAAG;;;;;;;;;;EAUrC,QAAQ,QAAQ,YAAY,OAAO,WAAW;;;;;;;;;;;EAW9C,YAAY,UAAU,qBAAqB,QAAQ,cAAc;;;;;;;;;EASjE;;;;;;EAMA;;KAGU,eAAe,UAAU,WAAW;EAC9C,eAAe;EACf;;EAEA,gBAAgB,cAAc;;;EAG9B;;KAGU,SACV,UAAU,WACV,GACA,UAAU,eAAe;EAEzB,QACE,mBACA,UAAU,eAAe,GAAG,KAC3B,cAAc,GAAG,GAAG;;;;;;;;;KAUb,wBAAwB;KAExB,YACR;EACE;EAAc,MAAM;EAAY,QAAQ;;EAExC;EAAY,KAAK;EAAY;;EAE7B;EAAY;EAAa;;;;;;;KAOnB;WACD;EACT,KAAK;EACL,MAAM,eAAe;EACrB,iBAAiB,cAAc;EAC/B,iBACE,iBACA,WAAW;IAAS;;EAEtB,iBACE,eACA,WAAW;IAAS;IAAc;;EAEpC,iBAAiB,eAAe;;KAGtB,kCAAkC,gBAAgB;KAElD,oBACV,UAAU,WACV,GACA,UAAU,eAAe;EAEzB,SAAS,QAAQ,GAAG,GAAG;;EAEvB,KAAK;;EAEL,eAAe,WAAW;;;EAG1B,YAAY;;;EAGZ;;;;;;;;EAQA;;iBAi6Bc,aACd,UAAU,WACV,GACA,UAAU,eAAe,sBACzB,SAAS,oBAAoB,GAAG,GAAG,KAAK,SAAS,GAAG,GAAG"}
package/dist/client.js CHANGED
@@ -20,10 +20,15 @@ import { c as isWireEvent, d as parseSocketFrame, f as presencePatchFromWire, l
20
20
  const PUSH_ATTEMPTS = 3;
21
21
  const DEFAULT_GC_TIME_MS = 3e5;
22
22
  const DEFAULT_HISTORY_LIMIT = 50;
23
+ const sessionUrl = (url, sessionId) => {
24
+ const resolved = typeof url === "function" ? url(sessionId) : url;
25
+ if (typeof resolved !== "string") throw new TypeError("a session URL function must return a string");
26
+ return resolved;
27
+ };
23
28
  const httpTransport = (routes, fetchImpl, presence) => ({
24
29
  async *connect({ sessionId, startAfter, signal }) {
25
- const sep = routes.stream.includes("?") ? "&" : "?";
26
- const res = await fetchImpl(`${routes.stream}${sep}sessionId=${encodeURIComponent(sessionId)}&index=${startAfter}`, {
30
+ const streamUrl = sessionUrl(routes.stream, sessionId);
31
+ const res = await fetchImpl(`${streamUrl}${streamUrl.includes("?") ? "&" : "?"}sessionId=${encodeURIComponent(sessionId)}&index=${startAfter}`, {
27
32
  headers: { accept: "text/event-stream" },
28
33
  signal
29
34
  });
@@ -75,7 +80,7 @@ const httpTransport = (routes, fetchImpl, presence) => ({
75
80
  }
76
81
  },
77
82
  async push(body) {
78
- const res = await fetchImpl(routes.push, {
83
+ const res = await fetchImpl(sessionUrl(routes.push, body.sessionId), {
79
84
  method: "POST",
80
85
  headers: { "content-type": "application/json" },
81
86
  body: JSON.stringify(body)
@@ -88,8 +93,8 @@ const httpTransport = (routes, fetchImpl, presence) => ({
88
93
  throw errorFromWire(await res.json().catch(() => null)) ?? new A2Error("STORE_UNAVAILABLE", `push failed with ${res.status}`);
89
94
  },
90
95
  async history({ sessionId, gte, lte }) {
91
- const sep = routes.stream.includes("?") ? "&" : "?";
92
- const res = await fetchImpl(`${routes.stream}${sep}sessionId=${encodeURIComponent(sessionId)}&gte=${gte}&lte=${lte}`, { headers: { accept: "application/json" } });
96
+ const streamUrl = sessionUrl(routes.stream, sessionId);
97
+ const res = await fetchImpl(`${streamUrl}${streamUrl.includes("?") ? "&" : "?"}sessionId=${encodeURIComponent(sessionId)}&gte=${gte}&lte=${lte}`, { headers: { accept: "application/json" } });
93
98
  if (!res.ok) throw errorFromWire(await res.json().catch(() => null)) ?? new A2Error("STORE_UNAVAILABLE", `history failed with ${res.status}`);
94
99
  const rows = await res.json();
95
100
  if (!Array.isArray(rows) || !rows.every(isWireEvent)) throw new A2Error("STORE_UNAVAILABLE", "history response was not a list of events");
@@ -98,7 +103,7 @@ const httpTransport = (routes, fetchImpl, presence) => ({
98
103
  sendPresence(envelope) {
99
104
  (async () => {
100
105
  try {
101
- await (await fetchImpl(routes.push, {
106
+ await (await fetchImpl(sessionUrl(routes.push, envelope.sessionId), {
102
107
  method: "POST",
103
108
  headers: { "content-type": "application/json" },
104
109
  body: JSON.stringify(envelope)
@@ -109,6 +114,217 @@ const httpTransport = (routes, fetchImpl, presence) => ({
109
114
  });
110
115
  /** `WebSocket.OPEN` — fixed by the spec, identical in every runtime. */
111
116
  const WS_OPEN = 1;
117
+ const webSocketUrl = (value) => {
118
+ let resolved;
119
+ if (/^(https?|wss?):\/\//i.test(value)) resolved = new URL(value);
120
+ else {
121
+ const base = globalThis.location?.href;
122
+ if (base === void 0) throw new TypeError(`cannot resolve the relative ws url '${value}' without a browser location — pass an absolute ws:// or wss:// url`);
123
+ resolved = new URL(value, base);
124
+ }
125
+ if (resolved.protocol === "http:") resolved.protocol = "ws:";
126
+ else if (resolved.protocol === "https:") resolved.protocol = "wss:";
127
+ return resolved;
128
+ };
129
+ const socketReadyWaiters = () => {
130
+ const sessions = /* @__PURE__ */ new Map();
131
+ const notify = (sessionId) => {
132
+ const waiters = sessions.get(sessionId);
133
+ if (!waiters) return;
134
+ sessions.delete(sessionId);
135
+ for (const waiter of waiters) waiter();
136
+ };
137
+ const wait = (sessionId, maxWaitMs) => new Promise((resolve) => {
138
+ let waiters = sessions.get(sessionId);
139
+ if (!waiters) {
140
+ waiters = /* @__PURE__ */ new Set();
141
+ sessions.set(sessionId, waiters);
142
+ }
143
+ let settled = false;
144
+ const done = () => {
145
+ if (settled) return;
146
+ settled = true;
147
+ clearTimeout(timer);
148
+ waiters.delete(done);
149
+ if (waiters.size === 0) sessions.delete(sessionId);
150
+ resolve();
151
+ };
152
+ const timer = setTimeout(done, maxWaitMs);
153
+ timer.unref?.();
154
+ waiters.add(done);
155
+ });
156
+ return {
157
+ notify,
158
+ wait
159
+ };
160
+ };
161
+ const rejectPendingAcks = (pending) => {
162
+ if (pending.size === 0) return;
163
+ const error = new A2Error("STORE_UNAVAILABLE", "socket closed with the ack outstanding");
164
+ for (const waiter of pending.values()) waiter.reject(error);
165
+ pending.clear();
166
+ };
167
+ const sessionWsTransport = (url, webSocketImpl, presence) => {
168
+ const connections = /* @__PURE__ */ new Map();
169
+ const ready = socketReadyWaiters();
170
+ let nextReq = 1;
171
+ const socketUrl = (sessionId, startAfter) => {
172
+ const resolved = webSocketUrl(sessionUrl(url, sessionId));
173
+ resolved.searchParams.set("sessionId", sessionId);
174
+ resolved.searchParams.set("index", String(startAfter));
175
+ return resolved.href;
176
+ };
177
+ return {
178
+ async *connect({ sessionId, startAfter, signal }) {
179
+ if (webSocketImpl === void 0) throw new TypeError("no WebSocket implementation available — pass one via createClient({ webSocket }) where the global is missing");
180
+ const socket = new webSocketImpl(socketUrl(sessionId, startAfter));
181
+ const connection = {
182
+ socket,
183
+ pending: /* @__PURE__ */ new Map()
184
+ };
185
+ connections.set(sessionId, connection);
186
+ const queue = [];
187
+ let ended = false;
188
+ let failure = null;
189
+ let wake = null;
190
+ const notify = () => {
191
+ wake?.();
192
+ wake = null;
193
+ };
194
+ const finish = (error) => {
195
+ if (ended) return;
196
+ ended = true;
197
+ failure = error;
198
+ if (connections.get(sessionId) === connection) connections.delete(sessionId);
199
+ rejectPendingAcks(connection.pending);
200
+ notify();
201
+ };
202
+ socket.addEventListener("open", () => {
203
+ if (connections.get(sessionId) !== connection || ended) return;
204
+ queue.push({ kind: "ping" });
205
+ notify();
206
+ ready.notify(sessionId);
207
+ });
208
+ socket.addEventListener("message", (event) => {
209
+ if (connections.get(sessionId) !== connection || ended) return;
210
+ if (typeof event.data !== "string") return;
211
+ const frame = parseSocketFrame(event.data);
212
+ if (frame === null) return;
213
+ switch (frame.kind) {
214
+ case "ack": {
215
+ const waiter = connection.pending.get(frame.req);
216
+ if (waiter === void 0) return;
217
+ connection.pending.delete(frame.req);
218
+ if (frame.sessionId !== void 0 || "events" in frame && frame.events.some((row) => row.sessionId !== sessionId)) {
219
+ waiter.reject(new A2Error("STORE_UNAVAILABLE", "ack does not belong to the socket session"));
220
+ return;
221
+ }
222
+ if ("events" in frame) waiter.resolve(frame.events);
223
+ else waiter.reject(frame.error);
224
+ return;
225
+ }
226
+ case "event":
227
+ if (frame.event.sessionId !== sessionId) return;
228
+ queue.push({
229
+ kind: "event",
230
+ event: frame.event
231
+ });
232
+ break;
233
+ case "presence":
234
+ if (presence && frame.sessionId === void 0) queue.push({
235
+ kind: "presence",
236
+ patch: frame.patch
237
+ });
238
+ break;
239
+ case "presence-snapshot":
240
+ if (presence && frame.sessionId === void 0) queue.push({
241
+ kind: "presence-snapshot",
242
+ snapshot: frame.snapshot
243
+ });
244
+ break;
245
+ case "ping":
246
+ queue.push(frame);
247
+ break;
248
+ case "subscribed":
249
+ case "unsubscribed": return;
250
+ }
251
+ notify();
252
+ });
253
+ socket.addEventListener("close", (event) => {
254
+ finish(event.code === 1e3 ? null : /* @__PURE__ */ new Error(`socket closed (${event.code}${event.reason ? `: ${event.reason}` : ""})`));
255
+ });
256
+ socket.addEventListener("error", () => {
257
+ finish(/* @__PURE__ */ new Error("socket error"));
258
+ });
259
+ const onAbort = () => {
260
+ finish(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("stream aborted"));
261
+ try {
262
+ socket.close(1e3);
263
+ } catch {}
264
+ };
265
+ if (signal.aborted) onAbort();
266
+ else signal.addEventListener("abort", onAbort, { once: true });
267
+ try {
268
+ for (;;) {
269
+ while (queue.length > 0) yield queue.shift();
270
+ if (ended) {
271
+ if (failure) throw failure;
272
+ return;
273
+ }
274
+ await new Promise((resolve) => {
275
+ wake = resolve;
276
+ });
277
+ }
278
+ } finally {
279
+ signal.removeEventListener("abort", onAbort);
280
+ finish(null);
281
+ try {
282
+ socket.close(1e3);
283
+ } catch {}
284
+ }
285
+ },
286
+ async push(body) {
287
+ const connection = connections.get(body.sessionId);
288
+ if (connection === void 0 || connection.socket.readyState !== WS_OPEN) throw new A2Error("STORE_UNAVAILABLE", "no open socket for the session");
289
+ const req = nextReq;
290
+ nextReq += 1;
291
+ return await new Promise((resolve, reject) => {
292
+ connection.pending.set(req, {
293
+ resolve,
294
+ reject
295
+ });
296
+ const frame = {
297
+ kind: "push",
298
+ req,
299
+ events: body.events
300
+ };
301
+ try {
302
+ connection.socket.send(JSON.stringify(frame));
303
+ } catch (cause) {
304
+ connection.pending.delete(req);
305
+ reject(new A2Error("STORE_UNAVAILABLE", "socket send failed", { cause }));
306
+ }
307
+ });
308
+ },
309
+ awaitReady(sessionId, maxWaitMs) {
310
+ if (connections.get(sessionId)?.socket.readyState === WS_OPEN) return Promise.resolve();
311
+ return ready.wait(sessionId, maxWaitMs);
312
+ },
313
+ sendPresence(envelope) {
314
+ const connection = connections.get(envelope.sessionId);
315
+ if (connection === void 0 || connection.socket.readyState !== WS_OPEN) return;
316
+ const frame = {
317
+ kind: "presence",
318
+ ...envelope.presence
319
+ };
320
+ try {
321
+ connection.socket.send(JSON.stringify(frame));
322
+ } catch {
323
+ return;
324
+ }
325
+ }
326
+ };
327
+ };
112
328
  const wakeChannel = (channel) => {
113
329
  channel.wake?.();
114
330
  channel.wake = null;
@@ -119,34 +335,16 @@ const subscribeFrame = (sessions) => {
119
335
  sessions
120
336
  });
121
337
  };
122
- const wsTransport = (url, webSocketImpl, presence) => {
338
+ const multiplexedWsTransport = (url, webSocketImpl, presence) => {
123
339
  const channels = /* @__PURE__ */ new Map();
124
340
  const pending = /* @__PURE__ */ new Map();
125
341
  /** Push retries parked in `awaitReady`, flushed per session on its
126
342
  * subscribe confirmation. */
127
- const readyWaiters = /* @__PURE__ */ new Map();
343
+ const ready = socketReadyWaiters();
128
344
  let socket = null;
129
345
  let socketOpen = false;
130
346
  let watchdog;
131
347
  let nextReq = 1;
132
- const flushReadyWaiters = (sessionId) => {
133
- const waiters = readyWaiters.get(sessionId);
134
- if (!waiters) return;
135
- readyWaiters.delete(sessionId);
136
- for (const waiter of waiters) waiter();
137
- };
138
- const socketUrl = () => {
139
- let resolved;
140
- if (/^(https?|wss?):\/\//i.test(url)) resolved = new URL(url);
141
- else {
142
- const base = globalThis.location?.href;
143
- if (base === void 0) throw new TypeError(`cannot resolve the relative ws url '${url}' without a browser location — pass an absolute ws:// or wss:// url`);
144
- resolved = new URL(url, base);
145
- }
146
- if (resolved.protocol === "http:") resolved.protocol = "ws:";
147
- else if (resolved.protocol === "https:") resolved.protocol = "wss:";
148
- return resolved.href;
149
- };
150
348
  const trySend = (data) => {
151
349
  if (socket === null || !socketOpen) return;
152
350
  try {
@@ -169,11 +367,7 @@ const wsTransport = (url, webSocketImpl, presence) => {
169
367
  socketOpen = false;
170
368
  clearTimeout(watchdog);
171
369
  watchdog = void 0;
172
- if (pending.size > 0) {
173
- const rejection = new A2Error("STORE_UNAVAILABLE", "socket closed with the ack outstanding");
174
- for (const waiter of pending.values()) waiter.reject(rejection);
175
- pending.clear();
176
- }
370
+ rejectPendingAcks(pending);
177
371
  return current;
178
372
  };
179
373
  const failSocket = (failure) => {
@@ -218,7 +412,7 @@ const wsTransport = (url, webSocketImpl, presence) => {
218
412
  if (channel === void 0 || channel.ended || channel.live) return;
219
413
  channel.live = true;
220
414
  deliver(channel, { kind: "ping" });
221
- flushReadyWaiters(frame.sessionId);
415
+ ready.notify(frame.sessionId);
222
416
  return;
223
417
  }
224
418
  case "unsubscribed": {
@@ -267,7 +461,7 @@ const wsTransport = (url, webSocketImpl, presence) => {
267
461
  const ensureSocket = () => {
268
462
  if (socket !== null) return;
269
463
  if (webSocketImpl === void 0) throw new TypeError("no WebSocket implementation available — pass one via createClient({ webSocket }) where the global is missing");
270
- const created = new webSocketImpl(socketUrl());
464
+ const created = new webSocketImpl(webSocketUrl(url).href);
271
465
  socket = created;
272
466
  socketOpen = false;
273
467
  armWatchdog(created);
@@ -378,25 +572,7 @@ const wsTransport = (url, webSocketImpl, presence) => {
378
572
  awaitReady(sessionId, maxWaitMs) {
379
573
  const channel = channels.get(sessionId);
380
574
  if (channel !== void 0 && channel.live && !channel.ended && socketOpen) return Promise.resolve();
381
- return new Promise((resolve) => {
382
- let waiters = readyWaiters.get(sessionId);
383
- if (!waiters) {
384
- waiters = /* @__PURE__ */ new Set();
385
- readyWaiters.set(sessionId, waiters);
386
- }
387
- let settled = false;
388
- const done = () => {
389
- if (settled) return;
390
- settled = true;
391
- clearTimeout(timer);
392
- waiters.delete(done);
393
- if (waiters.size === 0) readyWaiters.delete(sessionId);
394
- resolve();
395
- };
396
- const timer = setTimeout(done, maxWaitMs);
397
- timer.unref?.();
398
- waiters.add(done);
399
- });
575
+ return ready.wait(sessionId, maxWaitMs);
400
576
  },
401
577
  sendPresence(envelope) {
402
578
  const channel = channels.get(envelope.sessionId);
@@ -411,7 +587,7 @@ const wsTransport = (url, webSocketImpl, presence) => {
411
587
  };
412
588
  };
413
589
  const transportFor = (api, fetchImpl, webSocketImpl, presence) => {
414
- if (typeof api === "string") return httpTransport({
590
+ if (typeof api === "string" || typeof api === "function") return httpTransport({
415
591
  push: api,
416
592
  stream: api
417
593
  }, fetchImpl, presence);
@@ -419,7 +595,11 @@ const transportFor = (api, fetchImpl, webSocketImpl, presence) => {
419
595
  push: api.push,
420
596
  stream: api.stream
421
597
  }, fetchImpl, presence);
422
- return wsTransport(api.url, webSocketImpl, presence);
598
+ if (api.multiplex === true) {
599
+ if (typeof api.url !== "string") throw new TypeError("a multiplexed WebSocket URL must be a string");
600
+ return multiplexedWsTransport(api.url, webSocketImpl, presence);
601
+ }
602
+ return sessionWsTransport(api.url, webSocketImpl, presence);
423
603
  };
424
604
  function createClient(options) {
425
605
  const { reducer, api } = options;