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.
Files changed (92) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/dist/ai-B4YhEnfw.d.ts +333 -0
  3. package/dist/ai-server.d.ts +48 -9
  4. package/dist/ai-server.js +121 -49
  5. package/dist/ai.d.ts +2 -303
  6. package/dist/ai.js +231 -86
  7. package/dist/cli-B3VuxoDe.js +597 -0
  8. package/dist/cli-bin.d.ts +1 -0
  9. package/dist/cli-bin.js +5 -0
  10. package/dist/cli.d.ts +19 -0
  11. package/dist/cli.js +2 -0
  12. package/dist/client-BrfDXQ8A.d.ts +155 -0
  13. package/dist/client-Bt4tAKi9.js +798 -0
  14. package/dist/client.d.ts +2 -90
  15. package/dist/client.js +1 -409
  16. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
  17. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
  18. package/dist/devtools-J_jZ2vQf.d.ts +151 -0
  19. package/dist/devtools-kJJaORn-.js +338 -0
  20. package/dist/devtools-server.browser.js +1 -1
  21. package/dist/devtools-server.d.ts +2 -2
  22. package/dist/devtools-server.js +224 -43
  23. package/dist/devtools.d.ts +2 -0
  24. package/dist/devtools.js +2 -0
  25. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
  26. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
  27. package/dist/http.d.ts +71 -13
  28. package/dist/http.js +302 -41
  29. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +2 -2
  32. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
  33. package/dist/internal-aEotMzu_.js +209 -0
  34. package/dist/otel.d.ts +1 -1
  35. package/dist/platform-B4TnJtWu.js +32 -0
  36. package/dist/react.d.ts +37 -14
  37. package/dist/react.js +26 -15
  38. package/dist/scheduler-qstash.d.ts +78 -0
  39. package/dist/scheduler-qstash.js +499 -0
  40. package/dist/scheduler-task-BpzhPnRS.js +54 -0
  41. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
  42. package/dist/scheduler-vercel.js +226 -0
  43. package/dist/server-CcNnFnoW.js +1405 -0
  44. package/dist/server-YtPq7hjw.d.ts +260 -0
  45. package/dist/server.d.ts +4 -155
  46. package/dist/server.js +2 -2
  47. package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
  48. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
  49. package/dist/store-codec-DTG0Ftek.js +8 -0
  50. package/dist/store-memory.d.ts +11 -0
  51. package/dist/{log-memory.js → store-memory.js} +127 -24
  52. package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
  53. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
  54. package/dist/{log-postgres.js → store-postgres.js} +158 -24
  55. package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
  56. package/dist/store-redis-http.d.ts +21 -0
  57. package/dist/store-redis-http.js +70 -0
  58. package/dist/store-redis.d.ts +37 -0
  59. package/dist/store-redis.js +298 -0
  60. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
  61. package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
  62. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
  63. package/dist/testing.browser.d.ts +1 -0
  64. package/dist/testing.browser.js +4 -0
  65. package/dist/testing.d.ts +31 -0
  66. package/dist/testing.js +101 -0
  67. package/dist/wire-DCUZBUlT.js +222 -0
  68. package/docs/01-quickstart.mdx +4 -5
  69. package/docs/concepts/01-contracts.mdx +21 -17
  70. package/docs/concepts/02-handlers.mdx +7 -7
  71. package/docs/concepts/03-durability.mdx +26 -29
  72. package/docs/concepts/04-state.mdx +18 -21
  73. package/docs/guides/01-timers.mdx +154 -54
  74. package/docs/guides/02-cancellation.mdx +30 -4
  75. package/docs/guides/03-react.mdx +20 -21
  76. package/docs/guides/04-local-first.mdx +1 -1
  77. package/docs/guides/05-production.mdx +321 -60
  78. package/docs/guides/06-ai-agents.mdx +249 -49
  79. package/docs/guides/07-devtools.mdx +137 -12
  80. package/docs/guides/08-application-data.mdx +1 -1
  81. package/docs/guides/09-presence.mdx +284 -0
  82. package/docs/guides/10-transports.mdx +131 -0
  83. package/docs/index.mdx +22 -46
  84. package/docs/reference/01-api.mdx +751 -102
  85. package/docs/reference/02-errors.mdx +10 -5
  86. package/package.json +23 -6
  87. package/dist/internal-D6wNxTck.js +0 -36
  88. package/dist/log-memory.d.ts +0 -11
  89. package/dist/log-redis.d.ts +0 -31
  90. package/dist/recovery-vercel.js +0 -119
  91. package/dist/server-DJgD2YWP.js +0 -877
  92. package/dist/wire-BVsgR8o9.js +0 -62
