iterate 0.2.7 → 0.3.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 (218) hide show
  1. package/README.md +86 -76
  2. package/THIRD_PARTY_NOTICES.md +55 -0
  3. package/bin/iterate.js +11 -3
  4. package/dist/api-url-B6404M82.mjs +17 -0
  5. package/dist/api-url-B6404M82.mjs.map +1 -0
  6. package/dist/app-ref-BipL0feU.mjs +35 -0
  7. package/dist/app-ref-BipL0feU.mjs.map +1 -0
  8. package/dist/app-ref-C1CrgXqX.mjs +7 -0
  9. package/dist/app-ref-C1CrgXqX.mjs.map +1 -0
  10. package/dist/app-ref-DYai_om1.mjs +7 -0
  11. package/dist/app-ref-DYai_om1.mjs.map +1 -0
  12. package/dist/cli-D0c-pDL_.mjs +1010 -0
  13. package/dist/cli-D0c-pDL_.mjs.map +1 -0
  14. package/dist/client.d.ts +3 -0
  15. package/dist/client.mjs +4 -0
  16. package/dist/cloudflare-BTm90gQ4.mjs +951 -0
  17. package/dist/cloudflare-BTm90gQ4.mjs.map +1 -0
  18. package/dist/contract-s4FW4eES.mjs +309 -0
  19. package/dist/contract-s4FW4eES.mjs.map +1 -0
  20. package/dist/document-review/index.d.ts +5 -0
  21. package/dist/document-review/types.d.ts +107 -0
  22. package/dist/document-review.mjs +7015 -0
  23. package/dist/document-review.mjs.map +1 -0
  24. package/dist/durable-object-processor-durability-CNsTjAJS.mjs +205 -0
  25. package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +1 -0
  26. package/dist/idempotency-DleloJNt.mjs +28 -0
  27. package/dist/idempotency-DleloJNt.mjs.map +1 -0
  28. package/dist/index.mjs +1 -1
  29. package/dist/itx/api-url.d.ts +6 -0
  30. package/dist/itx/itx-node-client.d.ts +65 -0
  31. package/dist/itx/itx-session.d.ts +215 -0
  32. package/dist/itx/owned-rpc-session.d.ts +14 -0
  33. package/dist/itx/query-client.d.ts +10 -0
  34. package/dist/itx-api.generated.d.ts +6195 -0
  35. package/dist/itx-session-sjud8GiT.mjs +534 -0
  36. package/dist/itx-session-sjud8GiT.mjs.map +1 -0
  37. package/dist/live-state-BJNqOwFw.mjs +299 -0
  38. package/dist/live-state-BJNqOwFw.mjs.map +1 -0
  39. package/dist/next/api.d.ts +479 -0
  40. package/dist/next/api.mjs +0 -0
  41. package/dist/next/app-server.d.ts +44 -0
  42. package/dist/next/app-server.mjs +479 -0
  43. package/dist/next/app-server.mjs.map +1 -0
  44. package/dist/next/app-session.d.ts +49 -0
  45. package/dist/next/app-session.mjs +238 -0
  46. package/dist/next/app-session.mjs.map +1 -0
  47. package/dist/next/app.d.ts +29 -0
  48. package/dist/next/app.mjs +141 -0
  49. package/dist/next/app.mjs.map +1 -0
  50. package/dist/next/client/live-state.d.ts +63 -0
  51. package/dist/next/client/oauth.d.ts +12 -0
  52. package/dist/next/client/react.d.ts +109 -0
  53. package/dist/next/client/socket.d.ts +6 -0
  54. package/dist/next/client.mjs +156 -0
  55. package/dist/next/client.mjs.map +1 -0
  56. package/dist/next/expression.d.ts +146 -0
  57. package/dist/next/expression.mjs +399 -0
  58. package/dist/next/expression.mjs.map +1 -0
  59. package/dist/next/lib.d.ts +56 -0
  60. package/dist/next/lib.mjs +199 -0
  61. package/dist/next/lib.mjs.map +1 -0
  62. package/dist/next/oauth-scopes.d.ts +32 -0
  63. package/dist/next/oauth-scopes.mjs +40 -0
  64. package/dist/next/oauth-scopes.mjs.map +1 -0
  65. package/dist/next/oauth.mjs +29 -0
  66. package/dist/next/oauth.mjs.map +1 -0
  67. package/dist/next/principal.d.ts +64 -0
  68. package/dist/next/principal.mjs +98 -0
  69. package/dist/next/principal.mjs.map +1 -0
  70. package/dist/next/project-ingress.d.ts +37 -0
  71. package/dist/next/project-ingress.mjs +75 -0
  72. package/dist/next/project-ingress.mjs.map +1 -0
  73. package/dist/next/react.mjs +285 -0
  74. package/dist/next/react.mjs.map +1 -0
  75. package/dist/next/sdk/auth.d.ts +5 -0
  76. package/dist/next/sdk/index.d.ts +112 -0
  77. package/dist/next/sdk.mjs +139 -0
  78. package/dist/next/sdk.mjs.map +1 -0
  79. package/dist/next/stream/processor.d.ts +378 -0
  80. package/dist/next/stream/processor.mjs +582 -0
  81. package/dist/next/stream/processor.mjs.map +1 -0
  82. package/dist/next/stream/run.d.ts +58 -0
  83. package/dist/next/stream/run.mjs +40 -0
  84. package/dist/next/stream/run.mjs.map +1 -0
  85. package/dist/next-node.d.ts +15 -0
  86. package/dist/next-node.mjs +51 -0
  87. package/dist/next-node.mjs.map +1 -0
  88. package/dist/node.d.ts +3 -0
  89. package/dist/node.mjs +185 -0
  90. package/dist/node.mjs.map +1 -0
  91. package/dist/processor-host-capabilities-BMFH3KTM.mjs +56 -0
  92. package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +1 -0
  93. package/dist/processors/cloudflare.d.ts +3 -0
  94. package/dist/processors/durable-object-processor-durability.d.ts +79 -0
  95. package/dist/processors/event-consumption-metrics.d.ts +82 -0
  96. package/dist/processors/idempotency.d.ts +13 -0
  97. package/dist/processors/index.d.ts +12 -0
  98. package/dist/processors/processor-contracts.d.ts +342 -0
  99. package/dist/processors/processor-facet.d.ts +186 -0
  100. package/dist/processors/processor-host-capabilities.d.ts +60 -0
  101. package/dist/processors/prompt-sections.d.ts +17 -0
  102. package/dist/processors/rpc-types.d.ts +515 -0
  103. package/dist/processors/schemas.d.ts +102 -0
  104. package/dist/processors/stream-handle.d.ts +45 -0
  105. package/dist/processors/stream-processor-keepalive.d.ts +95 -0
  106. package/dist/processors/stream-processor-registry.d.ts +233 -0
  107. package/dist/processors/stream-processor-runner.d.ts +289 -0
  108. package/dist/processors/stream-processor.d.ts +339 -0
  109. package/dist/processors/stream-runtime-metrics.d.ts +107 -0
  110. package/dist/processors/testing.d.ts +302 -0
  111. package/dist/processors-BoNyeBfQ.mjs +10 -0
  112. package/dist/processors-BoNyeBfQ.mjs.map +1 -0
  113. package/dist/processors-cloudflare.mjs +3 -0
  114. package/dist/processors-testing.mjs +435 -0
  115. package/dist/processors-testing.mjs.map +1 -0
  116. package/dist/processors.mjs +52 -0
  117. package/dist/processors.mjs.map +1 -0
  118. package/dist/protocol-DnK_f2m6.mjs +251 -0
  119. package/dist/protocol-DnK_f2m6.mjs.map +1 -0
  120. package/dist/sdk/capnweb/index.d.ts +2 -0
  121. package/dist/sdk/capnweb/live-state/compact.d.ts +5 -0
  122. package/dist/sdk/capnweb/live-state/diff.d.ts +41 -0
  123. package/dist/sdk/capnweb/live-state/engine.d.ts +44 -0
  124. package/dist/sdk/capnweb/live-state/index.d.ts +41 -0
  125. package/dist/sdk/capnweb/live-state/protocol.d.ts +87 -0
  126. package/dist/sdk/capnweb/live-state/retain.d.ts +23 -0
  127. package/dist/sdk/capnweb/live-state/store.d.ts +20 -0
  128. package/dist/sdk/capnweb/live-state/types.d.ts +11 -0
  129. package/dist/sdk/capnweb/react.d.ts +45 -0
  130. package/dist/sdk/capnweb/react.mjs +316 -0
  131. package/dist/sdk/capnweb/react.mjs.map +1 -0
  132. package/dist/sdk/capnweb.mjs +4 -0
  133. package/dist/sdk/itx/react.d.ts +191 -0
  134. package/dist/sdk/itx/react.mjs +383 -0
  135. package/dist/sdk/itx/react.mjs.map +1 -0
  136. package/dist/sdk-DMB-IM11.mjs +933 -0
  137. package/dist/sdk-DMB-IM11.mjs.map +1 -0
  138. package/dist/sdk.d.ts +339 -0
  139. package/dist/sdk.mjs +2 -0
  140. package/dist/serve-itx.d.ts +46 -0
  141. package/dist/starter-apps/flake-dashboard/app-ref.d.ts +31 -0
  142. package/dist/starter-apps/flake-dashboard/configured-worker.mjs +1055 -0
  143. package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +1 -0
  144. package/dist/starter-apps/flake-dashboard/contract.d.ts +4839 -0
  145. package/dist/starter-apps/flake-dashboard/contract.mjs +2 -0
  146. package/dist/starter-apps/flake-dashboard/index.d.ts +17 -0
  147. package/dist/starter-apps/flake-dashboard/index.mjs +56 -0
  148. package/dist/starter-apps/flake-dashboard/index.mjs.map +1 -0
  149. package/dist/starter-apps/flake-dashboard/worker.d.ts +4607 -0
  150. package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +8914 -0
  151. package/dist/starter-apps/github-ai-linter/configured-worker.mjs +17987 -0
  152. package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +1 -0
  153. package/dist/starter-apps/github-ai-linter/contract.d.ts +9193 -0
  154. package/dist/starter-apps/github-ai-linter/index.d.ts +10 -0
  155. package/dist/starter-apps/github-ai-linter/index.mjs +36 -0
  156. package/dist/starter-apps/github-ai-linter/index.mjs.map +1 -0
  157. package/dist/starter-apps/github-ai-linter/prompt.d.ts +13 -0
  158. package/dist/starter-apps/github-ai-linter/review-bot.d.ts +808 -0
  159. package/dist/starter-apps/github-ai-linter/rules.d.ts +34 -0
  160. package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +19 -0
  161. package/dist/starter-apps/github-ai-linter/worker.d.ts +19 -0
  162. package/dist/starter-apps/github-ai-linter/worker.mjs +947 -0
  163. package/dist/starter-apps/github-ai-linter/worker.mjs.map +1 -0
  164. package/dist/starter-apps/guestbook/app-ref.d.ts +27 -0
  165. package/dist/starter-apps/guestbook/client.d.ts +7 -0
  166. package/dist/starter-apps/guestbook/client.mjs +59 -0
  167. package/dist/starter-apps/guestbook/configured-worker.mjs +205 -0
  168. package/dist/starter-apps/guestbook/configured-worker.mjs.map +1 -0
  169. package/dist/starter-apps/guestbook/index.d.ts +9 -0
  170. package/dist/starter-apps/guestbook/index.mjs +31 -0
  171. package/dist/starter-apps/guestbook/index.mjs.map +1 -0
  172. package/dist/starter-apps/guestbook/processor.d.ts +2267 -0
  173. package/dist/starter-apps/guestbook/worker.d.ts +26 -0
  174. package/dist/starter-apps/guestbook/worker.mjs +191 -0
  175. package/dist/starter-apps/guestbook/worker.mjs.map +1 -0
  176. package/dist/starter-apps/media/configured-worker.mjs +577 -0
  177. package/dist/starter-apps/media/configured-worker.mjs.map +1 -0
  178. package/dist/starter-apps/media/index.mjs +36 -0
  179. package/dist/starter-apps/media/index.mjs.map +1 -0
  180. package/dist/starter-apps/media/ref.mjs +20 -0
  181. package/dist/starter-apps/media/ref.mjs.map +1 -0
  182. package/dist/starter-apps/media/worker.mjs +579 -0
  183. package/dist/starter-apps/media/worker.mjs.map +1 -0
  184. package/dist/starter-apps/notes/configured-worker.mjs +6134 -0
  185. package/dist/starter-apps/notes/configured-worker.mjs.map +1 -0
  186. package/dist/starter-apps/notes/index.mjs +23 -0
  187. package/dist/starter-apps/notes/index.mjs.map +1 -0
  188. package/dist/starter-apps/notes/ref.mjs +21 -0
  189. package/dist/starter-apps/notes/ref.mjs.map +1 -0
  190. package/dist/starter-apps/notes/worker.mjs +427 -0
  191. package/dist/starter-apps/notes/worker.mjs.map +1 -0
  192. package/dist/starter-apps/todo/client.mjs +59 -0
  193. package/dist/starter-apps/todo/configured-worker.mjs +2864 -0
  194. package/dist/starter-apps/todo/configured-worker.mjs.map +1 -0
  195. package/dist/starter-apps/todo/index.d.ts +8 -0
  196. package/dist/starter-apps/todo/index.mjs +29 -0
  197. package/dist/starter-apps/todo/index.mjs.map +1 -0
  198. package/dist/stream-processor-keepalive-DAQTP6m3.mjs +2082 -0
  199. package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +1 -0
  200. package/dist/usingCtx-inzbY1Qz.mjs +57 -0
  201. package/dist/usingCtx-mZx5nsAW.mjs +11800 -0
  202. package/dist/usingCtx-mZx5nsAW.mjs.map +1 -0
  203. package/dist/worker-ref-DZxPDmb_.mjs +390 -0
  204. package/dist/worker-ref-DZxPDmb_.mjs.map +1 -0
  205. package/menubar/Iterate.entitlements +12 -0
  206. package/menubar/Iterate.swift +914 -0
  207. package/menubar/IterateIcon.swift +145 -0
  208. package/menubar/README.md +28 -0
  209. package/menubar/build-menubar-app.sh +59 -0
  210. package/package.json +235 -18
  211. package/dist/cli-DMS4kJph.mjs +0 -868
  212. package/dist/cli-DMS4kJph.mjs.map +0 -1
  213. package/dist/config-DtnR7Lv7.mjs +0 -170
  214. package/dist/config-DtnR7Lv7.mjs.map +0 -1
  215. package/dist/index.d.mts.map +0 -1
  216. package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
  217. package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
  218. package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.mjs","names":["#ensureInitialized","guestbookClientSource","RpcTarget","#app","#liveState"],"sources":["../../../src/starter-apps/guestbook/processor.ts","../../../src/starter-apps/guestbook/worker.ts"],"sourcesContent":["// The guestbook's stream processor: it folds the signatures on the project\n// stream at /guestbook into a birth certificate plus an append-only list of\n// entries. The reduce is the whole processor — pure fold, no side effects.\nimport { z } from \"zod\";\nimport { defineProcessorContract, StreamProcessor } from \"../../processors/index.ts\";\nimport type { ProcessorState, ReduceArgs } from \"../../processors/index.ts\";\n\nexport const GuestbookProcessorContract = defineProcessorContract({\n slug: \"guestbook\",\n version: \"0.1.0\",\n description: \"Reduces guestbook signatures on /guestbook.\",\n stateSchema: z.object({\n birthCertificate: z\n .object({\n config: z.object({\n title: z.string().meta({ description: \"Display title shown on the public page.\" }),\n }),\n })\n .nullable()\n .default(null)\n .meta({\n description: \"Existence marker: null until guestbook/created reduces. Signing requires it.\",\n }),\n entries: z\n .array(\n z.object({\n name: z.string().meta({ description: \"Signer display name.\" }),\n message: z.string().meta({ description: \"Note left by the signer.\" }),\n signedAt: z\n .string()\n .meta({ description: \"ISO-8601 time from the stream event createdAt.\" }),\n }),\n )\n .default([])\n .meta({ description: \"Signatures in stream order (oldest first).\" }),\n }),\n events: {\n \"events.iterate.com/guestbook/created\": {\n description:\n \"The guestbook exists: its birth certificate, the first event in its domain history. \" +\n \"Appended (idempotency-keyed) by whoever signs first or opens the API.\",\n payloadSchema: z.object({\n config: z\n .object({\n title: z.string().meta({ description: \"Display title for the guestbook.\" }),\n })\n .meta({ description: \"Initial configuration.\" }),\n }),\n examples: [\n {\n description: \"A guestbook born with its display title.\",\n payload: { config: { title: \"Guestbook\" } },\n },\n ],\n },\n \"events.iterate.com/guestbook/entry-signed\": {\n description: \"Someone signed the guestbook: their name and message.\",\n payloadSchema: z.object({\n name: z.string().trim().min(1).meta({ description: \"Signer display name.\" }),\n message: z.string().trim().min(1).meta({ description: \"Note left by the signer.\" }),\n }),\n examples: [\n {\n description: \"A visitor left a note.\",\n payload: { name: \"Ada\", message: \"Lovely worker you have here.\" },\n },\n ],\n },\n },\n consumes: [\"events.iterate.com/guestbook/created\", \"events.iterate.com/guestbook/entry-signed\"],\n emits: [],\n});\nexport type GuestbookProcessorContract = typeof GuestbookProcessorContract;\n\nexport type GuestbookState = ProcessorState<GuestbookProcessorContract>;\n\nexport class GuestbookProcessor extends StreamProcessor<GuestbookProcessorContract> {\n readonly contract = GuestbookProcessorContract;\n\n protected override reduce({ event, state }: ReduceArgs<GuestbookProcessorContract>) {\n switch (event.type) {\n case \"events.iterate.com/guestbook/created\":\n // Idempotency-keyed at the source, but a duplicate that slips through\n // folds to a no-op rather than wedging the frame.\n if (state.birthCertificate !== null) return state;\n return { ...state, birthCertificate: event.payload };\n case \"events.iterate.com/guestbook/entry-signed\":\n return {\n ...state,\n entries: [...state.entries, { ...event.payload, signedAt: event.createdAt }],\n };\n default:\n return state;\n }\n }\n}\n","/// <reference path=\"./virtual-client.d.ts\" />\nimport guestbookClientSource from \"iterate:guestbook-client-source\";\nimport {\n LiveStateRpcTarget,\n RpcTarget,\n newWorkersWebSocketRpcResponse,\n type LiveStateRpc,\n} from \"../../sdk/capnweb/index.ts\";\nimport type { StreamProcessorRegistry } from \"../../processors/cloudflare.ts\";\nimport {\n StreamProcessorDurableObject,\n type ProcessorHostDeps,\n type StreamEvent,\n} from \"../../sdk.ts\";\nimport { guestbookCreationEvents, guestbookStreamPath } from \"./app-ref.ts\";\nimport { GuestbookProcessor, type GuestbookState } from \"./processor.ts\";\n\n/** One packaged Durable Object owns the page, API, processor, and live value.\n * `alarm()`, the `processor` wake door, `snapshot()`, and `registry()` come\n * from {@link StreamProcessorDurableObject}; all this class writes is how to\n * build its processor plus its own HTTP + append verbs. */\nexport class GuestbookApp extends StreamProcessorDurableObject<GuestbookState> {\n protected readonly streamPath = guestbookStreamPath;\n protected createProcessor(deps: ProcessorHostDeps) {\n return new GuestbookProcessor(deps);\n }\n\n /** Lazily initialize the stream. The idempotency-keyed creation fact may be\n * offered by every caller. */\n async #ensureInitialized(): Promise<StreamProcessorRegistry<GuestbookState>> {\n const registry = await this.registry();\n await registry.catchUp(\"guestbook\");\n using project = await this.env.ITX.get();\n await project.streams.get(guestbookStreamPath).append(...guestbookCreationEvents());\n await registry.catchUp(\"guestbook\");\n return registry;\n }\n\n /** Project-worker event delivery calls this after a durable Guestbook event\n * commits. Catch-up owns validation, ordering, checkpointing, and dedupe. */\n async syncEvent(event: StreamEvent): Promise<void> {\n if (event.path !== guestbookStreamPath) return;\n const registry = await this.#ensureInitialized();\n await registry.catchUp(\"guestbook\");\n registry.refreshLive();\n }\n\n /** Internal worker RPC surface used to verify source upgrades and delivery. */\n async getState(): Promise<GuestbookState> {\n await this.#ensureInitialized();\n return (await this.snapshot()).state;\n }\n\n async sign(name: string, message: string): Promise<void> {\n const trimmedName = name.trim().slice(0, 80);\n const trimmedMessage = message.trim().slice(0, 500);\n if (trimmedName.length === 0 || trimmedMessage.length === 0) {\n throw new TypeError(\"Name and message are required\");\n }\n const registry = await this.#ensureInitialized();\n using project = await this.env.ITX.get();\n await project.streams.get(guestbookStreamPath).append({\n type: \"events.iterate.com/guestbook/entry-signed\",\n payload: { message: trimmedMessage, name: trimmedName },\n idempotencyKey: `guestbook/entry:${crypto.randomUUID()}`,\n });\n await registry.catchUp(\"guestbook\");\n registry.refreshLive();\n }\n\n async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url);\n if (url.pathname === \"/api\") {\n const registry = await this.#ensureInitialized();\n await registry.loadAndRefreshLive();\n return newWorkersWebSocketRpcResponse(request, new GuestbookApi(this, registry));\n }\n if (request.method === \"GET\" && url.pathname === \"/apps/guestbook/client.js\") {\n return new Response(guestbookClientSource, {\n headers: {\n \"content-type\": \"application/javascript; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\",\n },\n });\n }\n if (request.method !== \"GET\" || url.pathname !== \"/\") {\n return new Response(\"not found\", { status: 404 });\n }\n return new Response(\n `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Guestbook</title>\n <style>\n :root { color-scheme: light dark; font-family: system-ui, sans-serif; }\n body { margin: 0; padding: 2rem; }\n main { margin: 0 auto; max-width: 38rem; }\n form { display: grid; gap: .75rem; }\n input, textarea, button { font: inherit; padding: .6rem; }\n article { border-block-start: 1px solid #8886; margin-block-start: 1.25rem; padding-block-start: 1rem; }\n time { opacity: .65; }\n [role=\"alert\"] { color: #c33; }\n </style>\n </head>\n <body>\n <main id=\"root\"><p>Loading…</p></main>\n <script type=\"module\" src=\"/apps/guestbook/client.js\"></script>\n </body>\n</html>`,\n {\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"x-content-type-options\": \"nosniff\",\n },\n },\n );\n }\n}\n\nexport class GuestbookApi extends RpcTarget {\n readonly #app: GuestbookApp;\n readonly #liveState: LiveStateRpcTarget<GuestbookState>;\n\n constructor(app: GuestbookApp, registry: StreamProcessorRegistry<GuestbookState>) {\n super();\n this.#app = app;\n this.#liveState = new LiveStateRpcTarget(registry);\n }\n\n get liveState(): LiveStateRpc<GuestbookState> {\n return this.#liveState;\n }\n\n async sign(name: string, message: string): Promise<void> {\n await this.#app.sign(name, message);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,577 @@
