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 +14 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/{actor-shared-BACubf4x.d.ts → actor-shared-USo5MyuF.d.ts} +2 -2
- package/dist/{actor-shared-BACubf4x.d.ts.map → actor-shared-USo5MyuF.d.ts.map} +1 -1
- package/dist/actor.d.ts +2 -2
- package/dist/actor.js +1 -1
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.js +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/client.d.ts +13 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +234 -54
- package/dist/client.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +1 -1
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +1 -1
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-CKY3_lbw.d.ts → server-DgCrSuhB.d.ts} +3 -1
- package/dist/server-DgCrSuhB.d.ts.map +1 -0
- package/dist/{server-CBET-jSz.js → server-DlLyvaSH.js} +135 -76
- package/dist/server-DlLyvaSH.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/docs/guides/10-transports.mdx +72 -22
- package/docs/reference/01-api.mdx +30 -12
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/client.ts +332 -87
- package/src/server-fetch.ts +51 -23
- package/src/session-socket.ts +216 -81
- package/dist/server-CBET-jSz.js.map +0 -1
- package/dist/server-CKY3_lbw.d.ts.map +0 -1
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
|
package/dist/actor-client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as ActorRefusedError } from "./actor-shared-
|
|
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 = {
|
package/dist/actor-react.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
136
|
+
//# sourceMappingURL=actor-shared-USo5MyuF.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor-shared-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
/**
|
package/dist/ai-server.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
148
|
-
*
|
|
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
|
|
150
|
+
type SessionUrl = string | ((sessionId: string) => string);
|
|
151
|
+
type ClientApi = SessionUrl | {
|
|
151
152
|
type: "http";
|
|
152
|
-
push:
|
|
153
|
-
stream:
|
|
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
|
package/dist/client.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
26
|
-
const res = await fetchImpl(`${
|
|
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
|
|
92
|
-
const res = await fetchImpl(`${
|
|
96
|
+
const streamUrl = sessionUrl(routes.stream, sessionId);
|
|
97
|
+
const res = await fetchImpl(`${streamUrl}${streamUrl.includes("?") ? "&" : "?"}sessionId=${encodeURIComponent(sessionId)}>e=${gte}<e=${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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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;
|