@@ -1,5 +1,7 @@
1
- import { n as serverInspection, t as DEVTOOLS_TIMINGS } from "./inspection-E7qbD0Xj.js";
1
+ import { n as InspectionUnsupportedError, r as serverInspection, t as DEVTOOLS_TIMINGS } from "./inspection-DaxB5jM2.js";
2
+ import { c as encodeDevtoolsCapture, n as DEVTOOLS_CAPTURE_MEDIA_TYPE, o as DEVTOOLS_SESSION_PAGE_MAX_LIMIT, s as devtoolsSessionRevision, t as DEVTOOLS_CAPABILITIES } from "./devtools-kJJaORn-.js";
2
3
  //#region src/devtools-app.ts
4
+ /** Private browser application served by `experimental-a2/devtools/server`. */
3
5
  const escapeText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
4
6
  const escapeAttribute = (value) => escapeText(value).replaceAll("\"", "&quot;");
5
7
  const formatDuration = (milliseconds) => {
@@ -322,6 +324,91 @@ const request = async (resource, params = {}) => {
322
324
  return response.json()
323
325
  }
324
326
 
327
+ const detailRevision = async (detail) => {
328
+ const timestamp = (value) => value === null ? null : new Date(value).getTime()
329
+ const bytes = new TextEncoder().encode(JSON.stringify({
330
+ events: detail.events.map((event) => [
331
+ event.id,
332
+ event.index,
333
+ event.cause?.index ?? null,
334
+ event.cause?.attempt ?? null,
335
+ timestamp(event.processedAt),
336
+ event.processedByAttempt,
337
+ timestamp(event.firstClaimedAt),
338
+ timestamp(event.lastClaimedAt),
339
+ event.attemptCount,
340
+ event.failureCount,
341
+ timestamp(event.lastFailedAt),
342
+ event.lastFailedAttempt,
343
+ event.lastError,
344
+ timestamp(event.failedAt),
345
+ ]),
346
+ snapshots: detail.snapshots.map((snapshot) => [
347
+ snapshot.reducerName,
348
+ snapshot.index,
349
+ snapshot.updatedAt === null ? 0 : new Date(snapshot.updatedAt).getTime(),
350
+ ]),
351
+ }))
352
+ const digest = await crypto.subtle.digest('SHA-256', bytes)
353
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')
354
+ }
355
+
356
+ const requestDetail = async (contract, session) => {
357
+ const events = []
358
+ let snapshots = []
359
+ let cursor = null
360
+ let throughIndex = null
361
+ const seenCursors = new Set()
362
+ do {
363
+ const page = await request('session', {
364
+ contract,
365
+ session,
366
+ limit: ${250},
367
+ cursor,
368
+ })
369
+ if ('revision' in page) return page
370
+ if (page.contract !== contract || page.sessionId !== session) {
371
+ throw new Error('Session page identity changed during inspection.')
372
+ }
373
+ if (throughIndex === null) {
374
+ throughIndex = page.throughIndex
375
+ snapshots = page.snapshots
376
+ } else if (page.throughIndex !== throughIndex || page.snapshots.length > 0) {
377
+ throw new Error('Session page frontier changed during inspection.')
378
+ }
379
+ for (const event of page.events) {
380
+ if (event.index !== events.length + 1 || event.index > throughIndex) {
381
+ throw new Error('Session event pages are not contiguous.')
382
+ }
383
+ events.push(event)
384
+ }
385
+ if (page.cursor !== null) {
386
+ if (
387
+ page.events.length === 0 ||
388
+ events.length === throughIndex ||
389
+ seenCursors.has(page.cursor)
390
+ ) {
391
+ throw new Error('Session cursor did not advance.')
392
+ }
393
+ seenCursors.add(page.cursor)
394
+ }
395
+ cursor = page.cursor
396
+ } while (cursor !== null)
397
+ if (throughIndex !== events.length) {
398
+ throw new Error('Session event pages ended before their frozen frontier.')
399
+ }
400
+ const detail = {
401
+ protocolVersion: ${1},
402
+ contract,
403
+ sessionId: session,
404
+ revision: '',
405
+ events,
406
+ snapshots,
407
+ }
408
+ detail.revision = await detailRevision(detail)
409
+ return detail
410
+ }
411
+
325
412
  const watchUrl = () => {
326
413
  const url = new URL('_a2/watch', document.baseURI)
327
414
  url.searchParams.set('contract', state.contract)
@@ -411,10 +498,7 @@ const loadSessions = async (reset) => {
411
498
 
412
499
  const loadDetail = async () => {
413
500
  if (!state.contract || !state.session) return
414
- state.detail = await request('session', {
415
- contract: state.contract,
416
- session: state.session,
417
- })
501
+ state.detail = await requestDetail(state.contract, state.session)
418
502
  state.error = null
419
503
  render()
420
504
  }
@@ -715,7 +799,7 @@ const renderMain = () => {
715
799
  return main
716
800
  }
717
801
  if (!state.detail) {
718
- main.append(node('div', 'empty', 'Reading the durable log…'))
802
+ main.append(node('div', 'empty', 'Reading the durable store…'))
719
803
  return main
720
804
  }
721
805
 
@@ -825,7 +909,7 @@ if (bootstrap) {
825
909
  */
826
910
  const MARKER = "/_a2/";
827
911
  const encoder = new TextEncoder();
828
- const isProduction = () => typeof process !== "undefined" && process.env?.["NODE_ENV"] === "production";
912
+ const isDevelopment = () => typeof process !== "undefined" && process.env?.["NODE_ENV"] === "development";
829
913
  const headers = (contentType) => ({
830
914
  "content-type": contentType,
831
915
  "cache-control": "no-store",
@@ -846,59 +930,91 @@ const notFound = () => new Response("Not found", {
846
930
  const badRequest = (message) => json({ error: message }, { status: 400 });
847
931
  const unavailable = (message) => json({ error: message }, { status: 501 });
848
932
  const wireSummary = (session) => ({
849
- ...session,
933
+ sessionId: session.sessionId,
934
+ eventCount: session.eventCount,
935
+ pendingCount: session.pendingCount,
936
+ failedCount: session.failedCount,
937
+ attemptCount: session.attemptCount,
938
+ failureCount: session.failureCount,
850
939
  firstEventAt: session.firstEventAt.toISOString(),
851
940
  updatedAt: session.updatedAt.toISOString()
852
941
  });
853
942
  const wireEvent = (event) => ({
854
- ...event,
943
+ id: event.id,
944
+ type: event.type,
945
+ payload: event.payload,
946
+ index: event.index,
947
+ sessionId: event.sessionId,
855
948
  createdAt: event.createdAt.toISOString(),
949
+ cause: event.cause === null ? null : {
950
+ index: event.cause.index,
951
+ attempt: event.cause.attempt,
952
+ ...event.cause.batchSize === void 0 ? {} : { batchSize: event.cause.batchSize }
953
+ },
954
+ lane: event.lane,
856
955
  processedAt: event.processedAt?.toISOString() ?? null,
956
+ processedByAttempt: event.processedByAttempt,
957
+ returnedEventIds: event.returnedEventIds,
857
958
  firstClaimedAt: event.firstClaimedAt?.toISOString() ?? null,
858
959
  lastClaimedAt: event.lastClaimedAt?.toISOString() ?? null,
960
+ attemptCount: event.attemptCount,
961
+ claimHolder: event.claimHolder,
962
+ claimExpiresAt: event.claimExpiresAt?.toISOString() ?? null,
963
+ failureCount: event.failureCount,
859
964
  lastFailedAt: event.lastFailedAt?.toISOString() ?? null,
965
+ lastFailedAttempt: event.lastFailedAttempt,
966
+ lastError: event.lastError,
860
967
  failedAt: event.failedAt?.toISOString() ?? null
861
968
  });
862
969
  const wireSnapshot = (snapshot) => ({
863
- ...snapshot,
970
+ reducerName: snapshot.reducerName,
971
+ index: snapshot.index,
864
972
  updatedAt: snapshot.updatedAt.getTime() === 0 ? null : snapshot.updatedAt.toISOString()
865
973
  });
866
974
  const sessionRevision = async (detail) => {
867
- const serialized = JSON.stringify({
868
- events: detail.events.map((event) => [
869
- event.id,
870
- event.index,
871
- event.cause?.index ?? null,
872
- event.cause?.attempt ?? null,
873
- event.processedAt?.getTime() ?? null,
874
- event.processedByAttempt,
875
- event.firstClaimedAt?.getTime() ?? null,
876
- event.lastClaimedAt?.getTime() ?? null,
877
- event.attemptCount,
878
- event.failureCount,
879
- event.lastFailedAt?.getTime() ?? null,
880
- event.lastFailedAttempt,
881
- event.lastError,
882
- event.failedAt?.getTime() ?? null
883
- ]),
884
- snapshots: detail.snapshots.map((snapshot) => [
885
- snapshot.reducerName,
886
- snapshot.index,
887
- snapshot.updatedAt.getTime()
888
- ])
975
+ return devtoolsSessionRevision({
976
+ events: detail.events.map(wireEvent),
977
+ snapshots: detail.snapshots.map(wireSnapshot)
889
978
  });
890
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(serialized));
891
- return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
892
979
  };
893
980
  const wireDetail = async (contract, sessionId, detail) => ({
981
+ protocolVersion: 1,
894
982
  contract,
895
983
  sessionId,
896
984
  revision: await sessionRevision(detail),
897
985
  events: detail.events.map(wireEvent),
898
986
  snapshots: detail.snapshots.map(wireSnapshot)
899
987
  });
988
+ const readCompleteSession = async (inspection, sessionId) => {
989
+ const events = [];
990
+ let snapshots = [];
991
+ let afterIndex = 0;
992
+ let throughIndex;
993
+ for (;;) {
994
+ const page = await inspection.readSessionPage(sessionId, {
995
+ afterIndex,
996
+ limit: 250,
997
+ ...throughIndex === void 0 ? {} : { throughIndex }
998
+ });
999
+ if (throughIndex === void 0) {
1000
+ throughIndex = page.throughIndex;
1001
+ snapshots = page.snapshots;
1002
+ } else if (page.throughIndex !== throughIndex) throw new Error("inspection session frontier changed between pages");
1003
+ if (events.length + page.events.length > page.throughIndex) throw new Error("inspection session page crossed its frozen frontier");
1004
+ events.push(...page.events);
1005
+ if (page.nextIndex === null) break;
1006
+ if (events.length === page.throughIndex) throw new Error("inspection session cursor continued past its frontier");
1007
+ if (page.nextIndex <= afterIndex) throw new Error("inspection session cursor did not advance");
1008
+ afterIndex = page.nextIndex;
1009
+ }
1010
+ if (throughIndex !== events.length) throw new Error("inspection session ended before its frozen frontier");
1011
+ return {
1012
+ events,
1013
+ snapshots
1014
+ };
1015
+ };
900
1016
  const readRevision = async (inspection, sessionId) => {
901
- const detail = await inspection.readSession(sessionId);
1017
+ const detail = await readCompleteSession(inspection, sessionId);
902
1018
  return {
903
1019
  detail,
904
1020
  revision: await sessionRevision(detail)
@@ -930,7 +1046,7 @@ const bootstrapState = async (url, servers) => {
930
1046
  bootstrap.cursor = page.cursor;
931
1047
  bootstrap.session = (requestedContract === contract ? url.searchParams.get("session") : null) ?? page.sessions[0]?.sessionId ?? null;
932
1048
  if (!bootstrap.session) return bootstrap;
933
- const detail = await inspection.readSession(bootstrap.session);
1049
+ const detail = await readCompleteSession(inspection, bootstrap.session);
934
1050
  if (detail.events.length === 0) {
935
1051
  bootstrap.error = `session '${bootstrap.session}' was not found`;
936
1052
  return bootstrap;
@@ -951,6 +1067,30 @@ const inspectionOf = (servers, contractName) => {
951
1067
  if (!server) return notFound();
952
1068
  return serverInspection.get(server) ?? unavailable(`contract '${contractName}' is not an inspectable A2 server`);
953
1069
  };
1070
+ const encodeSessionCursor = (contract, sessionId, cursor) => JSON.stringify([
1071
+ 1,
1072
+ contract,
1073
+ sessionId,
1074
+ cursor.throughIndex,
1075
+ cursor.afterIndex
1076
+ ]);
1077
+ const parseSessionCursor = (value, contract, sessionId) => {
1078
+ let decoded;
1079
+ try {
1080
+ decoded = JSON.parse(value);
1081
+ } catch {
1082
+ return badRequest("cursor is invalid");
1083
+ }
1084
+ if (!Array.isArray(decoded) || decoded.length !== 5 || decoded[0] !== 1 || !Number.isSafeInteger(decoded[3]) || !Number.isSafeInteger(decoded[4]) || decoded[3] < 1 || decoded[4] < 1 || decoded[4] >= decoded[3]) return badRequest("cursor is invalid");
1085
+ if (decoded[1] !== contract || decoded[2] !== sessionId) return badRequest("cursor does not belong to this contract and session");
1086
+ return {
1087
+ throughIndex: decoded[3],
1088
+ afterIndex: decoded[4]
1089
+ };
1090
+ };
1091
+ const captureFilename = (contract, sessionId) => {
1092
+ return `${`${contract}-${sessionId}`.replaceAll(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "a2-session"}.a2log`;
1093
+ };
954
1094
  const watchResponse = (request, inspection, sessionId, initial) => {
955
1095
  let stopped = false;
956
1096
  let timeout = null;
@@ -1014,10 +1154,10 @@ const watchResponse = (request, inspection, sessionId, initial) => {
1014
1154
  } });
1015
1155
  };
1016
1156
  const authorizeRequest = async (request, authorize) => {
1017
- if (!authorize) return isProduction() ? notFound() : null;
1157
+ if (!authorize) return isDevelopment() ? null : notFound();
1018
1158
  const result = await authorize(request);
1019
1159
  if (result instanceof Response) return result;
1020
- return result ? null : notFound();
1160
+ return result === true ? null : notFound();
1021
1161
  };
1022
1162
  function createDevtools(options) {
1023
1163
  if (!options || !Array.isArray(options.servers) || options.servers.length === 0) throw new TypeError("createDevtools expects at least one A2 server");
@@ -1051,8 +1191,13 @@ function createDevtools(options) {
1051
1191
  }
1052
1192
  if (resource === "app.js") return new Response(DEVTOOLS_APP_JS, { headers: headers("text/javascript; charset=utf-8") });
1053
1193
  if (resource === "app.css") return new Response(DEVTOOLS_APP_CSS, { headers: headers("text/css; charset=utf-8") });
1054
- if (resource === "contracts") return json({ contracts: [...servers.keys()].map((name) => ({ name })) });
1055
- const inspection = inspectionOf(servers, url.searchParams.get("contract"));
1194
+ if (resource === "contracts") return json({
1195
+ protocolVersion: 1,
1196
+ contracts: [...servers.keys()].map((name) => ({ name }))
1197
+ });
1198
+ if (resource === "capabilities") return json(DEVTOOLS_CAPABILITIES);
1199
+ const contractName = url.searchParams.get("contract");
1200
+ const inspection = inspectionOf(servers, contractName);
1056
1201
  if (inspection instanceof Response) return inspection;
1057
1202
  try {
1058
1203
  if (resource === "sessions") {
@@ -1065,18 +1210,54 @@ function createDevtools(options) {
1065
1210
  ...cursor !== null ? { cursor } : {}
1066
1211
  });
1067
1212
  return json({
1213
+ protocolVersion: 1,
1068
1214
  cursor: page.cursor,
1069
1215
  sessions: page.sessions.map(wireSummary)
1070
1216
  });
1071
1217
  }
1072
1218
  const sessionId = url.searchParams.get("session");
1073
1219
  if (!sessionId) return badRequest("session is required");
1074
- const detail = await inspection.readSession(sessionId);
1220
+ if (resource === "session" && (url.searchParams.has("limit") || url.searchParams.has("cursor"))) {
1221
+ const requestedLimit = Number(url.searchParams.get("limit") ?? 250);
1222
+ if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 1) return badRequest("limit must be a positive integer");
1223
+ const limit = Math.min(requestedLimit, DEVTOOLS_SESSION_PAGE_MAX_LIMIT);
1224
+ const cursorValue = url.searchParams.get("cursor");
1225
+ const cursor = cursorValue === null ? {
1226
+ afterIndex: 0,
1227
+ throughIndex: void 0
1228
+ } : parseSessionCursor(cursorValue, contractName, sessionId);
1229
+ if (cursor instanceof Response) return cursor;
1230
+ const page = await inspection.readSessionPage(sessionId, {
1231
+ afterIndex: cursor.afterIndex,
1232
+ limit,
1233
+ ...cursor.throughIndex === void 0 ? {} : { throughIndex: cursor.throughIndex }
1234
+ });
1235
+ if (page.throughIndex === 0) return notFound();
1236
+ const response = {
1237
+ protocolVersion: 1,
1238
+ contract: contractName,
1239
+ sessionId,
1240
+ throughIndex: page.throughIndex,
1241
+ cursor: page.nextIndex === null ? null : encodeSessionCursor(contractName, sessionId, {
1242
+ throughIndex: page.throughIndex,
1243
+ afterIndex: page.nextIndex
1244
+ }),
1245
+ events: page.events.map(wireEvent),
1246
+ snapshots: page.snapshots.map(wireSnapshot)
1247
+ };
1248
+ return json(response);
1249
+ }
1250
+ const detail = await readCompleteSession(inspection, sessionId);
1075
1251
  if (detail.events.length === 0) return notFound();
1076
- if (resource === "session") return json(await wireDetail(url.searchParams.get("contract"), sessionId, detail));
1252
+ const wired = await wireDetail(contractName, sessionId, detail);
1253
+ if (resource === "session") return json(wired);
1254
+ if (resource === "export") return new Response(await encodeDevtoolsCapture(wired), { headers: {
1255
+ ...headers(`${DEVTOOLS_CAPTURE_MEDIA_TYPE}; charset=utf-8`),
1256
+ "content-disposition": `attachment; filename="${captureFilename(wired.contract, sessionId)}"`
1257
+ } });
1077
1258
  if (resource === "watch") return watchResponse(request, inspection, sessionId, detail);
1078
1259
  } catch (error) {
1079
- if (error instanceof TypeError && error.message.includes("does not support inspection")) return unavailable(error.message);
1260
+ if (error instanceof InspectionUnsupportedError) return unavailable(error.message);
1080
1261
  throw error;
1081
1262
  }
1082
1263
  return notFound();
@@ -0,0 +1,2 @@
1
+ import { C as parseDevtoolsCapture, S as encodeDevtoolsCapture, _ as DevtoolsSessionsResponse, a as DEVTOOLS_SESSION_PAGE_LIMIT, b as DevtoolsWireSnapshot, c as DevtoolsCapture, d as DevtoolsCaptureManifest, f as DevtoolsCaptureRecord, g as DevtoolsSessionPage, h as DevtoolsSessionDetail, i as DEVTOOLS_PROTOCOL_VERSION, l as DevtoolsCaptureEvent, m as DevtoolsContractsResponse, n as DEVTOOLS_CAPTURE_MEDIA_TYPE, o as DEVTOOLS_SESSION_PAGE_MAX_LIMIT, p as DevtoolsCaptureSnapshot, r as DEVTOOLS_CAPTURE_VERSION, s as DevtoolsCapabilities, t as DEVTOOLS_CAPABILITIES, u as DevtoolsCaptureFooter, v as DevtoolsWireEvent, w as verifyDevtoolsCapture, x as devtoolsSessionRevision, y as DevtoolsWireSessionSummary } from "./devtools-J_jZ2vQf.js";
2
+ export { DEVTOOLS_CAPABILITIES, DEVTOOLS_CAPTURE_MEDIA_TYPE, DEVTOOLS_CAPTURE_VERSION, DEVTOOLS_PROTOCOL_VERSION, DEVTOOLS_SESSION_PAGE_LIMIT, DEVTOOLS_SESSION_PAGE_MAX_LIMIT, DevtoolsCapabilities, DevtoolsCapture, DevtoolsCaptureEvent, DevtoolsCaptureFooter, DevtoolsCaptureManifest, DevtoolsCaptureRecord, DevtoolsCaptureSnapshot, DevtoolsContractsResponse, DevtoolsSessionDetail, DevtoolsSessionPage, DevtoolsSessionsResponse, DevtoolsWireEvent, DevtoolsWireSessionSummary, DevtoolsWireSnapshot, devtoolsSessionRevision, encodeDevtoolsCapture, parseDevtoolsCapture, verifyDevtoolsCapture };
@@ -0,0 +1,2 @@
1
+ import { a as DEVTOOLS_SESSION_PAGE_LIMIT, c as encodeDevtoolsCapture, i as DEVTOOLS_PROTOCOL_VERSION, l as parseDevtoolsCapture, n as DEVTOOLS_CAPTURE_MEDIA_TYPE, o as DEVTOOLS_SESSION_PAGE_MAX_LIMIT, r as DEVTOOLS_CAPTURE_VERSION, s as devtoolsSessionRevision, t as DEVTOOLS_CAPABILITIES, u as verifyDevtoolsCapture } from "./devtools-kJJaORn-.js";
2
+ export { DEVTOOLS_CAPABILITIES, DEVTOOLS_CAPTURE_MEDIA_TYPE, DEVTOOLS_CAPTURE_VERSION, DEVTOOLS_PROTOCOL_VERSION, DEVTOOLS_SESSION_PAGE_LIMIT, DEVTOOLS_SESSION_PAGE_MAX_LIMIT, devtoolsSessionRevision, encodeDevtoolsCapture, parseDevtoolsCapture, verifyDevtoolsCapture };
@@ -11,13 +11,13 @@ var A2Error = class extends Error {
11
11
  }
12
12
  };
13
13
  /**
14
- * Wrap an arbitrary thrown value as LOG_UNAVAILABLE, preserving the
14
+ * Wrap an arbitrary thrown value as STORE_UNAVAILABLE, preserving the
15
15
  * original as `cause`. A2Errors pass through untouched — they already
16
16
  * carry their own meaning.
17
17
  */
18
- function asLogUnavailable(err) {
18
+ function asStoreUnavailable(err) {
19
19
  if (err instanceof A2Error) return err;
20
- return new A2Error("LOG_UNAVAILABLE", "the log backend failed", { cause: err });
20
+ return new A2Error("STORE_UNAVAILABLE", "the store backend failed", { cause: err });
21
21
  }
22
22
  //#endregion
23
- export { asLogUnavailable as n, A2Error as t };
23
+ export { asStoreUnavailable as n, A2Error as t };
@@ -6,7 +6,7 @@
6
6
  * A single class serializes cleanly across the push-route boundary and
7
7
  * keeps `instanceof` checks working after bundling.
8
8
  */
9
- type A2ErrorCode = "INVALID_PAYLOAD" | "UNKNOWN_EVENT_TYPE" | "PARTIAL_DUPLICATE_BATCH" | "LOG_UNAVAILABLE" | "LOG_NOT_CONFIGURED";
9
+ type A2ErrorCode = "INVALID_PAYLOAD" | "UNKNOWN_EVENT_TYPE" | "PARTIAL_DUPLICATE_BATCH" | "SUPERSEDED_ATTEMPT" | "CLAIM_EXPIRED" | "STORE_UNAVAILABLE" | "STORE_NOT_CONFIGURED" | "UNKNOWN_PRESENCE_FIELD" | "PRESENCE_NOT_SUPPORTED";
10
10
  declare class A2Error extends Error {
11
11
  readonly code: A2ErrorCode;
12
12
  /** e.g. the Standard Schema issues for INVALID_PAYLOAD */
package/dist/http.d.ts CHANGED
@@ -1,36 +1,55 @@
1
- import { a as Event } from "./log-ldf5g8Cx.js";
2
- import { PushedEvent } from "./server.js";
3
- import { t as A2Error } from "./errors-xL_JTXsY.js";
1
+ import { a as EventDefs, c as PresencePatch, l as PresenceSnapshot } from "./contract-jIfaR085.js";
2
+ import { a as Event } from "./store-C3sNAaBT.js";
3
+ import { _ as Session, f as PushedEvent, i as DrainableServer, p as PushedPresence } from "./server-YtPq7hjw.js";
4
+ import { t as A2Error } from "./errors-W6nwJ-fm.js";
4
5
  //#region src/http.d.ts
6
+ /**
7
+ * Create one scheduler callback route for servers sharing the same configured
8
+ * scheduler. Configuration mistakes throw immediately while the route module
9
+ * is evaluated, before a delivery can be acknowledged by the wrong adapter.
10
+ */
11
+ declare function schedulerHandler(firstServer: DrainableServer, ...servers: DrainableServer[]): (req: Request) => Promise<Response>;
5
12
  /** The push envelope: what a client POSTs to append events. */
6
13
  type PushBody = {
7
14
  sessionId: string;
8
15
  /** Branded: `session.append` accepts these directly (the push route
9
- * path); schema validation still happens inside `append`. */
16
+ * path); schema validation still happens inside `append`. Empty only
17
+ * for a presence-only push. */
10
18
  events: PushedEvent[];
19
+ /** Branded like `events`: `session.setPresence` accepts it whole;
20
+ * field validation still happens inside `setPresence`. */
21
+ presence?: PushedPresence;
11
22
  };
12
23
  /**
13
- * Validate the push envelope — `{ sessionId, events }` — throwing
14
- * `INVALID_PAYLOAD` on a malformed body. Payload validation against the
15
- * machine's schemas happens in `append`, not here.
24
+ * Validate the push envelope — `{ sessionId, events, presence? }` —
25
+ * throwing `INVALID_PAYLOAD` on a malformed body. Payload validation
26
+ * against the machine's schemas happens in `append` and `setPresence`,
27
+ * not here. Either plane may appear alone: a presence-only push has no
28
+ * (or empty) `events`.
16
29
  */
17
30
  declare function parsePushBody(req: Request): Promise<PushBody>;
18
31
  /**
19
32
  * Pipe a live event iterable into an SSE `Response`. Each event is one
20
- * frame — `id:` carries the log index, `data:` the JSON event. A
33
+ * frame — `id:` carries the event-log index, `data:` the JSON event. A
34
+ * `stream({ presence: true })` iterable also yields presence items,
35
+ * which ride as named frames old clients skip: `event:
36
+ * presence-snapshot` first (the map, ISO dates), then `event: presence`
37
+ * per patch — no `id:`, presence never advances the resume frontier. A
21
38
  * disconnecting client cancels the stream, which closes the underlying
22
39
  * subscription. Two kinds of comment frames ride along: a `: connected`
23
40
  * prelude that flushes headers immediately, and a `: ping` heartbeat
24
41
  * every 15 seconds so clients (and proxies) can tell a quiet stream
25
42
  * from a dead connection — the session client's stall watchdog counts
26
- * on it.
43
+ * on it. When the platform exposes an invocation deadline, the response
44
+ * closes cleanly one second before it so the client reconnects without
45
+ * a platform-timeout failure.
27
46
  */
28
- declare function sseResponse(iterable: AsyncIterable<Event>): Response;
47
+ declare function sseResponse(iterable: AsyncIterable<Event | PresencePatch | PresenceSnapshot>): Response;
29
48
  /**
30
49
  * Serialize an error into the documented wire shape,
31
50
  * `{ error: { code, message, details } }`, with the mapped status
32
- * (400 for caller bugs, 503 for LOG_UNAVAILABLE). Non-A2Errors become a
33
- * 503 LOG_UNAVAILABLE — from the client's perspective an unknown server
51
+ * (400 for caller bugs, 503 for STORE_UNAVAILABLE). Non-A2Errors become a
52
+ * 503 STORE_UNAVAILABLE — from the client's perspective an unknown server
34
53
  * failure is retryable-once, not a protocol contract.
35
54
  */
36
55
  declare function errorResponse(error: unknown): Response;
@@ -40,5 +59,44 @@ declare function errorResponse(error: unknown): Response;
40
59
  * sides branch on identical codes.
41
60
  */
42
61
  declare function deserializeError(body: unknown): A2Error | null;
62
+ /**
63
+ * The structural socket `sessionSocket` speaks against — satisfied by
64
+ * `ws` and by `@vercel/functions`' upgraded socket alike, so the
65
+ * platform upgrade API stays out of the library. `message` data
66
+ * arrives as whatever the platform delivers (string, Buffer,
67
+ * ArrayBuffer, or fragments); `sessionSocket` normalizes defensively.
68
+ */
69
+ type A2Socket = {
70
+ send(data: string): void;
71
+ on(event: "message", listener: (data: unknown) => void): void;
72
+ on(event: "close", listener: () => void): void;
73
+ on(event: "error", listener: (error: unknown) => void): void;
74
+ close(code?: number, reason?: string): void;
75
+ /** Bytes queued but not yet transmitted — when exposed, the down
76
+ * pump uses it for backpressure (`SOCKET_TIMINGS`). */
77
+ bufferedAmount?: number;
78
+ };
79
+ type SessionSocketOptions = {
80
+ /** The resume frontier, from the upgrade URL's query — the same
81
+ * parameter the SSE GET reads. */
82
+ startAfter?: number;
83
+ /** The `stream()` presence opt-in — the server decides, same rule as
84
+ * the SSE route. */
85
+ presence?: boolean;
86
+ /** Epoch ms: close cleanly (code 1000) at this time, ahead of a known
87
+ * platform deadline, so clients reconnect on our schedule. */
88
+ deadline?: number;
89
+ };
90
+ /**
91
+ * Speak the A2 socket protocol (specs/a2-api.md §13) against one
92
+ * session: the live stream pumps down as `kind`-framed JSON with a
93
+ * `ping` heartbeat on the SSE cadence; `push` frames come up through
94
+ * the same validation seams as `parsePushBody` (same provenance brand,
95
+ * same `validatePush` calls, once per plane) and are answered with an
96
+ * ack; `presence` frames are fire-and-forget. Socket close or error
97
+ * tears the pump down and releases the store subscription; the stream
98
+ * ending closes the socket.
99
+ */
100
+ declare function sessionSocket<D extends EventDefs>(session: Session<D>, socket: A2Socket, options?: SessionSocketOptions): void;
43
101
  //#endregion
44
- export { PushBody, deserializeError, errorResponse, parsePushBody, sseResponse };
102
+ export { A2Socket, PushBody, SessionSocketOptions, deserializeError, errorResponse, parsePushBody, schedulerHandler, sessionSocket, sseResponse };