experimental-a2 0.2.0 → 0.4.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 +169 -0
- package/dist/ai-B4YhEnfw.d.ts +333 -0
- package/dist/ai-server.d.ts +48 -9
- package/dist/ai-server.js +121 -49
- package/dist/ai.d.ts +2 -303
- package/dist/ai.js +231 -86
- package/dist/cli-B3VuxoDe.js +597 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +5 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +2 -0
- package/dist/client-BrfDXQ8A.d.ts +155 -0
- package/dist/client-Bt4tAKi9.js +798 -0
- package/dist/client.d.ts +2 -90
- package/dist/client.js +1 -409
- package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
- package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
- package/dist/devtools-J_jZ2vQf.d.ts +151 -0
- package/dist/devtools-kJJaORn-.js +338 -0
- package/dist/devtools-server.browser.js +1 -1
- package/dist/devtools-server.d.ts +2 -2
- package/dist/devtools-server.js +224 -43
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +2 -0
- package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
- package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
- package/dist/http.d.ts +71 -13
- package/dist/http.js +302 -41
- package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
- package/dist/internal-aEotMzu_.js +209 -0
- package/dist/otel.d.ts +1 -1
- package/dist/platform-B4TnJtWu.js +32 -0
- package/dist/react.d.ts +37 -14
- package/dist/react.js +26 -15
- package/dist/scheduler-qstash.d.ts +78 -0
- package/dist/scheduler-qstash.js +499 -0
- package/dist/scheduler-task-BpzhPnRS.js +54 -0
- package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
- package/dist/scheduler-vercel.js +226 -0
- package/dist/server-CcNnFnoW.js +1405 -0
- package/dist/server-YtPq7hjw.d.ts +260 -0
- package/dist/server.d.ts +4 -155
- package/dist/server.js +2 -2
- package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
- package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
- package/dist/store-codec-DTG0Ftek.js +8 -0
- package/dist/store-memory.d.ts +11 -0
- package/dist/{log-memory.js → store-memory.js} +127 -24
- package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
- package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
- package/dist/{log-postgres.js → store-postgres.js} +158 -24
- package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
- package/dist/store-redis-http.d.ts +21 -0
- package/dist/store-redis-http.js +70 -0
- package/dist/store-redis.d.ts +37 -0
- package/dist/store-redis.js +298 -0
- package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
- package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
- package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
- package/dist/testing.browser.d.ts +1 -0
- package/dist/testing.browser.js +4 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +101 -0
- package/dist/wire-DCUZBUlT.js +222 -0
- package/docs/01-quickstart.mdx +4 -5
- package/docs/concepts/01-contracts.mdx +21 -17
- package/docs/concepts/02-handlers.mdx +7 -7
- package/docs/concepts/03-durability.mdx +26 -29
- package/docs/concepts/04-state.mdx +18 -21
- package/docs/guides/01-timers.mdx +154 -54
- package/docs/guides/02-cancellation.mdx +30 -4
- package/docs/guides/03-react.mdx +20 -21
- package/docs/guides/04-local-first.mdx +1 -1
- package/docs/guides/05-production.mdx +321 -60
- package/docs/guides/06-ai-agents.mdx +249 -49
- package/docs/guides/07-devtools.mdx +137 -12
- package/docs/guides/08-application-data.mdx +1 -1
- package/docs/guides/09-presence.mdx +284 -0
- package/docs/guides/10-transports.mdx +131 -0
- package/docs/index.mdx +22 -46
- package/docs/reference/01-api.mdx +751 -102
- package/docs/reference/02-errors.mdx +10 -5
- package/package.json +23 -6
- package/dist/internal-D6wNxTck.js +0 -36
- package/dist/log-memory.d.ts +0 -11
- package/dist/log-redis.d.ts +0 -31
- package/dist/recovery-vercel.js +0 -119
- package/dist/server-DJgD2YWP.js +0 -877
- package/dist/wire-BVsgR8o9.js +0 -62
package/dist/http.js
CHANGED
|
@@ -1,20 +1,75 @@
|
|
|
1
|
-
import { t as A2Error } from "./errors-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
|
+
import { _ as schedulerForServer, c as STREAM_TIMINGS, d as assertServerSchedulerBindings, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, u as assertSchedulerTargets } from "./internal-aEotMzu_.js";
|
|
3
|
+
import { t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
|
|
4
|
+
import { _ as socketFrameFor, f as presencePatchToWire, g as socketErrorAckFor, h as socketAckFor, i as errorToWire, m as presenceSnapshotToWire, n as errorFromWire, o as eventToWire, r as errorStatus, t as SOCKET_PING_FRAME } from "./wire-DCUZBUlT.js";
|
|
4
5
|
//#region src/http.ts
|
|
5
6
|
/**
|
|
6
|
-
* experimental-a2/http — route-side transport helpers (specs/a2-api.md §9–10).
|
|
7
|
+
* experimental-a2/http — route-side transport helpers (specs/a2-api.md §9–10, §13).
|
|
7
8
|
*
|
|
8
9
|
* `sseResponse` pipes a `session.stream()` iterable into a server-sent
|
|
9
|
-
* events Response; `
|
|
10
|
+
* events Response; `schedulerHandler` mounts servers on their configured
|
|
11
|
+
* scheduler callback; `parsePushBody` validates the push envelope;
|
|
10
12
|
* `errorResponse`/`deserializeError` are the A2Error wire pair the push
|
|
11
|
-
* route and the client's `push` share
|
|
13
|
+
* route and the client's `push` share; `sessionSocket` speaks the same
|
|
14
|
+
* lanes — stream down, push and presence up — over one WebSocket.
|
|
12
15
|
*/
|
|
13
16
|
const invalid = (message) => new A2Error("INVALID_PAYLOAD", `malformed push body: ${message}`);
|
|
14
17
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* Create one scheduler callback route for servers sharing the same configured
|
|
19
|
+
* scheduler. Configuration mistakes throw immediately while the route module
|
|
20
|
+
* is evaluated, before a delivery can be acknowledged by the wrong adapter.
|
|
21
|
+
*/
|
|
22
|
+
function schedulerHandler(firstServer, ...servers) {
|
|
23
|
+
if (firstServer === void 0) throw new TypeError("schedulerHandler() requires at least one A2 server");
|
|
24
|
+
const scheduler = schedulerForServer(firstServer);
|
|
25
|
+
const targets = [firstServer, ...servers];
|
|
26
|
+
assertSchedulerTargets(targets, { allowStructural: false });
|
|
27
|
+
assertServerSchedulerBindings(scheduler, targets);
|
|
28
|
+
return scheduler.handler(...targets);
|
|
29
|
+
}
|
|
30
|
+
function parsePresenceSibling(value) {
|
|
31
|
+
if (value === void 0) return void 0;
|
|
32
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalid("presence must be an object when present");
|
|
33
|
+
const { participant, values, seen, at } = value;
|
|
34
|
+
if (typeof participant !== "string" || participant.length === 0) throw invalid("presence.participant must be a non-empty string");
|
|
35
|
+
if (RESERVED_PARTICIPANT_IDS.has(participant)) throw invalid(`presence.participant must not be '${participant}'`);
|
|
36
|
+
if (values === null || typeof values !== "object" || Array.isArray(values)) throw invalid("presence.values must be an object");
|
|
37
|
+
if (seen !== void 0 && (typeof seen !== "number" || !Number.isFinite(seen) || seen < 0)) throw invalid("presence.seen must be a non-negative number when present");
|
|
38
|
+
if (at !== void 0 && (typeof at !== "number" || !Number.isFinite(at) || at < 0 || at > 864e13)) throw invalid("presence.at must be a non-negative epoch-milliseconds timestamp within the Date range when present");
|
|
39
|
+
const out = {
|
|
40
|
+
participant,
|
|
41
|
+
values
|
|
42
|
+
};
|
|
43
|
+
if (seen !== void 0) out.seen = seen;
|
|
44
|
+
if (at !== void 0) out.at = at;
|
|
45
|
+
Object.defineProperty(out, "~a2.pushed", { value: true });
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/** The events half of the envelope — shared verbatim by the POST body
|
|
49
|
+
* and the socket's `push` frames, so the provenance brand and the
|
|
50
|
+
* INVALID_PAYLOAD shapes are one implementation. */
|
|
51
|
+
function parsePushEvents(events) {
|
|
52
|
+
if (!Array.isArray(events) || events.length === 0) throw invalid("events must be a non-empty array");
|
|
53
|
+
return events.map((event, i) => {
|
|
54
|
+
if (event === null || typeof event !== "object") throw invalid(`events[${i}] must be an object`);
|
|
55
|
+
const { type, payload, id } = event;
|
|
56
|
+
if (typeof type !== "string" || type.length === 0) throw invalid(`events[${i}].type must be a non-empty string`);
|
|
57
|
+
if (id !== void 0 && typeof id !== "string") throw invalid(`events[${i}].id must be a string when present`);
|
|
58
|
+
const out = {
|
|
59
|
+
type,
|
|
60
|
+
payload
|
|
61
|
+
};
|
|
62
|
+
if (id !== void 0) out.id = id;
|
|
63
|
+
Object.defineProperty(out, "~a2.pushed", { value: true });
|
|
64
|
+
return out;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Validate the push envelope — `{ sessionId, events, presence? }` —
|
|
69
|
+
* throwing `INVALID_PAYLOAD` on a malformed body. Payload validation
|
|
70
|
+
* against the machine's schemas happens in `append` and `setPresence`,
|
|
71
|
+
* not here. Either plane may appear alone: a presence-only push has no
|
|
72
|
+
* (or empty) `events`.
|
|
18
73
|
*/
|
|
19
74
|
async function parsePushBody(req) {
|
|
20
75
|
let body;
|
|
@@ -24,43 +79,57 @@ async function parsePushBody(req) {
|
|
|
24
79
|
throw new A2Error("INVALID_PAYLOAD", "push body is not valid JSON", { cause });
|
|
25
80
|
}
|
|
26
81
|
if (body === null || typeof body !== "object") throw invalid("expected an object");
|
|
27
|
-
const { sessionId, events } = body;
|
|
82
|
+
const { sessionId, events, presence } = body;
|
|
28
83
|
if (typeof sessionId !== "string" || sessionId.length === 0) throw invalid("sessionId must be a non-empty string");
|
|
29
|
-
|
|
30
|
-
return {
|
|
84
|
+
const parsedPresence = parsePresenceSibling(presence);
|
|
85
|
+
if (parsedPresence !== void 0 && (events === void 0 || Array.isArray(events) && events.length === 0)) return {
|
|
86
|
+
sessionId,
|
|
87
|
+
events: [],
|
|
88
|
+
presence: parsedPresence
|
|
89
|
+
};
|
|
90
|
+
const parsed = parsePushEvents(events);
|
|
91
|
+
return parsedPresence !== void 0 ? {
|
|
92
|
+
sessionId,
|
|
93
|
+
events: parsed,
|
|
94
|
+
presence: parsedPresence
|
|
95
|
+
} : {
|
|
31
96
|
sessionId,
|
|
32
|
-
events:
|
|
33
|
-
if (event === null || typeof event !== "object") throw invalid(`events[${i}] must be an object`);
|
|
34
|
-
const { type, payload, id } = event;
|
|
35
|
-
if (typeof type !== "string" || type.length === 0) throw invalid(`events[${i}].type must be a non-empty string`);
|
|
36
|
-
if (id !== void 0 && typeof id !== "string") throw invalid(`events[${i}].id must be a string when present`);
|
|
37
|
-
const out = {
|
|
38
|
-
type,
|
|
39
|
-
payload
|
|
40
|
-
};
|
|
41
|
-
if (id !== void 0) out.id = id;
|
|
42
|
-
Object.defineProperty(out, "~a2.pushed", { value: true });
|
|
43
|
-
return out;
|
|
44
|
-
})
|
|
97
|
+
events: parsed
|
|
45
98
|
};
|
|
46
99
|
}
|
|
47
100
|
/**
|
|
48
101
|
* Pipe a live event iterable into an SSE `Response`. Each event is one
|
|
49
|
-
* frame — `id:` carries the log index, `data:` the JSON event. A
|
|
102
|
+
* frame — `id:` carries the event-log index, `data:` the JSON event. A
|
|
103
|
+
* `stream({ presence: true })` iterable also yields presence items,
|
|
104
|
+
* which ride as named frames old clients skip: `event:
|
|
105
|
+
* presence-snapshot` first (the map, ISO dates), then `event: presence`
|
|
106
|
+
* per patch — no `id:`, presence never advances the resume frontier. A
|
|
50
107
|
* disconnecting client cancels the stream, which closes the underlying
|
|
51
108
|
* subscription. Two kinds of comment frames ride along: a `: connected`
|
|
52
109
|
* prelude that flushes headers immediately, and a `: ping` heartbeat
|
|
53
110
|
* every 15 seconds so clients (and proxies) can tell a quiet stream
|
|
54
111
|
* from a dead connection — the session client's stall watchdog counts
|
|
55
|
-
* on it.
|
|
112
|
+
* on it. When the platform exposes an invocation deadline, the response
|
|
113
|
+
* closes cleanly one second before it so the client reconnects without
|
|
114
|
+
* a platform-timeout failure.
|
|
56
115
|
*/
|
|
57
116
|
function sseResponse(iterable) {
|
|
58
117
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
59
118
|
const encoder = new TextEncoder();
|
|
60
119
|
let heartbeat;
|
|
61
|
-
|
|
120
|
+
let deadlineTimer;
|
|
121
|
+
let closed = false;
|
|
122
|
+
let iteratorReturned = false;
|
|
123
|
+
const stopTimers = () => {
|
|
62
124
|
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
125
|
+
if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
|
|
63
126
|
heartbeat = void 0;
|
|
127
|
+
deadlineTimer = void 0;
|
|
128
|
+
};
|
|
129
|
+
const returnIterator = async () => {
|
|
130
|
+
if (iteratorReturned) return;
|
|
131
|
+
iteratorReturned = true;
|
|
132
|
+
await iterator.return?.();
|
|
64
133
|
};
|
|
65
134
|
const stream = new ReadableStream({
|
|
66
135
|
start(controller) {
|
|
@@ -69,23 +138,49 @@ function sseResponse(iterable) {
|
|
|
69
138
|
try {
|
|
70
139
|
controller.enqueue(encoder.encode(": ping\n\n"));
|
|
71
140
|
} catch {
|
|
72
|
-
|
|
141
|
+
stopTimers();
|
|
73
142
|
}
|
|
74
143
|
}, STREAM_TIMINGS.sseHeartbeatMs);
|
|
75
144
|
heartbeat.unref?.();
|
|
145
|
+
const invocationDeadline = invocationDeadlineMs();
|
|
146
|
+
if (invocationDeadline !== null) {
|
|
147
|
+
const delay = Math.max(0, invocationDeadline - Date.now() - STREAM_TIMINGS.sseDeadlineGraceMs);
|
|
148
|
+
deadlineTimer = setTimeout(() => {
|
|
149
|
+
if (closed) return;
|
|
150
|
+
closed = true;
|
|
151
|
+
stopTimers();
|
|
152
|
+
returnIterator().catch(() => {});
|
|
153
|
+
try {
|
|
154
|
+
controller.close();
|
|
155
|
+
} catch {}
|
|
156
|
+
}, delay);
|
|
157
|
+
deadlineTimer.unref?.();
|
|
158
|
+
}
|
|
76
159
|
},
|
|
77
160
|
async pull(controller) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
161
|
+
try {
|
|
162
|
+
const { value, done } = await iterator.next();
|
|
163
|
+
if (closed) return;
|
|
164
|
+
if (done) {
|
|
165
|
+
closed = true;
|
|
166
|
+
stopTimers();
|
|
167
|
+
controller.close();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
controller.enqueue(encoder.encode(frameFor(value)));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (closed) return;
|
|
173
|
+
closed = true;
|
|
174
|
+
stopTimers();
|
|
175
|
+
returnIterator().catch(() => {});
|
|
176
|
+
throw error;
|
|
83
177
|
}
|
|
84
|
-
controller.enqueue(encoder.encode(`id: ${value.index}\ndata: ${JSON.stringify(eventToWire(value))}\n\n`));
|
|
85
178
|
},
|
|
86
179
|
async cancel() {
|
|
87
|
-
|
|
88
|
-
|
|
180
|
+
if (closed) return;
|
|
181
|
+
closed = true;
|
|
182
|
+
stopTimers();
|
|
183
|
+
await returnIterator();
|
|
89
184
|
}
|
|
90
185
|
});
|
|
91
186
|
return new Response(stream, {
|
|
@@ -97,17 +192,23 @@ function sseResponse(iterable) {
|
|
|
97
192
|
}
|
|
98
193
|
});
|
|
99
194
|
}
|
|
195
|
+
function frameFor(item) {
|
|
196
|
+
if ("snapshot" in item) return `event: presence-snapshot\ndata: ${JSON.stringify(presenceSnapshotToWire(item))}\n\n`;
|
|
197
|
+
if ("participant" in item) return `event: presence\ndata: ${JSON.stringify(presencePatchToWire(item))}\n\n`;
|
|
198
|
+
return `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(item))}\n\n`;
|
|
199
|
+
}
|
|
100
200
|
/**
|
|
101
201
|
* Serialize an error into the documented wire shape,
|
|
102
202
|
* `{ error: { code, message, details } }`, with the mapped status
|
|
103
|
-
* (400 for caller bugs, 503 for
|
|
104
|
-
* 503
|
|
203
|
+
* (400 for caller bugs, 503 for STORE_UNAVAILABLE). Non-A2Errors become a
|
|
204
|
+
* 503 STORE_UNAVAILABLE — from the client's perspective an unknown server
|
|
105
205
|
* failure is retryable-once, not a protocol contract.
|
|
106
206
|
*/
|
|
107
207
|
function errorResponse(error) {
|
|
108
|
-
const a2error =
|
|
208
|
+
const a2error = asA2Error(error);
|
|
109
209
|
return Response.json(errorToWire(a2error), { status: errorStatus(a2error.code) });
|
|
110
210
|
}
|
|
211
|
+
const asA2Error = (error) => error instanceof A2Error ? error : new A2Error("STORE_UNAVAILABLE", "internal error", { cause: error });
|
|
111
212
|
/**
|
|
112
213
|
* The other half of the wire pair: rebuild an `A2Error` from a response
|
|
113
214
|
* body, or null if the body isn't one. Client `push` uses it so both
|
|
@@ -116,5 +217,165 @@ function errorResponse(error) {
|
|
|
116
217
|
function deserializeError(body) {
|
|
117
218
|
return errorFromWire(body);
|
|
118
219
|
}
|
|
220
|
+
const messageText = (data) => {
|
|
221
|
+
if (typeof data === "string") return data;
|
|
222
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
223
|
+
if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
|
|
224
|
+
if (Array.isArray(data) && data.every(ArrayBuffer.isView)) {
|
|
225
|
+
const decoder = new TextDecoder();
|
|
226
|
+
let text = "";
|
|
227
|
+
for (const part of data) text += decoder.decode(part, { stream: true });
|
|
228
|
+
return text + decoder.decode();
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* How many unparseable or invalid frames a socket may send before it
|
|
234
|
+
* is closed (1008): the budget absorbs isolated client bugs without
|
|
235
|
+
* letting a broken peer spin the validation path forever. Push frames
|
|
236
|
+
* with a usable `req` never count — their failures are answered with
|
|
237
|
+
* error acks, exactly like the POST path's 4xx bodies.
|
|
238
|
+
*/
|
|
239
|
+
const MAX_MALFORMED_FRAMES = 5;
|
|
240
|
+
/**
|
|
241
|
+
* Speak the A2 socket protocol (specs/a2-api.md §13) against one
|
|
242
|
+
* session: the live stream pumps down as `kind`-framed JSON with a
|
|
243
|
+
* `ping` heartbeat on the SSE cadence; `push` frames come up through
|
|
244
|
+
* the same validation seams as `parsePushBody` (same provenance brand,
|
|
245
|
+
* same `validatePush` calls, once per plane) and are answered with an
|
|
246
|
+
* ack; `presence` frames are fire-and-forget. Socket close or error
|
|
247
|
+
* tears the pump down and releases the store subscription; the stream
|
|
248
|
+
* ending closes the socket.
|
|
249
|
+
*/
|
|
250
|
+
function sessionSocket(session, socket, options) {
|
|
251
|
+
const target = session;
|
|
252
|
+
const startAfter = options?.startAfter ?? 0;
|
|
253
|
+
const iterator = (options?.presence === true ? target.stream({
|
|
254
|
+
startAfter,
|
|
255
|
+
presence: true
|
|
256
|
+
}) : target.stream({ startAfter }))[Symbol.asyncIterator]();
|
|
257
|
+
let torn = false;
|
|
258
|
+
let malformed = 0;
|
|
259
|
+
let heartbeat;
|
|
260
|
+
let deadline;
|
|
261
|
+
const teardown = () => {
|
|
262
|
+
if (torn) return;
|
|
263
|
+
torn = true;
|
|
264
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
265
|
+
if (deadline !== void 0) clearTimeout(deadline);
|
|
266
|
+
iterator.return?.()?.catch(() => {});
|
|
267
|
+
};
|
|
268
|
+
const shutdown = (code, reason) => {
|
|
269
|
+
if (torn) return;
|
|
270
|
+
teardown();
|
|
271
|
+
try {
|
|
272
|
+
socket.close(code, reason);
|
|
273
|
+
} catch {}
|
|
274
|
+
};
|
|
275
|
+
const send = (data) => {
|
|
276
|
+
if (torn) return;
|
|
277
|
+
if (typeof socket.bufferedAmount === "number" && socket.bufferedAmount > SOCKET_TIMINGS.disconnectWaterMarkBytes) {
|
|
278
|
+
shutdown(1008, "backpressure: peer not reading");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
socket.send(data);
|
|
283
|
+
} catch {
|
|
284
|
+
teardown();
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const malformedFrame = (reason) => {
|
|
288
|
+
malformed += 1;
|
|
289
|
+
if (malformed >= MAX_MALFORMED_FRAMES) shutdown(1008, `too many malformed frames (last: ${reason})`);
|
|
290
|
+
};
|
|
291
|
+
const handlePush = async (frame) => {
|
|
292
|
+
const req = frame["req"];
|
|
293
|
+
if (typeof req !== "number") {
|
|
294
|
+
malformedFrame("push frame without a numeric req");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const events = parsePushEvents(frame["events"]);
|
|
299
|
+
const appended = await target.append(...events);
|
|
300
|
+
send(socketAckFor(req, appended));
|
|
301
|
+
} catch (err) {
|
|
302
|
+
send(socketErrorAckFor(req, asA2Error(err)));
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const handlePresence = async (frame) => {
|
|
306
|
+
if (typeof target.setPresence !== "function") {
|
|
307
|
+
malformedFrame("presence frame on a presence-less session");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const patch = parsePresenceSibling(frame);
|
|
312
|
+
if (patch) await target.setPresence(patch);
|
|
313
|
+
} catch {
|
|
314
|
+
malformedFrame("invalid presence frame");
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
let inFlight = 0;
|
|
318
|
+
const track = (work) => {
|
|
319
|
+
inFlight += 1;
|
|
320
|
+
work.finally(() => {
|
|
321
|
+
inFlight -= 1;
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
const handleMessage = (data) => {
|
|
325
|
+
if (torn) return;
|
|
326
|
+
const text = messageText(data);
|
|
327
|
+
if (text === null) {
|
|
328
|
+
malformedFrame("non-text frame");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
let parsed;
|
|
332
|
+
try {
|
|
333
|
+
parsed = JSON.parse(text);
|
|
334
|
+
} catch {
|
|
335
|
+
malformedFrame("frame is not valid JSON");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
339
|
+
malformedFrame("frame is not an object");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const frame = parsed;
|
|
343
|
+
if (frame["kind"] === "push") {
|
|
344
|
+
if (inFlight >= SOCKET_TIMINGS.maxInFlightMessages) {
|
|
345
|
+
const req = frame["req"];
|
|
346
|
+
if (typeof req === "number") send(socketErrorAckFor(req, new A2Error("STORE_UNAVAILABLE", "push shed: too many in flight on this socket")));
|
|
347
|
+
else malformedFrame("push frame without a numeric req");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
track(handlePush(frame));
|
|
351
|
+
} else if (frame["kind"] === "presence") {
|
|
352
|
+
if (inFlight >= SOCKET_TIMINGS.maxInFlightMessages) return;
|
|
353
|
+
track(handlePresence(frame));
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
socket.on("message", handleMessage);
|
|
357
|
+
socket.on("close", teardown);
|
|
358
|
+
socket.on("error", teardown);
|
|
359
|
+
heartbeat = setInterval(() => send(SOCKET_PING_FRAME), STREAM_TIMINGS.sseHeartbeatMs);
|
|
360
|
+
heartbeat.unref?.();
|
|
361
|
+
if (options?.deadline !== void 0) {
|
|
362
|
+
deadline = setTimeout(() => shutdown(1e3, "deadline"), Math.max(0, options.deadline - Date.now()));
|
|
363
|
+
deadline.unref?.();
|
|
364
|
+
}
|
|
365
|
+
const buffered = () => typeof socket.bufferedAmount === "number" ? socket.bufferedAmount : 0;
|
|
366
|
+
const pump = async () => {
|
|
367
|
+
for (;;) {
|
|
368
|
+
for (;;) {
|
|
369
|
+
if (torn || buffered() <= SOCKET_TIMINGS.highWaterMarkBytes) break;
|
|
370
|
+
await new Promise((resolve) => setTimeout(resolve, SOCKET_TIMINGS.resumePollMs));
|
|
371
|
+
}
|
|
372
|
+
if (torn) return;
|
|
373
|
+
const { value, done } = await iterator.next();
|
|
374
|
+
if (done || torn) return;
|
|
375
|
+
send(socketFrameFor(value));
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
pump().then(() => shutdown(1e3), () => shutdown(1011, "stream failed"));
|
|
379
|
+
}
|
|
119
380
|
//#endregion
|
|
120
|
-
export { deserializeError, errorResponse, parsePushBody, sseResponse };
|
|
381
|
+
export { deserializeError, errorResponse, parsePushBody, schedulerHandler, sessionSocket, sseResponse };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as A2Error } from "./errors-
|
|
1
|
+
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
2
|
//#region src/idempotent-replay.ts
|
|
3
3
|
const canonicalJson = (value) => JSON.stringify(value, (_key, candidate) => {
|
|
4
4
|
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) return candidate;
|
|
5
|
-
return Object.fromEntries(Object.entries(candidate).toSorted(([left], [right]) => left
|
|
5
|
+
return Object.fromEntries(Object.entries(candidate).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
|
|
6
6
|
}) ?? "null";
|
|
7
7
|
const idempotentReplay = (events, existing) => {
|
|
8
8
|
const byId = new Map(existing.map((event) => [event.id, event]));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as EventDefs, c as
|
|
2
|
-
import { a as Event } from "./
|
|
3
|
-
import { i as A2Telemetry, n as A2SpanHandle, r as A2SpanName, t as A2AttributeValue } from "./telemetry-
|
|
4
|
-
import { n as A2ErrorCode, t as A2Error } from "./errors-
|
|
5
|
-
export { type A2AttributeValue, A2Error, type A2ErrorCode, type A2SpanHandle, type A2SpanName, type A2Telemetry, type AppendInput, type Contract, type ContractEvent, type Event, type EventBatchFactory, type EventDefs, type Reducer, type ReducerBuilder, type ReducerOptions, type StandardSchemaV1, contract };
|
|
1
|
+
import { a as EventDefs, c as PresencePatch, f as contract, h as StandardSchemaV1, i as EventBatchFactory, l as PresenceSnapshot, m as ReducerBuilder, n as Contract, o as PresenceDefs, p as Reducer, r as ContractEvent, s as PresenceMap, t as AppendInput, u as ReducerOptions } from "./contract-jIfaR085.js";
|
|
2
|
+
import { a as Event } from "./store-C3sNAaBT.js";
|
|
3
|
+
import { i as A2Telemetry, n as A2SpanHandle, r as A2SpanName, t as A2AttributeValue } from "./telemetry-BjYHTfh2.js";
|
|
4
|
+
import { n as A2ErrorCode, t as A2Error } from "./errors-W6nwJ-fm.js";
|
|
5
|
+
export { type A2AttributeValue, A2Error, type A2ErrorCode, type A2SpanHandle, type A2SpanName, type A2Telemetry, type AppendInput, type Contract, type ContractEvent, type Event, type EventBatchFactory, type EventDefs, type PresenceDefs, type PresenceMap, type PresencePatch, type PresenceSnapshot, type Reducer, type ReducerBuilder, type ReducerOptions, type StandardSchemaV1, contract };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as contract } from "./contract-
|
|
2
|
-
import { t as A2Error } from "./errors-
|
|
1
|
+
import { t as contract } from "./contract-48bUMgcL.js";
|
|
2
|
+
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
3
3
|
export { A2Error, contract };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
//#region src/inspection.ts
|
|
2
|
+
var InspectionUnsupportedError = class extends Error {};
|
|
2
3
|
const serverInspection = /* @__PURE__ */ new WeakMap();
|
|
3
4
|
/** Owned live-inspection cadence; mutable only for white-box tests. */
|
|
4
5
|
const DEVTOOLS_TIMINGS = {
|
|
@@ -7,4 +8,4 @@ const DEVTOOLS_TIMINGS = {
|
|
|
7
8
|
heartbeatMs: 15e3
|
|
8
9
|
};
|
|
9
10
|
//#endregion
|
|
10
|
-
export {
|
|
11
|
+
export { InspectionUnsupportedError as n, serverInspection as r, DEVTOOLS_TIMINGS as t };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//#region src/internal.ts
|
|
2
|
+
/**
|
|
3
|
+
* A null-prototype record for maps keyed by caller-controlled strings
|
|
4
|
+
* (participant ids, presence field names). On a normal object,
|
|
5
|
+
* assigning to '__proto__' rewrites the prototype instead of creating
|
|
6
|
+
* an own property — the prototype-pollution class. Null-prototype
|
|
7
|
+
* objects still satisfy Record types and JSON.stringify cleanly.
|
|
8
|
+
*/
|
|
9
|
+
function nullProtoRecord() {
|
|
10
|
+
return Object.create(null);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* ECMA-262's maximum Date value (±8.64e15 ms). A presence stamp past
|
|
14
|
+
* it becomes an Invalid Date whose toISOString throws — one hostile
|
|
15
|
+
* stamp would poison every subscriber's snapshot frame — so both
|
|
16
|
+
* seams bound `at` to the representable range.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_DATE_MS = 864e13;
|
|
19
|
+
/**
|
|
20
|
+
* Participant ids that collide with Object.prototype plumbing.
|
|
21
|
+
* Null-prototype construction already defuses them, but they are
|
|
22
|
+
* rejected at both seams (`parsePushBody`, `setPresence`) so a hostile
|
|
23
|
+
* id never propagates into userland maps either.
|
|
24
|
+
*/
|
|
25
|
+
const RESERVED_PARTICIPANT_IDS = /* @__PURE__ */ new Set([
|
|
26
|
+
"__proto__",
|
|
27
|
+
"constructor",
|
|
28
|
+
"prototype"
|
|
29
|
+
]);
|
|
30
|
+
const serverInternals = /* @__PURE__ */ new WeakMap();
|
|
31
|
+
/** Scheduler configuration captured only for servers made by `createServer`. */
|
|
32
|
+
const serverSchedulerBindings = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
const objectLike = (value) => typeof value === "object" && value !== null || typeof value === "function";
|
|
34
|
+
const TOOL_SCOPE_STORAGE = Symbol.for("experimental-a2.ai.tool-scope");
|
|
35
|
+
function ambientToolScopeStorage() {
|
|
36
|
+
const value = Reflect.get(globalThis, TOOL_SCOPE_STORAGE);
|
|
37
|
+
if (!objectLike(value) || typeof Reflect.get(value, "getStore") !== "function" || typeof Reflect.get(value, "run") !== "function") return;
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function installAmbientToolScopeStorage(create) {
|
|
41
|
+
const existing = ambientToolScopeStorage();
|
|
42
|
+
if (existing !== void 0) return existing;
|
|
43
|
+
const storage = create();
|
|
44
|
+
Reflect.set(globalThis, TOOL_SCOPE_STORAGE, storage);
|
|
45
|
+
return storage;
|
|
46
|
+
}
|
|
47
|
+
const SCHEDULER_SEND_FAILURE = Symbol.for("experimental-a2.scheduler.send-failure");
|
|
48
|
+
const schedulerSendFailureMarker = (value) => {
|
|
49
|
+
if (!objectLike(value)) return void 0;
|
|
50
|
+
const marker = Reflect.get(value, SCHEDULER_SEND_FAILURE);
|
|
51
|
+
if (!objectLike(marker) || Reflect.get(marker, "version") !== 1 || !Array.isArray(Reflect.get(marker, "pending")) || !Array.isArray(Reflect.get(marker, "active"))) return;
|
|
52
|
+
return marker;
|
|
53
|
+
};
|
|
54
|
+
const schedulerFailureObject = (error) => {
|
|
55
|
+
if (objectLike(error) && (schedulerSendFailureMarker(error) !== void 0 || Object.isExtensible(error))) return error;
|
|
56
|
+
return new Error(String(error), { cause: error });
|
|
57
|
+
};
|
|
58
|
+
const schedulerSendFailureState = (error) => {
|
|
59
|
+
const existing = schedulerSendFailureMarker(error);
|
|
60
|
+
if (existing !== void 0) return existing;
|
|
61
|
+
const marker = {
|
|
62
|
+
version: 1,
|
|
63
|
+
pending: [],
|
|
64
|
+
active: []
|
|
65
|
+
};
|
|
66
|
+
Object.defineProperty(error, SCHEDULER_SEND_FAILURE, {
|
|
67
|
+
value: marker,
|
|
68
|
+
enumerable: false,
|
|
69
|
+
configurable: true,
|
|
70
|
+
writable: false
|
|
71
|
+
});
|
|
72
|
+
return marker;
|
|
73
|
+
};
|
|
74
|
+
const clearSchedulerSendFailureState = (error, marker) => {
|
|
75
|
+
if (marker.pending.length === 0 && marker.active.length === 0) Reflect.deleteProperty(error, SCHEDULER_SEND_FAILURE);
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Classify an adapter error before throwing it. Unclassified adapter failures
|
|
79
|
+
* are retryable because the caller cannot know whether a remote send landed.
|
|
80
|
+
*/
|
|
81
|
+
function classifySchedulerSendFailure(error, disposition) {
|
|
82
|
+
const classified = schedulerFailureObject(error);
|
|
83
|
+
schedulerSendFailureState(classified).pending.push(disposition);
|
|
84
|
+
return classified;
|
|
85
|
+
}
|
|
86
|
+
/** Consume a pending adapter classification and count this rejected send. */
|
|
87
|
+
function markSchedulerSendFailure(error) {
|
|
88
|
+
const marked = schedulerFailureObject(error);
|
|
89
|
+
const marker = schedulerSendFailureState(marked);
|
|
90
|
+
marker.active.push(marker.pending.pop() ?? "retryable");
|
|
91
|
+
return marked;
|
|
92
|
+
}
|
|
93
|
+
function consumeSchedulerSendFailure(error) {
|
|
94
|
+
if (!objectLike(error)) return void 0;
|
|
95
|
+
const marker = schedulerSendFailureMarker(error);
|
|
96
|
+
const disposition = marker?.active.pop();
|
|
97
|
+
if (marker !== void 0) clearSchedulerSendFailureState(error, marker);
|
|
98
|
+
return disposition;
|
|
99
|
+
}
|
|
100
|
+
const isDrainableServer = (value) => {
|
|
101
|
+
if (value === null || typeof value !== "object") return false;
|
|
102
|
+
const contract = Reflect.get(value, "contract");
|
|
103
|
+
return contract !== null && typeof contract === "object" && typeof Reflect.get(contract, "name") === "string" && typeof Reflect.get(value, "drain") === "function";
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Validate the server list captured by a scheduler route at construction time.
|
|
107
|
+
* Built-in adapters keep their low-level target seam structural for custom
|
|
108
|
+
* integrations. The application-facing `schedulerHandler` uses strict mode
|
|
109
|
+
* so only servers made by this A2 copy reach a configured adapter.
|
|
110
|
+
*/
|
|
111
|
+
function assertSchedulerTargets(servers, options) {
|
|
112
|
+
const contracts = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const target of servers) {
|
|
114
|
+
if (!isDrainableServer(target)) throw new TypeError("a2 scheduler: expected a server from createServer()");
|
|
115
|
+
const name = target.contract.name;
|
|
116
|
+
if (contracts.has(name)) throw new TypeError(`a2 scheduler: duplicate server contract '${name}' in one handler`);
|
|
117
|
+
contracts.add(name);
|
|
118
|
+
if (!serverSchedulerBindings.get(target)) {
|
|
119
|
+
if (!options.allowStructural) throw new TypeError(`a2 scheduler: server for contract '${name}' was not created by createServer()`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Require every real server to share one exact configured scheduler. */
|
|
125
|
+
function assertServerSchedulerBindings(scheduler, servers) {
|
|
126
|
+
for (const server of servers) {
|
|
127
|
+
const binding = serverSchedulerBindings.get(server);
|
|
128
|
+
if (!binding?.scheduler) throw new TypeError(`a2 scheduler: server for contract '${server.contract.name}' has no scheduler configured`);
|
|
129
|
+
if (binding.scheduler !== scheduler) throw new TypeError(`a2 scheduler: server for contract '${server.contract.name}' is configured with a different scheduler instance`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Resolve the scheduler privately bound to a real A2 server. */
|
|
133
|
+
function schedulerForServer(server) {
|
|
134
|
+
if (!isDrainableServer(server)) throw new TypeError("a2 scheduler: expected a server from createServer()");
|
|
135
|
+
const binding = serverSchedulerBindings.get(server);
|
|
136
|
+
if (!binding) throw new TypeError(`a2 scheduler: server for contract '${server.contract.name}' was not created by createServer()`);
|
|
137
|
+
if (!binding.scheduler) throw new TypeError(`a2 scheduler: server for contract '${server.contract.name}' has no scheduler configured`);
|
|
138
|
+
return binding.scheduler;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Drain timing knobs. Mutable only as a white-box test seam. Claim
|
|
142
|
+
* heartbeats run on real timers, so tests shrink these to keep suites
|
|
143
|
+
* fast. Production always uses the defaults.
|
|
144
|
+
*/
|
|
145
|
+
const DRAIN_TIMINGS = {
|
|
146
|
+
claimTtlMs: 5e3,
|
|
147
|
+
claimHeartbeatMs: 2e3,
|
|
148
|
+
renewRetryMs: 400,
|
|
149
|
+
watchdogGraceMs: 1e3,
|
|
150
|
+
schedulerArmTimeoutMs: 2e3
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Stream liveness knobs — the server-side SSE heartbeat and the
|
|
154
|
+
* client-side stall watchdog. Paired by design: the client declares a
|
|
155
|
+
* connection dead after roughly two missed heartbeats. Mutable only as
|
|
156
|
+
* a white-box test seam; production always uses the defaults.
|
|
157
|
+
*/
|
|
158
|
+
/**
|
|
159
|
+
* Poll cadence for the poll-based store streams (sqlite, postgres) —
|
|
160
|
+
* owned, not configurable: adaptive polling removed the operator's
|
|
161
|
+
* reason to tune it (the floor keeps active streams smooth; the
|
|
162
|
+
* ceiling bounds idle cost). Mutable only as a white-box test seam so
|
|
163
|
+
* suites don't wait out real idle gaps.
|
|
164
|
+
*/
|
|
165
|
+
const POLL_TIMINGS = {
|
|
166
|
+
activeFloorMs: 25,
|
|
167
|
+
idleCeilingMs: 250
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Safety-read cadence for the notify-driven redis live stream. A
|
|
171
|
+
* parked feed re-reads the store at this interval even without a
|
|
172
|
+
* pub/sub wake-up, so a lost notification (subscriber reconnect gap,
|
|
173
|
+
* dropped PUBLISH) costs latency, never an event. Mutable only as a
|
|
174
|
+
* white-box test seam.
|
|
175
|
+
*/
|
|
176
|
+
const NOTIFY_TIMINGS = { safetyReadMs: 1e4 };
|
|
177
|
+
const STREAM_TIMINGS = {
|
|
178
|
+
sseHeartbeatMs: 15e3,
|
|
179
|
+
sseDeadlineGraceMs: 1e3,
|
|
180
|
+
stallTimeoutMs: 35e3,
|
|
181
|
+
reconnectBaseMs: 500,
|
|
182
|
+
reconnectMaxMs: 5e3
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* WebSocket down-pump backpressure — `sessionSocket` parks its pull
|
|
186
|
+
* loop while the socket reports more buffered bytes than the
|
|
187
|
+
* high-water mark and re-checks on the poll cadence, so a slow reader
|
|
188
|
+
* applies backpressure to the store subscription instead of buffering
|
|
189
|
+
* without bound in the socket. Acks and heartbeats stay exempt: they
|
|
190
|
+
* are small and correctness-relevant. Mutable only as a white-box test
|
|
191
|
+
* seam.
|
|
192
|
+
*/
|
|
193
|
+
const SOCKET_TIMINGS = {
|
|
194
|
+
highWaterMarkBytes: 1048576,
|
|
195
|
+
resumePollMs: 25,
|
|
196
|
+
maxInFlightMessages: 32,
|
|
197
|
+
disconnectWaterMarkBytes: 8388608
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Client presence send cadence — a leading send immediately, then at
|
|
201
|
+
* most one merged send per interval (~15Hz). Owned, not configurable
|
|
202
|
+
* (decision #36's rule): coupling the cadence to the POST ack tied
|
|
203
|
+
* remote cursor smoothness to RTT, and the ack serves no correctness
|
|
204
|
+
* purpose on a fire-and-forget LWW plane. Mutable only as a white-box
|
|
205
|
+
* test seam.
|
|
206
|
+
*/
|
|
207
|
+
const PRESENCE_TIMINGS = { sendIntervalMs: 66 };
|
|
208
|
+
//#endregion
|
|
209
|
+
export { schedulerForServer as _, PRESENCE_TIMINGS as a, STREAM_TIMINGS as c, assertServerSchedulerBindings as d, classifySchedulerSendFailure as f, nullProtoRecord as g, markSchedulerSendFailure as h, POLL_TIMINGS as i, ambientToolScopeStorage as l, installAmbientToolScopeStorage as m, MAX_DATE_MS as n, RESERVED_PARTICIPANT_IDS as o, consumeSchedulerSendFailure as p, NOTIFY_TIMINGS as r, SOCKET_TIMINGS as s, DRAIN_TIMINGS as t, assertSchedulerTargets as u, serverInternals as v, serverSchedulerBindings as y };
|
package/dist/otel.d.ts
CHANGED