experimental-a2 0.8.0 → 0.9.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 (182) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +29 -0
  4. package/dist/actor-client.d.ts +46 -0
  5. package/dist/actor-client.d.ts.map +1 -0
  6. package/dist/actor-client.js +54 -0
  7. package/dist/actor-client.js.map +1 -0
  8. package/dist/actor-react.d.ts +54 -0
  9. package/dist/actor-react.d.ts.map +1 -0
  10. package/dist/actor-react.js +79 -0
  11. package/dist/actor-react.js.map +1 -0
  12. package/dist/actor-shared-BACubf4x.d.ts +136 -0
  13. package/dist/actor-shared-BACubf4x.d.ts.map +1 -0
  14. package/dist/actor-shared-DI7J5upy.js +127 -0
  15. package/dist/actor-shared-DI7J5upy.js.map +1 -0
  16. package/dist/actor.browser.d.ts +1 -0
  17. package/dist/actor.browser.js +13 -0
  18. package/dist/actor.browser.js.map +1 -0
  19. package/dist/actor.d.ts +176 -0
  20. package/dist/actor.d.ts.map +1 -0
  21. package/dist/actor.js +437 -0
  22. package/dist/actor.js.map +1 -0
  23. package/dist/ai-server.d.ts +2 -2
  24. package/dist/ai-server.js +2 -2
  25. package/dist/ai.d.ts +2 -2
  26. package/dist/client.d.ts +1 -1
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +4 -4
  29. package/dist/client.js.map +1 -1
  30. package/dist/{errors-BQuJpe82.js → errors-DCk6ch5n.js} +16 -2
  31. package/dist/{errors-BQuJpe82.js.map → errors-DCk6ch5n.js.map} +1 -1
  32. package/dist/{idempotent-replay-DuqEkYA7.js → idempotent-replay-DVOlyYbx.js} +2 -2
  33. package/dist/{idempotent-replay-DuqEkYA7.js.map → idempotent-replay-DVOlyYbx.js.map} +1 -1
  34. package/dist/index.d.ts +16 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -2
  37. package/dist/react.d.ts +1 -1
  38. package/dist/{contract-jIfaR085.d.ts → reducer-DJKWm3cp.d.ts} +39 -39
  39. package/dist/reducer-DJKWm3cp.d.ts.map +1 -0
  40. package/dist/scheduler-qstash.d.ts +2 -2
  41. package/dist/scheduler-qstash.js +2 -2
  42. package/dist/scheduler-vercel.d.ts +2 -2
  43. package/dist/scheduler-vercel.js +1 -1
  44. package/dist/{server-B2XNevQA.js → server-CBET-jSz.js} +6 -6
  45. package/dist/server-CBET-jSz.js.map +1 -0
  46. package/dist/{server-DjPhHnbI.d.ts → server-CKY3_lbw.d.ts} +3 -3
  47. package/dist/{server-DjPhHnbI.d.ts.map → server-CKY3_lbw.d.ts.map} +1 -1
  48. package/dist/server.d.ts +3 -3
  49. package/dist/server.js +1 -1
  50. package/dist/{store-RJO35BMj.d.ts → store-DGHeBtIQ.d.ts} +2 -2
  51. package/dist/{store-RJO35BMj.d.ts.map → store-DGHeBtIQ.d.ts.map} +1 -1
  52. package/dist/store-memory.d.ts +1 -1
  53. package/dist/store-memory.js +2 -2
  54. package/dist/store-postgres.d.ts +1 -1
  55. package/dist/store-postgres.js +2 -2
  56. package/dist/{store-redis-core-DT01r4GZ.js → store-redis-core-z-ykbyMg.js} +3 -3
  57. package/dist/{store-redis-core-DT01r4GZ.js.map → store-redis-core-z-ykbyMg.js.map} +1 -1
  58. package/dist/store-redis-http.d.ts +1 -1
  59. package/dist/store-redis-http.js +2 -2
  60. package/dist/store-redis.d.ts +1 -1
  61. package/dist/store-redis.js +2 -2
  62. package/dist/store-sqlite.d.ts +1 -1
  63. package/dist/store-sqlite.js +2 -2
  64. package/dist/{wire-B6te_wns.js → wire--yji6mO3.js} +2 -2
  65. package/dist/{wire-B6te_wns.js.map → wire--yji6mO3.js.map} +1 -1
  66. package/docs/actors/01-introduction.mdx +189 -0
  67. package/docs/actors/02-concurrency.mdx +154 -0
  68. package/docs/actors/03-timers.mdx +120 -0
  69. package/docs/actors/04-routes.mdx +352 -0
  70. package/docs/actors/meta.ts +1 -0
  71. package/docs/concepts/meta.ts +1 -0
  72. package/docs/guides/07-examples.mdx +56 -0
  73. package/docs/guides/meta.ts +1 -0
  74. package/docs/index.mdx +16 -0
  75. package/docs/reference/02-errors.mdx +33 -0
  76. package/docs/reference/meta.ts +1 -0
  77. package/examples/README.md +15 -0
  78. package/examples/playground/AGENTS.md +11 -0
  79. package/examples/playground/DEPLOY.md +106 -0
  80. package/examples/playground/README.md +19 -0
  81. package/examples/playground/activity-feed.test.ts +10 -0
  82. package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
  83. package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
  84. package/examples/playground/app/agent/events/route.ts +4 -0
  85. package/examples/playground/app/agent/model.ts +3 -0
  86. package/examples/playground/app/agent/new-agent-session.tsx +98 -0
  87. package/examples/playground/app/agent/page.tsx +25 -0
  88. package/examples/playground/app/agent/scheduler/route.ts +5 -0
  89. package/examples/playground/app/agent/server.ts +153 -0
  90. package/examples/playground/app/agent/session.ts +13 -0
  91. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
  92. package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
  93. package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
  94. package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
  95. package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
  96. package/examples/playground/app/canvas/events/route.ts +13 -0
  97. package/examples/playground/app/canvas/model.ts +94 -0
  98. package/examples/playground/app/canvas/open-canvas.tsx +40 -0
  99. package/examples/playground/app/canvas/page.tsx +20 -0
  100. package/examples/playground/app/canvas/server.ts +9 -0
  101. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
  102. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
  103. package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
  104. package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
  105. package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
  106. package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
  107. package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
  108. package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
  109. package/examples/playground/app/chat/[chatId]/session.ts +7 -0
  110. package/examples/playground/app/chat/events/route.ts +7 -0
  111. package/examples/playground/app/chat/model.test.ts +155 -0
  112. package/examples/playground/app/chat/model.ts +310 -0
  113. package/examples/playground/app/chat/new-conversation.tsx +16 -0
  114. package/examples/playground/app/chat/page.tsx +25 -0
  115. package/examples/playground/app/chat/scheduler/route.ts +5 -0
  116. package/examples/playground/app/chat/server.ts +184 -0
  117. package/examples/playground/app/components/activity-feed.tsx +54 -0
  118. package/examples/playground/app/components/connection-pill.tsx +29 -0
  119. package/examples/playground/app/counter/counter-client.tsx +72 -0
  120. package/examples/playground/app/counter/events/route.ts +4 -0
  121. package/examples/playground/app/counter/model.test.ts +36 -0
  122. package/examples/playground/app/counter/model.ts +31 -0
  123. package/examples/playground/app/counter/page.tsx +24 -0
  124. package/examples/playground/app/counter/server.ts +9 -0
  125. package/examples/playground/app/counter/session.ts +13 -0
  126. package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
  127. package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
  128. package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
  129. package/examples/playground/app/documents/[documentId]/session.ts +7 -0
  130. package/examples/playground/app/documents/events/route.ts +4 -0
  131. package/examples/playground/app/documents/model.ts +55 -0
  132. package/examples/playground/app/documents/open-document.tsx +40 -0
  133. package/examples/playground/app/documents/page.tsx +22 -0
  134. package/examples/playground/app/documents/server.ts +9 -0
  135. package/examples/playground/app/globals.css +2078 -0
  136. package/examples/playground/app/layout.tsx +44 -0
  137. package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
  138. package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
  139. package/examples/playground/app/orders/[orderId]/session.ts +11 -0
  140. package/examples/playground/app/orders/create/route.ts +30 -0
  141. package/examples/playground/app/orders/events/route.ts +4 -0
  142. package/examples/playground/app/orders/model.ts +79 -0
  143. package/examples/playground/app/orders/new-order-form.tsx +98 -0
  144. package/examples/playground/app/orders/page.tsx +22 -0
  145. package/examples/playground/app/orders/scheduler/route.ts +5 -0
  146. package/examples/playground/app/orders/server.ts +50 -0
  147. package/examples/playground/app/page.tsx +111 -0
  148. package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
  149. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
  150. package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
  151. package/examples/playground/app/recovery/events/route.ts +3 -0
  152. package/examples/playground/app/recovery/model.ts +55 -0
  153. package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
  154. package/examples/playground/app/recovery/page.tsx +22 -0
  155. package/examples/playground/app/recovery/scheduler/route.ts +7 -0
  156. package/examples/playground/app/recovery/server.ts +53 -0
  157. package/examples/playground/app/recovery/start/route.ts +41 -0
  158. package/examples/playground/app/vault/[vaultId]/route.ts +19 -0
  159. package/examples/playground/app/vault/page.tsx +12 -0
  160. package/examples/playground/app/vault/server.ts +9 -0
  161. package/examples/playground/app/vault/vault-client.tsx +124 -0
  162. package/examples/playground/app/vault/vault.test.ts +147 -0
  163. package/examples/playground/app/vault/vault.ts +119 -0
  164. package/examples/playground/css.d.ts +4 -0
  165. package/examples/playground/lib/store.ts +15 -0
  166. package/examples/playground/next-env.d.ts +5 -0
  167. package/examples/playground/next.config.ts +10 -0
  168. package/examples/playground/package.json +46 -0
  169. package/examples/playground/tsconfig.json +37 -0
  170. package/examples/playground/vercel.json +40 -0
  171. package/package.json +11 -2
  172. package/src/actor-client.ts +132 -0
  173. package/src/actor-react.ts +143 -0
  174. package/src/actor-shared.ts +356 -0
  175. package/src/actor.browser.ts +12 -0
  176. package/src/actor.ts +914 -0
  177. package/src/client.ts +9 -1
  178. package/src/errors.ts +15 -0
  179. package/src/index.ts +1 -1
  180. package/src/server.ts +13 -3
  181. package/dist/contract-jIfaR085.d.ts.map +0 -1
  182. package/dist/server-B2XNevQA.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor-shared-DI7J5upy.js","names":["createContract"],"sources":["../src/actor-shared.ts"],"sourcesContent":["/**\n * Internal shared vocabulary of the actor module — the reserved event\n * envelopes, the protocol and context types, the state fold. Not an\n * entry point: the public surfaces are experimental-a2/actor (server),\n * /actor/client, and /actor/react.\n *\n * An actor is a durable mailbox with memory: one session per instance,\n * events processed one at a time (a lane) unless a handler opts out\n * with `concurrent: true`, state materialized as `a2.actor.state`\n * events committed atomically with each handler's completion.\n */\n\nimport { contract as createContract } from './contract.ts'\nimport type { Reducer } from './reducer.ts'\nimport type { ScheduleTiming } from './server.ts'\nimport type { StandardSchemaV1 } from './standard-schema.ts'\n\n/** Event type of a committed state change — one per completed serial handler. */\nexport const ACTOR_STATE_EVENT = 'a2.actor.state'\n/** Event type of a refusal — a handler that answered by throwing NonRetriableError. */\nexport const ACTOR_FAILED_EVENT = 'a2.actor.failed'\n/** The per-instance lane every serial handler shares — the state's write lock. */\nexport const ACTOR_LANE = 'a2.actor'\n/** Reducer identity — bump when the fold's meaning changes. */\nexport const ACTOR_REDUCER_NAME = 'a2.actor.v1'\n\n/**\n * A handler answered by throwing `NonRetriableError`: the event\n * settled, the state did not change. Isomorphic — the server raises it\n * from calls and `fetch()` serializes it as a 409; the client's `call`\n * proxy revives it from that response.\n */\nexport class ActorRefusedError extends Error {\n /** The refused event's name. */\n readonly event: string\n /** The refused event's message id. */\n readonly messageId: string\n constructor(event: string, messageId: string, message: string) {\n super(message)\n this.name = 'ActorRefusedError'\n this.event = event\n this.messageId = messageId\n }\n}\n\n/**\n * The declaration an actor is defined over: its state shape and its\n * event vocabulary, as types. Core A2 contracts are schemas because\n * the wire is untrusted; actor protocols are types because your\n * server is trusted — same contract-first design language, dialed to\n * the trust level.\n */\nexport type ActorProtocol = {\n state: object\n events: object\n /**\n * Optional third vocabulary: presence field → value type. Ephemeral\n * audience state (who is here, cursors), replicated to subscribers\n * and never stored in the log. Declaring it requires `presence:\n * true` in the actor options — the wire bit types cannot carry.\n */\n presence?: object\n}\n\n/** The protocol's presence vocabulary, `never` when undeclared. */\nexport type ActorPresenceOf<D extends ActorProtocol> = D extends {\n presence: infer P extends object\n}\n ? P\n : never\n\n/** A `setPresence` patch: changed fields, `null` clears one. */\nexport type ActorPresenceValues<P> = {\n [F in keyof P & string]?: P[F] | null\n}\n\n/**\n * The replicated presence map: participant → field → latest value,\n * last write wins per field. Values are peer-authored — render them\n * like user input.\n */\nexport type ActorPresenceMap<P> = {\n [participant: string]: {\n [F in keyof P & string]?: { value: P[F]; seen: number; at: Date }\n }\n}\n\n/**\n * The typed self-send surface: one method per declared event —\n * `ctx.send.transfer({ ref, amount })`. Buffered, not immediate: every\n * send requested during a handler commits atomically with that\n * handler's completion (a serial handler's state commit, a concurrent\n * handler's settlement), riding core's returned-events semantics — so\n * a reserve-and-trigger can never half-happen, and re-runs converge on\n * the same deterministic message ids.\n */\nexport type ActorSend<E> = {\n readonly [K in keyof E]: {} extends E[K]\n ? (input?: E[K]) => void\n : (input: E[K]) => void\n}\n\n/**\n * Timing for a scheduled event: exactly `{ delay: '5d' }` or\n * `{ at: Date }` (core §6 semantics — relative delays anchor to the\n * triggering message's durable `createdAt`, so re-runs resolve the\n * same due time). `name` overrides the timer's identity — it defaults\n * to the target event name, scoped to the triggering message, so one\n * handler run gets one timer per target event unless named apart.\n */\nexport type ActorScheduleOptions = ScheduleTiming & { name?: string }\n\n/**\n * The typed durable-timer surface: one method per declared event —\n * `ctx.schedule.refund({ ref: ctx.id }, { delay: '1h' })`. Unlike\n * `send`, scheduling is immediate, not buffered: it awaits provider\n * acceptance at call time (a timer is provider-side, not a log row),\n * so a handler that schedules and then refuses has still armed the\n * timer. That is safe by the guarded-delivery idiom — the stale timer\n * fires into an idempotent no-op — but it is the one exception to\n * \"refusals are total\". Requires a configured `scheduler`.\n */\nexport type ActorScheduleSend<E> = {\n readonly [K in keyof E]: {} extends E[K]\n ? (input: E[K] | undefined, options: ActorScheduleOptions) => Promise<void>\n : (input: E[K], options: ActorScheduleOptions) => Promise<void>\n}\n\n/** What a serial handler receives alongside its typed input. */\nexport type ActorContext<D extends ActorProtocol> = {\n /** Mutable draft, committed atomically with the handler's completion. */\n state: D['state']\n /** The message id — stable across re-runs; the idempotency key for external I/O. */\n id: string\n /** Durable 1-based dispatch ordinal of this message. */\n attempt: number\n /** The ordinary A2 handler signal — fires on claim expiry or supersession. */\n signal: AbortSignal\n /** Typed buffered self-send — committed atomically with the state commit. */\n send: ActorSend<D['events']>\n /** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */\n schedule: ActorScheduleSend<D['events']>\n}\n\n/**\n * What a `concurrent: true` handler receives. No draft — concurrent\n * handlers run off the lane, in parallel, so state is a snapshot read\n * (honest about staleness) and mutations happen by sending events\n * whose serial handlers decide against fresh state.\n */\nexport type ActorConcurrentContext<D extends ActorProtocol> = {\n /** The message id — stable across re-runs; the idempotency key for external I/O. */\n id: string\n /** Durable 1-based dispatch ordinal of this message. */\n attempt: number\n /** The ordinary A2 handler signal — fires on claim expiry or supersession. */\n signal: AbortSignal\n /** Snapshot read — observational; the world moves while this runs. */\n state(): Promise<{ state: D['state']; index: number }>\n /** Typed buffered self-send — committed atomically with settlement. */\n send: ActorSend<D['events']>\n /** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */\n schedule: ActorScheduleSend<D['events']>\n}\n\nexport type ActorStatePayload<S> = {\n state: S\n /** The event whose handler committed this state. */\n event: string\n /** The message (invocation event) id this state answers. */\n message: string\n}\n\nexport type ActorFailedPayload = {\n /** The message (invocation event) id this refusal answers. */\n message: string\n event: string\n error: string\n}\n\n/** The event vocabulary a client needs to follow an actor's state. */\nexport type ActorClientEventDefs<S> = {\n 'a2.actor.state': StandardSchemaV1<ActorStatePayload<S>>\n}\n\ntype SchemaResult<T> = StandardSchemaV1.Result<T>\n\nconst issue = <T>(message: string): SchemaResult<T> => ({\n issues: [{ message }],\n})\n\nconst schema = <T>(\n label: string,\n parse: (value: unknown) => SchemaResult<T>,\n): StandardSchemaV1<T> => ({\n '~standard': {\n version: 1,\n vendor: 'a2',\n validate(value) {\n try {\n return parse(value)\n } catch (error) {\n return issue(\n `${label}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n },\n },\n})\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** True for plain JSON trees — what the log can store without loss. */\nexport function isJsonTree(\n value: unknown,\n seen: Set<object> = new Set(),\n): boolean {\n if (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'boolean'\n ) {\n return true\n }\n if (typeof value === 'number') return Number.isFinite(value)\n if (typeof value !== 'object') return false\n if (seen.has(value)) return false\n seen.add(value)\n const valid = Array.isArray(value)\n ? value.every((item) => isJsonTree(item, seen))\n : Object.getPrototypeOf(value) === Object.prototype &&\n Object.values(value).every(\n (item) => item === undefined || isJsonTree(item, seen),\n )\n seen.delete(value)\n return valid\n}\n\n/** Per-field ceiling for presence values — presence is a cursor, not a document. */\nexport const PRESENCE_VALUE_MAX_BYTES = 8_192\n\n/**\n * The open presence vocabulary: the '*' catch-all validates any field\n * name against the JSON floor. The protocol types the vocabulary at\n * compile time; the route's `authorize` sees every set before\n * acceptance; renderers treat values as user input. Meaning has three\n * guards — this schema only owns the bytes.\n */\nexport const openPresenceDefs: Readonly<\n Record<string, StandardSchemaV1<unknown>>\n> = Object.freeze({\n '*': {\n '~standard': {\n version: 1,\n vendor: 'a2',\n validate: (value: unknown) => {\n if (!isJsonTree(value)) {\n return {\n issues: [{ message: 'presence values must be plain JSON trees' }],\n }\n }\n if (JSON.stringify(value).length > PRESENCE_VALUE_MAX_BYTES) {\n return {\n issues: [\n {\n message: `presence values are capped at ${PRESENCE_VALUE_MAX_BYTES} bytes`,\n },\n ],\n }\n }\n return { value }\n },\n },\n } satisfies StandardSchemaV1<unknown>,\n})\n\n/**\n * The `a2.actor.state` envelope: provenance plus the state, held to a\n * plain JSON tree (a `Date` or `Map` in state fails loudly at commit,\n * never silently coerces in the log).\n */\nexport function actorStateSchema<S>(): StandardSchemaV1<ActorStatePayload<S>> {\n return schema(ACTOR_STATE_EVENT, (value) => {\n if (\n !isRecord(value) ||\n typeof value['event'] !== 'string' ||\n typeof value['message'] !== 'string'\n ) {\n return issue(`invalid ${ACTOR_STATE_EVENT} payload`)\n }\n const state = value['state']\n if (!isJsonTree(state)) {\n return issue('the actor state must be a plain JSON tree')\n }\n return {\n value: {\n state: state as S,\n event: value['event'],\n message: value['message'],\n },\n }\n })\n}\n\nexport const actorFailedSchema: StandardSchemaV1<ActorFailedPayload> = schema(\n ACTOR_FAILED_EVENT,\n (value) => {\n if (\n !isRecord(value) ||\n typeof value['message'] !== 'string' ||\n typeof value['event'] !== 'string' ||\n typeof value['error'] !== 'string'\n ) {\n return issue(`invalid ${ACTOR_FAILED_EVENT} payload`)\n }\n return {\n value: {\n message: value['message'],\n event: value['event'],\n error: value['error'],\n },\n }\n },\n)\n\nexport type ActorReducerOptions<S> = {\n /** The actor's name — must match the server-side definition. */\n name: string\n /** The initial state — a plain JSON tree, the fold's seed. */\n state: S\n}\n\n/**\n * The library fold over an actor's state commits — last write wins.\n * Pure library code: a browser bundle folds an actor's live state with\n * no user code, so the actor definition itself stays server-only\n * (import the server module's type for typing, this reducer for data).\n */\nexport function actorReducer<S>(\n options: ActorReducerOptions<S>,\n): Reducer<ActorClientEventDefs<S>, S> {\n if (!isJsonTree(options.state)) {\n throw new TypeError(\n `actor '${options.name}': the initial state must be a plain JSON tree`,\n )\n }\n const events: ActorClientEventDefs<S> = {\n [ACTOR_STATE_EVENT]: actorStateSchema(),\n }\n return createContract({ name: options.name, events })\n .reducer({ name: ACTOR_REDUCER_NAME, initialState: options.state })\n .fold((state, event) =>\n event.type === ACTOR_STATE_EVENT ? event.payload.state : state,\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAa,oBAAoB;;AAEjC,MAAa,qBAAqB;;AAElC,MAAa,aAAa;;AAE1B,MAAa,qBAAqB;;;;;;;AAQlC,IAAa,oBAAb,cAAuC,MAAM;;CAE3C;;CAEA;CACA,YAAY,OAAe,WAAmB,SAAiB;EAC7D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,YAAY;CACnB;AACF;AAgJA,MAAM,SAAY,aAAsC,EACtD,QAAQ,CAAC,EAAE,QAAQ,CAAC,EACtB;AAEA,MAAM,UACJ,OACA,WACyB,EACzB,aAAa;CACX,SAAS;CACT,QAAQ;CACR,SAAS,OAAO;EACd,IAAI;GACF,OAAO,MAAM,KAAK;EACpB,SAAS,OAAO;GACd,OAAO,MACL,GAAG,MAAM,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACpE;EACF;CACF;AACF,EACF;AAEA,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;AAGrE,SAAgB,WACd,OACA,uBAAoB,IAAI,IAAI,GACnB;CACT,IACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,WAEjB,OAAO;CAET,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,SAAS,KAAK;CAC3D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO;CAC5B,KAAK,IAAI,KAAK;CACd,MAAM,QAAQ,MAAM,QAAQ,KAAK,IAC7B,MAAM,OAAO,SAAS,WAAW,MAAM,IAAI,CAAC,IAC5C,OAAO,eAAe,KAAK,MAAM,OAAO,aACxC,OAAO,OAAO,KAAK,CAAC,CAAC,OAClB,SAAS,SAAS,KAAA,KAAa,WAAW,MAAM,IAAI,CACvD;CACJ,KAAK,OAAO,KAAK;CACjB,OAAO;AACT;;AAGA,MAAa,2BAA2B;;;;;;;;AASxC,MAAa,mBAET,OAAO,OAAO,EAChB,KAAK,EACH,aAAa;CACX,SAAS;CACT,QAAQ;CACR,WAAW,UAAmB;EAC5B,IAAI,CAAC,WAAW,KAAK,GACnB,OAAO,EACL,QAAQ,CAAC,EAAE,SAAS,2CAA2C,CAAC,EAClE;EAEF,IAAI,KAAK,UAAU,KAAK,CAAC,CAAC,SAAA,MACxB,OAAO,EACL,QAAQ,CACN,EACE,SAAS,iCAAiC,yBAAyB,QACrE,CACF,EACF;EAEF,OAAO,EAAE,MAAM;CACjB;AACF,EACF,EACF,CAAC;;;;;;AAOD,SAAgB,mBAA8D;CAC5E,OAAO,OAAO,oBAAoB,UAAU;EAC1C,IACE,CAAC,SAAS,KAAK,KACf,OAAO,MAAM,aAAa,YAC1B,OAAO,MAAM,eAAe,UAE5B,OAAO,MAAM,WAAW,kBAAkB,SAAS;EAErD,MAAM,QAAQ,MAAM;EACpB,IAAI,CAAC,WAAW,KAAK,GACnB,OAAO,MAAM,2CAA2C;EAE1D,OAAO,EACL,OAAO;GACE;GACP,OAAO,MAAM;GACb,SAAS,MAAM;EACjB,EACF;CACF,CAAC;AACH;AAEA,MAAa,oBAA0D,OACrE,qBACC,UAAU;CACT,IACE,CAAC,SAAS,KAAK,KACf,OAAO,MAAM,eAAe,YAC5B,OAAO,MAAM,aAAa,YAC1B,OAAO,MAAM,aAAa,UAE1B,OAAO,MAAM,WAAW,mBAAmB,SAAS;CAEtD,OAAO,EACL,OAAO;EACL,SAAS,MAAM;EACf,OAAO,MAAM;EACb,OAAO,MAAM;CACf,EACF;AACF,CACF;;;;;;;AAeA,SAAgB,aACd,SACqC;CACrC,IAAI,CAAC,WAAW,QAAQ,KAAK,GAC3B,MAAM,IAAI,UACR,UAAU,QAAQ,KAAK,+CACzB;CAEF,MAAM,SAAkC,GACrC,oBAAoB,iBAAiB,EACxC;CACA,OAAOA,SAAe;EAAE,MAAM,QAAQ;EAAM;CAAO,CAAC,CAAC,CAClD,QAAQ;EAAE,MAAM;EAAoB,cAAc,QAAQ;CAAM,CAAC,CAAC,CAClE,MAAM,OAAO,UACZ,MAAM,SAAA,mBAA6B,MAAM,QAAQ,QAAQ,KAC3D;AACJ"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,13 @@
1
+ //#region src/actor.browser.ts
2
+ /**
3
+ * The browser build of experimental-a2/actor. There isn't one — on
4
+ * purpose. Actions are server code; the browser follows an actor's
5
+ * state with `actorReducer()` from `experimental-a2/actor` (isomorphic)
6
+ * over the ordinary session client. This module existing in a client
7
+ * bundle means a `'use client'` file (or something it imports)
8
+ * value-imported your actor module.
9
+ */
10
+ throw new Error("experimental-a2/actor is server-only — a client bundle imported it. Import actorReducer from experimental-a2/actor and the session client (experimental-a2/client, experimental-a2/react) in browser code instead.");
11
+ //#endregion
12
+
13
+ //# sourceMappingURL=actor.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor.browser.js","names":[],"sources":["../src/actor.browser.ts"],"sourcesContent":["/**\n * The browser build of experimental-a2/actor. There isn't one — on\n * purpose. Actions are server code; the browser follows an actor's\n * state with `actorReducer()` from `experimental-a2/actor` (isomorphic)\n * over the ordinary session client. This module existing in a client\n * bundle means a `'use client'` file (or something it imports)\n * value-imported your actor module.\n */\nthrow new Error(\n 'experimental-a2/actor is server-only — a client bundle imported it. Import actorReducer from experimental-a2/actor and the session client (experimental-a2/client, experimental-a2/react) in browser code instead.',\n)\nexport {}\n"],"mappings":";;;;;;;;;AAQA,MAAM,IAAI,MACR,oNACF"}
@@ -0,0 +1,176 @@
1
+ import { i as Contract, s as EventDefs, t as Reducer } from "./reducer-DJKWm3cp.js";
2
+ import { t as A2Store } from "./store-DGHeBtIQ.js";
3
+ import { m as Session, n as A2Server, t as A2Scheduler } from "./server-CKY3_lbw.js";
4
+ import { i as A2Telemetry } from "./telemetry-CpeclqB2.js";
5
+ import { a as ActorPresenceOf, c as ActorRefusedError, d as ActorSend, i as ActorPresenceMap, l as ActorScheduleOptions, n as ActorConcurrentContext, o as ActorPresenceValues, r as ActorContext, s as ActorProtocol, t as ActorClientEventDefs, u as ActorScheduleSend } from "./actor-shared-BACubf4x.js";
6
+ //#region src/actor.d.ts
7
+ /**
8
+ * One event's handler: ordinary server code. Serial by default — read
9
+ * `ctx.state`, perform I/O, mutate `ctx.state`, one atomic commit at
10
+ * return. `{ concurrent: true, handle }` opts the handler out of the
11
+ * lane for slow I/O: state becomes a snapshot read and mutations
12
+ * happen by sending events. The input is typed by the protocol; the
13
+ * wire hands handlers parsed JSON, so a wire-exposed event guards its
14
+ * input in its first line (`schema.parse(input)` if you like schemas —
15
+ * userland either way).
16
+ */
17
+ type ActorHandler<D extends ActorProtocol, K extends keyof D["events"]> = ((ctx: ActorContext<D>, input: D["events"][K]) => void | Promise<void>) | {
18
+ concurrent: true;
19
+ handle: (ctx: ActorConcurrentContext<D>, input: D["events"][K]) => void | Promise<void>;
20
+ };
21
+ /**
22
+ * Mark a handler concurrent — off the lane, in parallel with the lane
23
+ * and with other concurrent handlers, `ctx.state()` a snapshot read.
24
+ * Sugar for the structural `{ concurrent: true, handle }` form:
25
+ *
26
+ * ```ts
27
+ * handlers: {
28
+ * transfer: concurrent(async (ctx, input) => {
29
+ * await bank.transfer(input, { idempotencyKey: input.ref })
30
+ * ctx.send.settle({ ref: input.ref })
31
+ * }),
32
+ * }
33
+ * ```
34
+ */
35
+ declare function concurrent<Ctx, I>(handle: (ctx: Ctx, input: I) => void | Promise<void>): {
36
+ concurrent: true;
37
+ handle: (ctx: Ctx, input: I) => void | Promise<void>;
38
+ };
39
+ type ActorOptions<D extends ActorProtocol> = {
40
+ /** Identity — prefixes storage keys; one contract per actor definition. */
41
+ name: string;
42
+ /** The initial state — checked against the protocol, a plain JSON tree. */
43
+ state: D["state"];
44
+ /**
45
+ * One handler per declared event — completeness and payload shapes
46
+ * are compile-checked against the protocol.
47
+ */
48
+ handlers: { [K in keyof D["events"]]: ActorHandler<D, K>; };
49
+ store?: A2Store;
50
+ scheduler?: A2Scheduler;
51
+ telemetry?: A2Telemetry;
52
+ } & ([ActorPresenceOf<D>] extends [never] ? {
53
+ presence?: never;
54
+ } : {
55
+ /**
56
+ * The wire bit for the protocol's `presence` vocabulary — types
57
+ * erase, so the runtime needs one value to arm the presence
58
+ * lanes (frames on GET, envelopes on POST). Required exactly
59
+ * when the protocol declares `presence`; the vocabulary itself
60
+ * is typed there. Values are validated to the JSON floor and
61
+ * pass through `authorize` as `{ type: 'presence' }` operations;
62
+ * meaning stays with your renderers — treat values as user
63
+ * input.
64
+ */
65
+ presence: true;
66
+ });
67
+ /**
68
+ * One HTTP operation `handle.fetch` is about to serve — the actor
69
+ * mirror of core's `A2Operation`. `authorize` sees it before any
70
+ * write or subscription; returning `false` answers 403.
71
+ */
72
+ type ActorOperation = {
73
+ readonly type: "stream";
74
+ readonly id: string;
75
+ readonly startAfter: number;
76
+ } | {
77
+ readonly type: "call";
78
+ readonly id: string;
79
+ readonly event: string;
80
+ readonly input: unknown;
81
+ readonly messageId?: string;
82
+ } | {
83
+ readonly type: "presence";
84
+ readonly id: string;
85
+ readonly participant: string;
86
+ readonly values: Readonly<Record<string, unknown>>;
87
+ };
88
+ type ActorFetchOptions<D extends ActorProtocol> = {
89
+ /** Per-operation policy — authentication happened in your route. */
90
+ authorize?: (operation: ActorOperation) => boolean | Promise<boolean>;
91
+ /**
92
+ * Request-time projection: what this mount's audience sees. Applied
93
+ * to every state frame on the stream AND to call answers (the two
94
+ * untrusted lanes must agree, or the call lane leaks what the
95
+ * stream hides). The definition stays audience-agnostic — different
96
+ * routes project the same actor differently, and per-viewer views
97
+ * are just closures over the route's auth. Output is held to the
98
+ * JSON-tree floor like everything else. Transport-agnostic: applied
99
+ * at frame emission, so any wire the door speaks emits projected
100
+ * frames. Absent, the full state ships (the trusted default).
101
+ */
102
+ view?: (state: D["state"]) => unknown;
103
+ };
104
+ type ActorSendOptions = {
105
+ /** Explicit message id — makes retries of this send idempotent. */
106
+ id?: string;
107
+ };
108
+ type ActorCallOptions = ActorSendOptions & {
109
+ /** How long to await the answer before rejecting the wait (default 30s). */
110
+ timeoutMs?: number;
111
+ };
112
+ type ActorCallResult<S> = {
113
+ /** The actor's state after this message was processed. */
114
+ state: S;
115
+ /** Log index of the state commit that answered this message. */
116
+ index: number;
117
+ };
118
+ type CallMethod<D extends ActorProtocol, K extends keyof D["events"]> = {} extends D["events"][K] ? (input?: D["events"][K], options?: ActorCallOptions) => Promise<ActorCallResult<D["state"]>> : (input: D["events"][K], options?: ActorCallOptions) => Promise<ActorCallResult<D["state"]>>;
119
+ type SendMethod<D extends ActorProtocol, K extends keyof D["events"]> = {} extends D["events"][K] ? (input?: D["events"][K], options?: ActorSendOptions) => Promise<{
120
+ id: string;
121
+ index: number;
122
+ }> : (input: D["events"][K], options?: ActorSendOptions) => Promise<{
123
+ id: string;
124
+ index: number;
125
+ }>;
126
+ /** One instance: typed calls and sends, reads, the managed door, the escape hatch. */
127
+ type ActorHandle<D extends ActorProtocol> = {
128
+ readonly id: string;
129
+ /**
130
+ * Send the event and await its answer: the state after the handler
131
+ * ran, or a rejection with `ActorRefusedError`. Serial events only —
132
+ * concurrent handlers produce no answer to await; `send` them.
133
+ */
134
+ readonly call: { readonly [K in keyof D["events"]]: CallMethod<D, K>; };
135
+ /** Cast: validate, append the event durably, return without waiting. */
136
+ readonly send: { readonly [K in keyof D["events"]]: SendMethod<D, K>; };
137
+ /** Snapshot read — never queues behind pending messages. */
138
+ readonly state: () => Promise<ActorCallResult<D["state"]>>;
139
+ /**
140
+ * The managed door — the actor mirror of `server.fetch`, bound to
141
+ * this instance. GET streams the state plane (`a2.actor.state`
142
+ * commits only, `?index` resume, heartbeat, deadline rotation);
143
+ * POST is the call lane `createActorClient` speaks (`{ event,
144
+ * input, messageId? }` → the answer, 409 for a refusal). Your route
145
+ * authenticates and picks the instance; `authorize` sees every
146
+ * operation. Custom endpoints are route code around this call.
147
+ */
148
+ readonly fetch: (request: Request, options?: ActorFetchOptions<D>) => Promise<Response>;
149
+ /** Escape hatch: the raw A2 session (history, stream, schedule). */
150
+ readonly session: Session<EventDefs>;
151
+ };
152
+ type ActorDefinition<D extends ActorProtocol> = {
153
+ readonly name: string;
154
+ /**
155
+ * Type-only carrier of the protocol, so clients deriving from
156
+ * `typeof def` can see the vocabularies (`experimental-a2/actor/react`
157
+ * types `presence` from it). Never a runtime value.
158
+ */
159
+ readonly protocol?: D;
160
+ /** The assembled contract — declared events plus the reserved a2.actor.* pair. */
161
+ readonly contract: Contract<EventDefs>;
162
+ /** The underlying A2 server — `server.fetch` mounts the full session wire. */
163
+ readonly server: A2Server<EventDefs>;
164
+ /** The state fold — same identity as the client's follower fold. */
165
+ readonly reducer: Reducer<ActorClientEventDefs<D["state"]>, D["state"]>;
166
+ actor(id: string): ActorHandle<D>;
167
+ };
168
+ /**
169
+ * Define and serve an actor over its protocol: a named, durable
170
+ * instance-per-id with serialized state handlers, concurrent I/O
171
+ * handlers, and typed self-sends.
172
+ */
173
+ declare function actor<D extends ActorProtocol>(options: ActorOptions<D>): ActorDefinition<D>;
174
+ //#endregion
175
+ export { ActorCallOptions, ActorCallResult, type ActorConcurrentContext, type ActorContext, ActorDefinition, ActorFetchOptions, ActorHandle, ActorHandler, ActorOperation, ActorOptions, type ActorPresenceMap, type ActorPresenceOf, type ActorPresenceValues, type ActorProtocol, ActorRefusedError, type ActorScheduleOptions, type ActorScheduleSend, type ActorSend, ActorSendOptions, actor, concurrent };
176
+ //# sourceMappingURL=actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor.d.ts","names":[],"sources":["../src/actor.ts"],"mappings":";;;;;;;;;;;;;;;;KA6GY,aAAa,UAAU,eAAe,gBAAgB,iBAC5D,KAAK,aAAa,IAAI,OAAO,YAAY,cAAc;EAEvD;EACA,SACE,KAAK,uBAAuB,IAC5B,OAAO,YAAY,cACT;;;;;;;;;;;;;;;;iBAiBF,WAAW,KAAK,GAC9B,SAAS,KAAK,KAAK,OAAO,aAAa;EACpC;EAAkB,SAAS,KAAK,KAAK,OAAO,aAAa;;KAIlD,aAAa,UAAU;;EAEjC;;EAEA,OAAO;;;;;EAKP,aAAa,WAAW,cAAc,aAAa,GAAG;EACtD,QAAQ;EACR,YAAY;EACZ,YAAY;MACR,gBAAgB;EAChB;;;;;;;;;;;;EAYA;;;;;;;KAQM;WAEG;WACA;WACA;;WAGA;WACA;WACA;WACA;WACA;;WAGA;WACA;WACA;WACA,QAAQ,SAAS;;KAGpB,kBAAkB,UAAU;;EAEtC,aAAa,WAAW,6BAA6B;;;;;;;;;;;;EAYrD,QAAQ,OAAO;;KAGL;;EAEV;;KAGU,mBAAmB;;EAE7B;;KAGU,gBAAgB;;EAE1B,OAAO;;EAEP;;KAGG,WACH,UAAU,eACV,gBAAgB,0BACH,YAAY,MAErB,QAAQ,YAAY,IACpB,UAAU,qBACP,QAAQ,gBAAgB,gBAE3B,OAAO,YAAY,IACnB,UAAU,qBACP,QAAQ,gBAAgB;KAE5B,WACH,UAAU,eACV,gBAAgB,0BACH,YAAY,MAErB,QAAQ,YAAY,IACpB,UAAU,qBACP;EAAU;EAAY;MAEzB,OAAO,YAAY,IACnB,UAAU,qBACP;EAAU;EAAY;;;KAGnB,YAAY,UAAU;WACvB;;;;;;WAMA,kBAAkB,WAAW,cAAc,WAAW,GAAG;;WAEzD,kBAAkB,WAAW,cAAc,WAAW,GAAG;;WAEzD,aAAa,QAAQ,gBAAgB;;;;;;;;;;WAUrC,QACP,SAAS,SACT,UAAU,kBAAkB,OACzB,QAAQ;;WAEJ,SAAS,QAAQ;;KAGhB,gBAAgB,UAAU;WAC3B;;;;;;WAMA,WAAW;;WAEX,UAAU,SAAS;;WAEnB,QAAQ,SAAS;;WAEjB,SAAS,QAAQ,qBAAqB,aAAa;EAC5D,MAAM,aAAa,YAAY;;;;;;;iBAsDjB,MAAM,UAAU,eAC9B,SAAS,aAAa,KACrB,gBAAgB"}
package/dist/actor.js ADDED
@@ -0,0 +1,437 @@
1
+ import { t as contract } from "./contract-48bUMgcL.js";
2
+ import { n as NonRetriableError, t as A2Error } from "./errors-DCk6ch5n.js";
3
+ import { a as parsePresenceSibling, i as sseResponse, r as setServerFetchHooks, t as createServer } from "./server-CBET-jSz.js";
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
+ //#region src/actor.ts
6
+ /**
7
+ * experimental-a2/actor — durable actors over the log. Server-only
8
+ * (handlers are server code); the browser condition resolves to a
9
+ * throwing stub, and the browser surfaces are experimental-a2/actor/client
10
+ * and experimental-a2/actor/react.
11
+ *
12
+ * An actor is defined over a protocol — its state shape and event
13
+ * vocabulary, as types — and one complete `handlers` bag:
14
+ *
15
+ * ```ts
16
+ * interface Counter {
17
+ * state: { count: number }
18
+ * events: { increment: object; add: { by: number } }
19
+ * }
20
+ * const counter = actor<Counter>({
21
+ * name: 'counter',
22
+ * state: { count: 0 },
23
+ * handlers: {
24
+ * increment: (ctx) => {
25
+ * ctx.state.count++
26
+ * },
27
+ * add: (ctx, input) => {
28
+ * ctx.state.count += input.by
29
+ * },
30
+ * },
31
+ * })
32
+ * ```
33
+ *
34
+ * Handlers are serial by default: one at a time per instance (a lane —
35
+ * the state's write lock), `ctx.state` a mutable draft committed
36
+ * atomically with completion. A handler holding slow I/O opts out of
37
+ * the lane with `{ concurrent: true, handle }`: it runs in parallel,
38
+ * reads state as a snapshot, and mutates by sending events whose
39
+ * serial handlers decide against fresh state. `ctx.send.eventName(...)`
40
+ * is typed by the protocol and buffered — committed atomically with
41
+ * the handler's completion, so a reserve and its trigger can never
42
+ * half-happen.
43
+ *
44
+ * A thrown `NonRetriableError` is an answer (settled, delivered to a
45
+ * waiting caller); any other error retries like every A2 handler
46
+ * failure; a crash re-runs against the same pre-state. Everything
47
+ * compiles to contract + reducer + laned handlers + returned events —
48
+ * no new store operations, no new wire.
49
+ */
50
+ /**
51
+ * Mark a handler concurrent — off the lane, in parallel with the lane
52
+ * and with other concurrent handlers, `ctx.state()` a snapshot read.
53
+ * Sugar for the structural `{ concurrent: true, handle }` form:
54
+ *
55
+ * ```ts
56
+ * handlers: {
57
+ * transfer: concurrent(async (ctx, input) => {
58
+ * await bank.transfer(input, { idempotencyKey: input.ref })
59
+ * ctx.send.settle({ ref: input.ref })
60
+ * }),
61
+ * }
62
+ * ```
63
+ */
64
+ function concurrent(handle) {
65
+ return {
66
+ concurrent: true,
67
+ handle
68
+ };
69
+ }
70
+ const UNSAFE_EVENT_NAMES = /* @__PURE__ */ new Set([
71
+ "__proto__",
72
+ "constructor",
73
+ "prototype"
74
+ ]);
75
+ const forbidden = () => Response.json({ error: "the operation is not authorized" }, { status: 403 });
76
+ const CALL_TIMEOUT_MS = 3e4;
77
+ const CALL_POLL_MS = 25;
78
+ /** Declared events accept any JSON tree; `undefined` normalizes to `{}`. */
79
+ const jsonInputSchema = { "~standard": {
80
+ version: 1,
81
+ vendor: "a2",
82
+ validate(value) {
83
+ if (value === void 0 || value === null) return { value: {} };
84
+ if (!isJsonTree(value)) return { issues: [{ message: "event input must be a plain JSON tree" }] };
85
+ return { value };
86
+ }
87
+ } };
88
+ const describeError = (error) => error instanceof Error ? error.message : String(error);
89
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
90
+ /**
91
+ * Define and serve an actor over its protocol: a named, durable
92
+ * instance-per-id with serialized state handlers, concurrent I/O
93
+ * handlers, and typed self-sends.
94
+ */
95
+ function actor(options) {
96
+ const { name } = options;
97
+ const handlersByEvent = /* @__PURE__ */ new Map();
98
+ const events = {
99
+ [ACTOR_STATE_EVENT]: actorStateSchema(),
100
+ [ACTOR_FAILED_EVENT]: actorFailedSchema
101
+ };
102
+ for (const [eventName, def] of Object.entries(options.handlers)) {
103
+ if (UNSAFE_EVENT_NAMES.has(eventName)) throw new TypeError(`actor '${name}': '${eventName}' collides with object plumbing and cannot name an event`);
104
+ if (eventName.startsWith("a2.")) throw new TypeError(`actor '${name}': event names starting with 'a2.' are reserved`);
105
+ handlersByEvent.set(eventName, typeof def === "function" ? {
106
+ kind: "serial",
107
+ handle: def
108
+ } : {
109
+ kind: "concurrent",
110
+ handle: def.handle
111
+ });
112
+ events[eventName] = jsonInputSchema;
113
+ }
114
+ const reducer = actorReducer({
115
+ name,
116
+ state: options.state
117
+ });
118
+ const serverReducer = reducer;
119
+ const makeSchedule = (schedule) => {
120
+ const surface = {};
121
+ for (const eventName of handlersByEvent.keys()) Object.defineProperty(surface, eventName, {
122
+ value: (input, scheduleOptions) => {
123
+ const { name: taskName, ...timing } = scheduleOptions;
124
+ return schedule(taskName ?? eventName, timing, {
125
+ type: eventName,
126
+ payload: input ?? {}
127
+ });
128
+ },
129
+ enumerable: true,
130
+ configurable: true,
131
+ writable: false
132
+ });
133
+ return surface;
134
+ };
135
+ const makeSend = (buffer) => {
136
+ const send = {};
137
+ for (const eventName of handlersByEvent.keys()) Object.defineProperty(send, eventName, {
138
+ value: (input) => {
139
+ buffer.push({
140
+ type: eventName,
141
+ payload: input ?? {}
142
+ });
143
+ },
144
+ enumerable: true,
145
+ configurable: true,
146
+ writable: false
147
+ });
148
+ return send;
149
+ };
150
+ const handlers = {};
151
+ for (const [eventName, def] of handlersByEvent) {
152
+ if (def.kind === "concurrent") {
153
+ handlers[eventName] = async (ctx) => {
154
+ const sends = [];
155
+ try {
156
+ await def.handle({
157
+ id: ctx.event.id,
158
+ attempt: ctx.attempt,
159
+ signal: ctx.signal,
160
+ send: makeSend(sends),
161
+ schedule: makeSchedule((taskName, timing, event) => ctx.session.schedule(taskName, timing, event)),
162
+ state: async () => {
163
+ const { state, index } = await ctx.session.state(serverReducer, { through: "latest" });
164
+ return {
165
+ state,
166
+ index
167
+ };
168
+ }
169
+ }, ctx.event.payload);
170
+ } catch (error) {
171
+ if (!(error instanceof NonRetriableError)) throw error;
172
+ const payload = {
173
+ message: ctx.event.id,
174
+ event: eventName,
175
+ error: describeError(error)
176
+ };
177
+ return {
178
+ type: ACTOR_FAILED_EVENT,
179
+ payload
180
+ };
181
+ }
182
+ return sends;
183
+ };
184
+ continue;
185
+ }
186
+ handlers[eventName] = {
187
+ lane: ACTOR_LANE,
188
+ handler: async (ctx) => {
189
+ const { state } = await ctx.session.state(serverReducer, { through: "latest" });
190
+ const draft = structuredClone(state);
191
+ const sends = [];
192
+ try {
193
+ await def.handle({
194
+ state: draft,
195
+ id: ctx.event.id,
196
+ attempt: ctx.attempt,
197
+ signal: ctx.signal,
198
+ send: makeSend(sends),
199
+ schedule: makeSchedule((taskName, timing, event) => ctx.session.schedule(taskName, timing, event))
200
+ }, ctx.event.payload);
201
+ } catch (error) {
202
+ if (!(error instanceof NonRetriableError)) throw error;
203
+ const payload = {
204
+ message: ctx.event.id,
205
+ event: eventName,
206
+ error: describeError(error)
207
+ };
208
+ return {
209
+ type: ACTOR_FAILED_EVENT,
210
+ payload
211
+ };
212
+ }
213
+ const payload = {
214
+ state: draft,
215
+ event: eventName,
216
+ message: ctx.event.id
217
+ };
218
+ return [...sends, {
219
+ type: ACTOR_STATE_EVENT,
220
+ payload
221
+ }];
222
+ }
223
+ };
224
+ }
225
+ const presenceEnabled = options.presence === true;
226
+ const contract$1 = presenceEnabled ? contract({
227
+ name,
228
+ events,
229
+ presence: openPresenceDefs
230
+ }) : contract({
231
+ name,
232
+ events
233
+ });
234
+ const server = createServer({
235
+ contract: contract$1,
236
+ handlers,
237
+ ...options.store ? { store: options.store } : {},
238
+ ...options.scheduler ? { scheduler: options.scheduler } : {},
239
+ ...options.telemetry ? { telemetry: options.telemetry } : {}
240
+ });
241
+ setServerFetchHooks(server, { validateIngress: ({ events: pushed }) => {
242
+ for (const event of pushed) if (event.type.startsWith("a2.actor.")) throw new TypeError(`'${event.type}' is reserved — only a handler's completion can author it`);
243
+ } });
244
+ const makeHandle = (id) => {
245
+ const session = server.session(id);
246
+ const sendEvent = async (eventName, input, sendOptions) => {
247
+ const [message] = await session.append({
248
+ type: eventName,
249
+ payload: input ?? {},
250
+ ...sendOptions?.id ? { id: sendOptions.id } : {}
251
+ });
252
+ if (!message) throw new Error("append returned no event");
253
+ return {
254
+ id: message.id,
255
+ index: message.index
256
+ };
257
+ };
258
+ const callEvent = async (eventName, input, callOptions) => {
259
+ if (handlersByEvent.get(eventName)?.kind === "concurrent") throw new TypeError(`actor '${name}': '${eventName}' is concurrent — it produces no answer to await; send it instead`);
260
+ const message = await sendEvent(eventName, input, callOptions);
261
+ const deadline = Date.now() + (callOptions?.timeoutMs ?? CALL_TIMEOUT_MS);
262
+ let cursor = message.index + 1;
263
+ for (;;) {
264
+ await server.drain(id);
265
+ const tail = await session.history({ gte: cursor });
266
+ for (const event of tail) {
267
+ if (event.type === "a2.actor.state") {
268
+ const payload = event.payload;
269
+ if (payload.message === message.id) return {
270
+ state: payload.state,
271
+ index: event.index
272
+ };
273
+ }
274
+ if (event.type === "a2.actor.failed") {
275
+ const payload = event.payload;
276
+ if (payload.message === message.id) throw new ActorRefusedError(eventName, message.id, payload.error);
277
+ }
278
+ }
279
+ const last = tail[tail.length - 1];
280
+ if (last) cursor = last.index + 1;
281
+ if (Date.now() >= deadline) throw new Error(`actor '${name}': timed out waiting for '${eventName}' (message ${message.id})`);
282
+ await sleep(CALL_POLL_MS);
283
+ }
284
+ };
285
+ const readState = async () => {
286
+ const { state, index } = await session.state(serverReducer);
287
+ return {
288
+ state,
289
+ index
290
+ };
291
+ };
292
+ const applyView = (view, state) => {
293
+ const viewed = view(state);
294
+ if (!isJsonTree(viewed)) throw new TypeError(`actor '${name}': the view returned a value that is not a plain JSON tree`);
295
+ return viewed;
296
+ };
297
+ const streamStates = async function* (startAfter, view) {
298
+ const source = presenceEnabled ? session.stream({
299
+ startAfter,
300
+ presence: true
301
+ }) : session.stream({ startAfter });
302
+ for await (const item of source) {
303
+ if (!("type" in item)) {
304
+ yield item;
305
+ continue;
306
+ }
307
+ const event = item;
308
+ if (event.type !== "a2.actor.state") continue;
309
+ if (!view) {
310
+ yield event;
311
+ continue;
312
+ }
313
+ const payload = event.payload;
314
+ yield {
315
+ ...event,
316
+ payload: {
317
+ ...payload,
318
+ state: applyView(view, payload.state)
319
+ }
320
+ };
321
+ }
322
+ };
323
+ const handleFetch = async (request, fetchOptions) => {
324
+ const authorized = async (operation) => await fetchOptions?.authorize?.(Object.freeze(operation)) !== false;
325
+ if (request.method === "GET") {
326
+ const url = new URL(request.url);
327
+ const raw = Number(url.searchParams.get("index"));
328
+ const startAfter = Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
329
+ if (!await authorized({
330
+ type: "stream",
331
+ id,
332
+ startAfter
333
+ })) return forbidden();
334
+ return sseResponse(streamStates(startAfter, fetchOptions?.view));
335
+ }
336
+ if (request.method !== "POST") return new Response(null, {
337
+ status: 405,
338
+ headers: { allow: "GET, POST" }
339
+ });
340
+ let body;
341
+ try {
342
+ body = await request.json();
343
+ } catch {
344
+ return Response.json({ error: "invalid JSON body" }, { status: 400 });
345
+ }
346
+ const record = typeof body === "object" && body !== null ? body : {};
347
+ if ("presence" in record) {
348
+ if (!presenceEnabled) return Response.json({ error: `actor '${name}' declares no presence` }, { status: 400 });
349
+ const pushedEvents = record["events"];
350
+ if (pushedEvents !== void 0 && (!Array.isArray(pushedEvents) || pushedEvents.length > 0)) return Response.json({ error: "the actor wire takes calls, not event pushes" }, { status: 400 });
351
+ let patch;
352
+ try {
353
+ const parsed = parsePresenceSibling(record["presence"]);
354
+ if (!parsed) throw new TypeError("presence must be an object");
355
+ patch = parsed;
356
+ } catch (error) {
357
+ return Response.json({ error: describeError(error) }, { status: 400 });
358
+ }
359
+ if (!await authorized({
360
+ type: "presence",
361
+ id,
362
+ participant: patch.participant,
363
+ values: patch.values
364
+ })) return forbidden();
365
+ try {
366
+ await session.setPresence(patch);
367
+ } catch (error) {
368
+ if (error instanceof A2Error && (error.code === "INVALID_PAYLOAD" || error.code === "UNKNOWN_PRESENCE_FIELD")) return Response.json({ error: error.message }, { status: 400 });
369
+ throw error;
370
+ }
371
+ return Response.json([]);
372
+ }
373
+ const eventName = record["event"];
374
+ if (typeof eventName !== "string" || handlersByEvent.get(eventName)?.kind !== "serial") return Response.json({ error: "unknown event" }, { status: 400 });
375
+ const messageId = record["messageId"];
376
+ if (messageId !== void 0 && typeof messageId !== "string") return Response.json({ error: "invalid messageId" }, { status: 400 });
377
+ const input = record["input"];
378
+ if (!await authorized({
379
+ type: "call",
380
+ id,
381
+ event: eventName,
382
+ input,
383
+ ...messageId === void 0 ? {} : { messageId }
384
+ })) return forbidden();
385
+ try {
386
+ const result = await callEvent(eventName, input, messageId === void 0 ? void 0 : { id: messageId });
387
+ return Response.json(fetchOptions?.view ? {
388
+ state: applyView(fetchOptions.view, result.state),
389
+ index: result.index
390
+ } : result);
391
+ } catch (error) {
392
+ if (error instanceof ActorRefusedError) return Response.json({
393
+ error: error.message,
394
+ event: error.event,
395
+ messageId: error.messageId
396
+ }, { status: 409 });
397
+ if (error instanceof A2Error && (error.code === "INVALID_PAYLOAD" || error.code === "UNKNOWN_EVENT_TYPE")) return Response.json({ error: error.message }, { status: 400 });
398
+ throw error;
399
+ }
400
+ };
401
+ const call = {};
402
+ const send = {};
403
+ for (const eventName of handlersByEvent.keys()) {
404
+ Object.defineProperty(call, eventName, {
405
+ value: (input, callOptions) => callEvent(eventName, input, callOptions),
406
+ enumerable: true,
407
+ configurable: true,
408
+ writable: false
409
+ });
410
+ Object.defineProperty(send, eventName, {
411
+ value: (input, sendOptions) => sendEvent(eventName, input, sendOptions),
412
+ enumerable: true,
413
+ configurable: true,
414
+ writable: false
415
+ });
416
+ }
417
+ return {
418
+ id,
419
+ session,
420
+ call,
421
+ send,
422
+ state: readState,
423
+ fetch: handleFetch
424
+ };
425
+ };
426
+ return {
427
+ name,
428
+ contract: contract$1,
429
+ server,
430
+ reducer,
431
+ actor: makeHandle
432
+ };
433
+ }
434
+ //#endregion
435
+ export { ActorRefusedError, actor, concurrent };
436
+
437
+ //# sourceMappingURL=actor.js.map