1
+ import { _ as object, f as array, g as number, l as defineProcessorContract, o as StreamProcessor, p as boolean, r as StreamProcessorDurableObject, t as _usingCtx, v as record, y as string } from "../../usingCtx-mZx5nsAW.mjs";
2
+ import { t as isIdempotencyConflict } from "../../processors-BoNyeBfQ.mjs";
3
+ //#region src/starter-apps/media/analysis.ts
4
+ /** Sees the pixels: transcribes text verbatim and picks tags. */
5
+ const MEDIA_VISION_MODEL = "@cf/meta/llama-4-scout-17b-16e-instruct";
6
+ /**
7
+ * Starter taxonomy — expect churn. Multi-tag with overlap allowed; the model
8
+ * may also coin up to two novel kebab-case tags, and returning NO tags is an
9
+ * acceptable answer (deliberately conservative — early dogfood tagged
10
+ * everything `bug-report` on wild guesses).
11
+ */
12
+ const MEDIA_TAG_TAXONOMY = [
13
+ {
14
+ tag: "screenshot",
15
+ hint: "a captured device screen: app UI, web page, status bar visible"
16
+ },
17
+ {
18
+ tag: "photo",
19
+ hint: "a camera photograph of the physical world"
20
+ },
21
+ {
22
+ tag: "transient",
23
+ hint: "one-off info with no lasting value: OTP codes, confirmations"
24
+ },
25
+ {
26
+ tag: "clipping",
27
+ hint: "a saved post, article, meme, or quote worth keeping"
28
+ },
29
+ {
30
+ tag: "logistics",
31
+ hint: "tickets, bookings, schedules, travel details, QR codes"
32
+ },
33
+ {
34
+ tag: "receipt",
35
+ hint: "proof of purchase, invoices, order confirmations"
36
+ },
37
+ {
38
+ tag: "conversation",
39
+ hint: "chat threads, DMs, emails, comment sections"
40
+ },
41
+ {
42
+ tag: "code",
43
+ hint: "code, terminals, dev tools, technical docs"
44
+ },
45
+ {
46
+ tag: "reference",
47
+ hint: "info to keep long-term: settings, lists, instructions"
48
+ }
49
+ ];
50
+ /**
51
+ * One full analysis of a stored media file. Throws on conversion failure so
52
+ * the caller (the processor's obligation attempt) owns retry/settlement; an
53
+ * unparseable vision answer degrades to empty title/transcript and
54
+ * ["untagged"] so failures stay visible instead of hiding an item.
55
+ */
56
+ async function analyzeMediaImage(session, input) {
57
+ const bytes = await session.files.get(input.path).bytes();
58
+ const described = await session.ai.toMarkdown({
59
+ name: input.filename,
60
+ blob: bytes
61
+ });
62
+ if (described.format === "error") throw new Error(`toMarkdown failed for ${input.filename}: ${described.error}`);
63
+ const markdown = (described.data || "").trim();
64
+ let visionBytes = bytes;
65
+ let visionType = input.contentType;
66
+ if (bytes.length > 1e6) try {
67
+ const resized = await session.integrations.cf.images.transformBytes({
68
+ image: bytes,
69
+ transforms: [{ width: 1280 }],
70
+ output: {
71
+ format: "image/jpeg",
72
+ quality: 80
73
+ }
74
+ });
75
+ visionBytes = resized.bytes;
76
+ visionType = resized.contentType;
77
+ } catch {}
78
+ let binary = "";
79
+ for (let i = 0; i < visionBytes.length; i += 32768) binary += String.fromCharCode(...visionBytes.subarray(i, i + 32768));
80
+ return {
81
+ ...parseVisionAnswer(visionAnswerText(await session.ai.run(MEDIA_VISION_MODEL, {
82
+ messages: [{
83
+ role: "user",
84
+ content: [{
85
+ type: "text",
86
+ text: mediaVisionPrompt()
87
+ }, {
88
+ type: "image_url",
89
+ image_url: { url: `data:${visionType};base64,${btoa(binary)}` }
90
+ }]
91
+ }],
92
+ max_tokens: 1024
93
+ }))),
94
+ markdown
95
+ };
96
+ }
97
+ /** Both Workers AI answer shapes: `{ response }` and OpenAI-style choices. */
98
+ function visionAnswerText(answer) {
99
+ const bag = answer;
100
+ if (typeof bag?.response === "string") return bag.response;
101
+ const content = bag?.choices?.[0]?.message?.content;
102
+ return typeof content === "string" ? content : "";
103
+ }
104
+ /** Extract the JSON object from a chatty model answer, defensively. */
105
+ function parseVisionAnswer(text) {
106
+ let title = "";
107
+ let transcript = "";
108
+ let tags = ["untagged"];
109
+ const match = text.match(/\{[\s\S]*\}/);
110
+ if (match) try {
111
+ const parsed = JSON.parse(match[0]);
112
+ if (typeof parsed.title === "string") title = parsed.title.trim().slice(0, 120);
113
+ if (typeof parsed.transcript === "string") transcript = parsed.transcript.trim();
114
+ if (Array.isArray(parsed.tags)) tags = [...new Set(parsed.tags.filter((tag) => typeof tag === "string").map((tag) => tag.toLowerCase().trim().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "")).filter((tag) => tag.length > 0 && tag.length <= 30))].slice(0, 6);
115
+ } catch {}
116
+ return {
117
+ title,
118
+ transcript,
119
+ tags,
120
+ processedBy: MEDIA_VISION_MODEL
121
+ };
122
+ }
123
+ function mediaVisionPrompt() {
124
+ return [
125
+ "Reply with ONLY a JSON object: {\"title\": string, \"transcript\": string, \"tags\": string[]}.",
126
+ "title: ONE line saying what the image IS, specific not generic — 'Trenitalia ticket Rome→Florence 09:45', never 'Screenshot' or 'Image Description'.",
127
+ "transcript: ALL text visible in the image, verbatim, reading order. Empty string if there is none.",
128
+ "tags: pick from the list below. Include a tag ONLY when the image clearly shows it — no guesses.",
129
+ "Fewer tags is better; an empty array is a fine answer. Overlap is allowed.",
130
+ ...MEDIA_TAG_TAXONOMY.map(({ tag, hint }) => `- "${tag}": ${hint}`),
131
+ "You may add at most 2 extra kebab-case tags if something important has no tag above."
132
+ ].join("\n");
133
+ }
134
+ //#endregion
135
+ //#region src/starter-apps/media/ref.ts
136
+ const mediaStreamPath = "/media";
137
+ //#endregion
138
+ //#region src/starter-apps/media/processor.ts
139
+ /**
140
+ * An analysis obligation is abandoned (settled as failed, without dialing
141
+ * the AI) when its requesting event is older than this — the staleness
142
+ * doctrine's horizon. Generous on purpose: a phone can upload while the
143
+ * analyzer is broken and still get analyzed when the fix deploys; Re-analyze
144
+ * covers anything older.
145
+ */
146
+ const MEDIA_ANALYSIS_EXPIRY_MS = 1440 * 60 * 1e3;
147
+ /** Tries per attempt, with backoff between them — covers the transient
148
+ * Workers AI failure class ("8005: Internal server error") observed in prod
149
+ * without an extra durable event per retry. */
150
+ const ANALYSIS_TRIES = 3;
151
+ const ANALYSIS_RETRY_BACKOFF_MS = [2e3, 8e3];
152
+ /**
153
+ * Attempts started per caught-up pass, matching the phone's old 3-wide
154
+ * pipeline. A 19-item Delete-all re-sync once fired 19 parallel vision
155
+ * pipelines from one pass and Workers AI answered with 8004s and 60s
156
+ * binding timeouts — the in-attempt retries all landed inside the same
157
+ * overloaded burst. The rest of the queue needs no timer: every settlement
158
+ * append comes back through this processor's own delivery and the next
159
+ * caught-up pass starts the next wave.
160
+ */
161
+ const MAX_CONCURRENT_ANALYSES = 3;
162
+ const processingFields = {
163
+ title: string().default("").meta({ description: "One-line description of what the image IS (pre-title events fold to empty)." }),
164
+ markdown: string().meta({ description: "Vision-model description — half the search corpus." }),
165
+ transcript: string().meta({ description: "Verbatim OCR of visible text — the other half." }),
166
+ tags: array(string()).meta({ description: "Conservative multi-label tags." }),
167
+ processedBy: string().meta({ description: "Model id that produced this processing." })
168
+ };
169
+ const fileFields = {
170
+ stableKey: string().meta({ description: "Content hash — the item's identity." }),
171
+ path: string().meta({ description: "itx.files path holding the bytes." }),
172
+ filename: string().meta({ description: "Original filename as picked/synced." }),
173
+ contentType: string(),
174
+ width: number(),
175
+ height: number(),
176
+ source: string().default("picker").meta({ description: "\"picker\", \"library-sync\", or \"note\" (a note attachment)." }),
177
+ capturedAt: string().nullable().default(null).meta({ description: "Asset creation time (ISO) when the source knew it." }),
178
+ isScreenshot: boolean().nullable().default(null).meta({ description: "iOS mediaSubtypes screenshot flag." })
179
+ };
180
+ const MediaItem = object({
181
+ ...fileFields,
182
+ capturedEventAt: string().meta({ description: "Stream time the capture committed." }),
183
+ ...processingFields,
184
+ analysisError: string().nullable().default(null).meta({ description: "The latest analysis settlement's failure, or null. Rows born from media/uploaded exist regardless of analysis outcome; a terminal failure lands here." }),
185
+ settledRequestOffset: number().nullable().default(null).meta({ description: "requestOffset of the last APPLIED settlement — settlements apply monotonically, so a late one answering an older request can never overwrite a newer result (null until a requestOffset-carrying settlement lands; legacy settlements don't advance it)." })
186
+ });
187
+ const PendingAnalysis = object({
188
+ stableKey: string(),
189
+ path: string(),
190
+ filename: string(),
191
+ contentType: string(),
192
+ requestOffset: number().meta({ description: "Offset of the uploaded/reanalyze event that opened this obligation — the settlement's idempotency identity." }),
193
+ expiresAtMs: number().meta({ description: "Epoch ms (request createdAt + MEDIA_ANALYSIS_EXPIRY_MS); past it the obligation settles as expired without dialing the AI." })
194
+ });
195
+ const MediaProcessorContract = defineProcessorContract({
196
+ slug: "media",
197
+ version: "0.2.1",
198
+ description: "Reduces captured media (screenshots/photos) on /media into a searchable index, and drives server-side vision analysis of uploaded items (obligation pattern).",
199
+ stateSchema: object({
200
+ items: record(string(), MediaItem).default({}).meta({ description: "Items by stableKey, latest processing overlaid." }),
201
+ pendingAnalyses: record(string(), PendingAnalysis).default({}).meta({ description: "Open analysis obligations by stableKey (latest request wins); settled by one media/processed event each." })
202
+ }),
203
+ events: {
204
+ "events.iterate.com/media/uploaded": {
205
+ description: "A media item's bytes landed in project files: the phone appends this cheap durable fact right after files.put (metadata only — no analysis yet), idempotency-keyed by content hash + wipe generation, sharing the media/captured key scheme so an item already captured never re-uploads as a duplicate. Opens an analysis obligation.",
206
+ payloadSchema: object(fileFields),
207
+ examples: [{
208
+ description: "A synced screenshot, uploaded and awaiting analysis.",
209
+ payload: {
210
+ stableKey: "abc123",
211
+ path: "/media/abc123-IMG_0001.png",
212
+ filename: "IMG_0001.png",
213
+ contentType: "image/png",
214
+ width: 1170,
215
+ height: 2532,
216
+ source: "library-sync",
217
+ capturedAt: "2026-08-10T09:00:00.000Z",
218
+ isScreenshot: true
219
+ }
220
+ }]
221
+ },
222
+ "events.iterate.com/media/reanalyze-requested": {
223
+ description: "Re-analyze an existing item (the app's Re-analyze button): opens an analysis obligation for it; the settlement overlays the item's processing fields.",
224
+ payloadSchema: object({ stableKey: string() }),
225
+ examples: [{
226
+ description: "A user-requested re-run.",
227
+ payload: { stableKey: "abc123" }
228
+ }]
229
+ },
230
+ "events.iterate.com/media/captured": {
231
+ description: "LEGACY (pre media/uploaded): a media item entered the project with its first vision processing inline, appended by the old phone-driven capture script. Still folded so history keeps rendering; nothing appends it anymore.",
232
+ payloadSchema: object({
233
+ ...fileFields,
234
+ ...processingFields
235
+ }),
236
+ examples: [{
237
+ description: "A synced screenshot with its first processing.",
238
+ payload: {
239
+ stableKey: "abc123",
240
+ path: "/media/abc123-IMG_0001.png",
241
+ filename: "IMG_0001.png",
242
+ contentType: "image/png",
243
+ width: 1170,
244
+ height: 2532,
245
+ source: "library-sync",
246
+ capturedAt: "2026-08-10T09:00:00.000Z",
247
+ isScreenshot: true,
248
+ markdown: "A train ticket from Rome to Florence.",
249
+ transcript: "Trenitalia 09:45",
250
+ tags: ["screenshot", "logistics"],
251
+ processedBy: "@cf/meta/llama-4-scout-17b-16e-instruct"
252
+ }
253
+ }]
254
+ },
255
+ "events.iterate.com/media/wiped": {
256
+ description: "Everything before this tombstone is gone: the wipe script deleted the stored files and every derivation (phone list, this fold) resets. Deliberate per-invocation idempotency key.",
257
+ payloadSchema: object({
258
+ deletedFiles: number().meta({ description: "Files actually deleted." }),
259
+ items: number().meta({ description: "Items that existed at wipe time." })
260
+ }),
261
+ examples: [{
262
+ description: "A from-scratch reset.",
263
+ payload: {
264
+ deletedFiles: 24,
265
+ items: 24
266
+ }
267
+ }]
268
+ },
269
+ "events.iterate.com/media/processed": {
270
+ description: "One analysis settlement (the obligation's single terminal event): on success the processing fields overlay the item's, `error: null`; on terminal failure `error` says why and the item's previous fields stay. Also appended by legacy phone-driven re-analysis (no error/requestOffset).",
271
+ payloadSchema: object({
272
+ stableKey: string(),
273
+ ...processingFields,
274
+ error: string().nullable().default(null).meta({ description: "Terminal analysis failure, or null on success." }),
275
+ requestOffset: number().nullable().default(null).meta({ description: "The uploaded/reanalyze event this settles (null on legacy appends)." })
276
+ }),
277
+ examples: [{
278
+ description: "A successful server-side analysis of an uploaded item.",
279
+ payload: {
280
+ stableKey: "abc123",
281
+ title: "Trenitalia ticket Rome→Florence 09:45",
282
+ markdown: "A train ticket from Rome to Florence.",
283
+ transcript: "Trenitalia 09:45",
284
+ tags: ["screenshot", "logistics"],
285
+ processedBy: "@cf/meta/llama-4-scout-17b-16e-instruct",
286
+ error: null,
287
+ requestOffset: 12
288
+ }
289
+ }]
290
+ }
291
+ },
292
+ consumes: [
293
+ "events.iterate.com/media/uploaded",
294
+ "events.iterate.com/media/reanalyze-requested",
295
+ "events.iterate.com/media/captured",
296
+ "events.iterate.com/media/processed",
297
+ "events.iterate.com/media/wiped"
298
+ ],
299
+ emits: ["events.iterate.com/media/processed"]
300
+ });
301
+ var MediaProcessor = class extends StreamProcessor {
302
+ contract = MediaProcessorContract;
303
+ /**
304
+ * Runtime-only attempts by stableKey. The uploaded/reanalyze +
305
+ * processed events are the durable truth: after an eviction this set is
306
+ * empty and the next caught-up pass restarts any still-open obligation.
307
+ */
308
+ #liveAnalyses = /* @__PURE__ */ new Set();
309
+ reduce({ event, state }) {
310
+ switch (event.type) {
311
+ case "events.iterate.com/media/uploaded":
312
+ if (state.items[event.payload.stableKey] !== void 0) return state;
313
+ return {
314
+ ...state,
315
+ items: {
316
+ ...state.items,
317
+ [event.payload.stableKey]: {
318
+ ...event.payload,
319
+ capturedEventAt: event.createdAt,
320
+ title: "",
321
+ markdown: "",
322
+ transcript: "",
323
+ tags: [],
324
+ processedBy: "",
325
+ analysisError: null,
326
+ settledRequestOffset: null
327
+ }
328
+ },
329
+ pendingAnalyses: {
330
+ ...state.pendingAnalyses,
331
+ [event.payload.stableKey]: pendingAnalysisFor(event)
332
+ }
333
+ };
334
+ case "events.iterate.com/media/reanalyze-requested": {
335
+ const item = state.items[event.payload.stableKey];
336
+ if (item === void 0) return state;
337
+ return {
338
+ ...state,
339
+ pendingAnalyses: {
340
+ ...state.pendingAnalyses,
341
+ [item.stableKey]: {
342
+ stableKey: item.stableKey,
343
+ path: item.path,
344
+ filename: item.filename,
345
+ contentType: item.contentType,
346
+ requestOffset: event.offset,
347
+ expiresAtMs: Date.parse(event.createdAt) + MEDIA_ANALYSIS_EXPIRY_MS
348
+ }
349
+ }
350
+ };
351
+ }
352
+ case "events.iterate.com/media/captured":
353
+ if (state.items[event.payload.stableKey] !== void 0) return state;
354
+ return {
355
+ ...state,
356
+ items: {
357
+ ...state.items,
358
+ [event.payload.stableKey]: {
359
+ ...event.payload,
360
+ capturedEventAt: event.createdAt,
361
+ analysisError: null,
362
+ settledRequestOffset: null
363
+ }
364
+ }
365
+ };
366
+ case "events.iterate.com/media/wiped": return {
367
+ ...state,
368
+ items: {},
369
+ pendingAnalyses: {}
370
+ };
371
+ case "events.iterate.com/media/processed": {
372
+ const pendingEntry = state.pendingAnalyses[event.payload.stableKey];
373
+ const existing = state.items[event.payload.stableKey];
374
+ const requestOffset = event.payload.requestOffset === void 0 ? null : event.payload.requestOffset;
375
+ const settledFloor = Math.max(pendingEntry === void 0 ? 0 : pendingEntry.requestOffset, existing?.settledRequestOffset || 0);
376
+ if (requestOffset !== null && requestOffset < settledFloor) return state;
377
+ const { [event.payload.stableKey]: _settled, ...pendingAnalyses } = state.pendingAnalyses;
378
+ if (existing === void 0) return {
379
+ ...state,
380
+ pendingAnalyses
381
+ };
382
+ const error = event.payload.error || null;
383
+ const settledRequestOffset = requestOffset === null ? existing.settledRequestOffset : requestOffset;
384
+ const item = error === null ? {
385
+ ...existing,
386
+ title: event.payload.title,
387
+ markdown: event.payload.markdown,
388
+ transcript: event.payload.transcript,
389
+ tags: event.payload.tags,
390
+ processedBy: event.payload.processedBy,
391
+ analysisError: null,
392
+ settledRequestOffset
393
+ } : {
394
+ ...existing,
395
+ analysisError: error,
396
+ settledRequestOffset
397
+ };
398
+ return {
399
+ ...state,
400
+ items: {
401
+ ...state.items,
402
+ [event.payload.stableKey]: item
403
+ },
404
+ pendingAnalyses
405
+ };
406
+ }
407
+ default: return state;
408
+ }
409
+ }
410
+ processEvent(args) {
411
+ if (!args.delivery.caughtUp) return;
412
+ const now = this.deps.now();
413
+ const expired = [];
414
+ for (const pending of Object.values(args.state.pendingAnalyses)) {
415
+ if (this.#liveAnalyses.has(pending.stableKey)) continue;
416
+ if (now >= pending.expiresAtMs) {
417
+ expired.push(pending);
418
+ continue;
419
+ }
420
+ if (this.#liveAnalyses.size >= MAX_CONCURRENT_ANALYSES) continue;
421
+ this.#liveAnalyses.add(pending.stableKey);
422
+ args.runInBackground(async () => {
423
+ try {
424
+ const settlement = await this.#attemptAnalysis(pending);
425
+ await this.#appendSettlement(args.append, pending, settlement);
426
+ } finally {
427
+ this.#liveAnalyses.delete(pending.stableKey);
428
+ }
429
+ });
430
+ }
431
+ if (expired.length === 0) return;
432
+ args.blockProcessorWhile(async () => {
433
+ for (const pending of expired) await this.#appendSettlement(args.append, pending, { error: `analysis window expired before it could run (older than ${MEDIA_ANALYSIS_EXPIRY_MS / 36e5}h) — use Re-analyze to retry` });
434
+ });
435
+ }
436
+ /** One attempt = up to {@link ANALYSIS_TRIES} tries with short backoff. */
437
+ async #attemptAnalysis(pending) {
438
+ let lastError = "analysis failed";
439
+ for (let tryIndex = 0; tryIndex < ANALYSIS_TRIES; tryIndex++) {
440
+ if (tryIndex > 0) await this.deps.sleep(ANALYSIS_RETRY_BACKOFF_MS[Math.min(tryIndex - 1, ANALYSIS_RETRY_BACKOFF_MS.length - 1)]);
441
+ try {
442
+ return { result: await this.deps.analyze(pending) };
443
+ } catch (error) {
444
+ lastError = error instanceof Error ? error.message : String(error);
445
+ }
446
+ }
447
+ return { error: lastError.slice(0, 2e3) };
448
+ }
449
+ /**
450
+ * The obligation's single terminal event. The key is state-derived and
451
+ * deterministic per obligation (`@<stableKey>:<requestOffset>`); bodies
452
+ * contain AI output, so two racing incarnations can collide
453
+ * same-key/different-body — the first committed settlement is the
454
+ * authority and the loser tolerates the conflict (ai-linter shape).
455
+ */
456
+ async #appendSettlement(append, pending, outcome) {
457
+ const event = {
458
+ type: "events.iterate.com/media/processed",
459
+ idempotencyKey: this.idempotencyKey(`analysis-settled@${pending.stableKey}:${pending.requestOffset}`),
460
+ payload: {
461
+ stableKey: pending.stableKey,
462
+ requestOffset: pending.requestOffset,
463
+ ..."result" in outcome ? {
464
+ ...outcome.result,
465
+ error: null
466
+ } : {
467
+ title: "",
468
+ markdown: "",
469
+ transcript: "",
470
+ tags: [],
471
+ processedBy: "",
472
+ error: outcome.error
473
+ }
474
+ }
475
+ };
476
+ try {
477
+ await append(event);
478
+ } catch (error) {
479
+ if (!isIdempotencyConflict(error)) throw error;
480
+ }
481
+ }
482
+ };
483
+ function pendingAnalysisFor(event) {
484
+ return {
485
+ stableKey: event.payload.stableKey,
486
+ path: event.payload.path,
487
+ filename: event.payload.filename,
488
+ contentType: event.payload.contentType,
489
+ requestOffset: event.offset,
490
+ expiresAtMs: Date.parse(event.createdAt) + MEDIA_ANALYSIS_EXPIRY_MS
491
+ };
492
+ }
493
+ /**
494
+ * Search semantics shared with the phone's client-side filter
495
+ * (apps/mobile/src/lib/media.ts filterMedia — kept in sync by hand, the two
496
+ * runtimes can't share a module yet): every whitespace-separated term must
497
+ * appear in description, transcript, filename, stored name, or tags; every
498
+ * selected tag must be present. Newest first.
499
+ */
500
+ function searchMediaItems(state, query) {
501
+ const terms = (query.q || "").toLowerCase().split(/\s+/).filter(Boolean);
502
+ const requiredTags = query.tags || [];
503
+ return Object.values(state.items).filter((item) => {
504
+ const storedName = (item.path.split("/").at(-1) || "").replace(/^[0-9a-f]{32,}-/, "");
505
+ const haystack = `${item.title} ${item.markdown} ${item.transcript} ${item.filename} ${storedName} ${item.tags.join(" ")}`.toLowerCase();
506
+ return terms.every((term) => haystack.includes(term)) && requiredTags.every((tag) => item.tags.includes(tag));
507
+ }).sort((a, b) => (b.capturedAt || b.capturedEventAt).localeCompare(a.capturedAt || a.capturedEventAt)).slice(0, query.limit || 20);
508
+ }
509
+ //#endregion
510
+ //#region src/starter-apps/media/worker.ts
511
+ var MediaApp = class extends StreamProcessorDurableObject {
512
+ streamPath = mediaStreamPath;
513
+ /** Analysis obligations must survive eviction: the keepalive revives this
514
+ * DO and the caught-up pass restarts still-open work from reduced state. */
515
+ recovery = true;
516
+ createProcessor(deps) {
517
+ return new MediaProcessor({
518
+ ...deps,
519
+ now: Date.now,
520
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
521
+ analyze: async (input) => {
522
+ try {
523
+ var _usingCtx$1 = _usingCtx();
524
+ return await analyzeMediaImage(_usingCtx$1.u(await this.env.ITX.get()), input);
525
+ } catch (_) {
526
+ _usingCtx$1.e = _;
527
+ } finally {
528
+ _usingCtx$1.d();
529
+ }
530
+ }
531
+ });
532
+ }
533
+ /** Project-worker event delivery calls this after a durable /media event
534
+ * commits. Catch-up owns validation, ordering, checkpointing, and dedupe. */
535
+ async syncEvent(event) {
536
+ if (event.path !== "/media") return;
537
+ const registry = await this.registry();
538
+ await registry.catchUp("media");
539
+ registry.refreshLive();
540
+ }
541
+ /**
542
+ * Keyword search over descriptions, OCR transcripts, filenames, and tags —
543
+ * "train ticket" finds the train ticket because the vision model's prose
544
+ * says so. `tags` all must be present; newest first; default limit 20.
545
+ */
546
+ async search(query) {
547
+ try {
548
+ var _usingCtx3 = _usingCtx();
549
+ await (await this.registry()).catchUp("media");
550
+ const { state } = await this.snapshot();
551
+ const hits = searchMediaItems(state, query);
552
+ const project = _usingCtx3.u(await this.env.ITX.get());
553
+ return await Promise.all(hits.map(async (item) => ({
554
+ ...item,
555
+ url: await project.files.get(item.path).url()
556
+ })));
557
+ } catch (_) {
558
+ _usingCtx3.e = _;
559
+ } finally {
560
+ _usingCtx3.d();
561
+ }
562
+ }
563
+ /** Newest items, no filtering — `search` with an empty query. */
564
+ async list(query) {
565
+ return this.search({ limit: query.limit });
566
+ }
567
+ /** One item by its content hash, or null. */
568
+ async get(stableKey) {
569
+ await (await this.registry()).catchUp("media");
570
+ const { state } = await this.snapshot();
571
+ return state.items[stableKey] || null;
572
+ }
573
+ };
574
+ //#endregion
575
+ export { MediaApp };
576
+
577
+ //# sourceMappingURL=configured-worker.mjs.map