@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.13

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 (424) hide show
  1. package/dist/cli/serve.js +4 -4
  2. package/dist/cli/serve.js.map +1 -1
  3. package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
  4. package/dist/conformance/.generated/can-i-use-browser.js +1 -1
  5. package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
  6. package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
  7. package/dist/conformance/.generated/can-i-use.js +2 -2
  8. package/dist/conformance/.generated/can-i-use.js.map +1 -1
  9. package/dist/serve/bundler.d.ts +13 -8
  10. package/dist/serve/bundler.d.ts.map +1 -1
  11. package/dist/serve/bundler.js +31 -36
  12. package/dist/serve/bundler.js.map +1 -1
  13. package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
  14. package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
  15. package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
  16. package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
  17. package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
  18. package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
  19. package/dist/serve/docs-ui/docs/api-reference.md +1 -1
  20. package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
  21. package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
  22. package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
  23. package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
  24. package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
  25. package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
  26. package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
  27. package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
  28. package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
  29. package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
  30. package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
  31. package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
  32. package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
  33. package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
  34. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
  35. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
  36. package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
  37. package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
  38. package/dist/serve/docs-ui/docs/index.json +349 -29
  39. package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
  40. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
  41. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
  42. package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
  43. package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
  44. package/dist/serve/docs-ui/docs/overview/index.html +1 -1
  45. package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  46. package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  47. package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  48. package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  49. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
  50. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
  51. package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  52. package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
  53. package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  54. package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
  55. package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
  56. package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  57. package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  58. package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  59. package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  60. package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  61. package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  62. package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  63. package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  64. package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  65. package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  66. package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  67. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +168 -76
  68. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +156 -73
  69. package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  70. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
  71. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
  72. package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
  73. package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  74. package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  75. package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  76. package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
  77. package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  78. package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  79. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
  80. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
  81. package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
  82. package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  83. package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  84. package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  85. package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  86. package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  87. package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  88. package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  89. package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  90. package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  91. package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  92. package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  93. package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  94. package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  95. package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  96. package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  97. package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
  98. package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
  99. package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  100. package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
  101. package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  102. package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
  103. package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  104. package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
  105. package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  106. package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
  107. package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
  108. package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
  109. package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  110. package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  111. package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
  112. package/dist/serve/docs-ui/docs/tutorial.md +701 -0
  113. package/dist/serve/docs-ui/llms.txt +3 -1
  114. package/dist/serve/entries/init.d.ts.map +1 -1
  115. package/dist/serve/entries/init.js +3 -0
  116. package/dist/serve/entries/init.js.map +1 -1
  117. package/dist/serve/entries/runtime.d.ts.map +1 -1
  118. package/dist/serve/entries/runtime.js +20 -5
  119. package/dist/serve/entries/runtime.js.map +1 -1
  120. package/dist/serve/entries/worker-runtime.d.ts +2 -2
  121. package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
  122. package/dist/serve/entries/worker-runtime.js +45 -10
  123. package/dist/serve/entries/worker-runtime.js.map +1 -1
  124. package/dist/serve/runtime/chip-config.d.ts +19 -0
  125. package/dist/serve/runtime/chip-config.d.ts.map +1 -0
  126. package/dist/serve/runtime/chip-config.js +19 -0
  127. package/dist/serve/runtime/chip-config.js.map +1 -0
  128. package/dist/serve/runtime/chip-install.d.ts +10 -0
  129. package/dist/serve/runtime/chip-install.d.ts.map +1 -0
  130. package/dist/serve/runtime/chip-install.js +15 -0
  131. package/dist/serve/runtime/chip-install.js.map +1 -0
  132. package/dist/serve/runtime/chip.d.ts +17 -0
  133. package/dist/serve/runtime/chip.d.ts.map +1 -0
  134. package/dist/serve/runtime/chip.js +257 -0
  135. package/dist/serve/runtime/chip.js.map +1 -0
  136. package/dist/serve/runtime/manifest.d.ts +22 -0
  137. package/dist/serve/runtime/manifest.d.ts.map +1 -0
  138. package/dist/serve/runtime/manifest.js +20 -0
  139. package/dist/serve/runtime/manifest.js.map +1 -0
  140. package/dist/serve/runtime/status.d.ts +49 -0
  141. package/dist/serve/runtime/status.d.ts.map +1 -0
  142. package/dist/serve/runtime/status.js +164 -0
  143. package/dist/serve/runtime/status.js.map +1 -0
  144. package/dist/serve/runtime/worker-connection.d.ts +4 -0
  145. package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
  146. package/dist/serve/runtime/worker-connection.js +11 -0
  147. package/dist/serve/runtime/worker-connection.js.map +1 -0
  148. package/dist/serve/runtime/worker-generation.d.ts +14 -0
  149. package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
  150. package/dist/serve/runtime/worker-generation.js +56 -0
  151. package/dist/serve/runtime/worker-generation.js.map +1 -0
  152. package/dist/serve/runtime/worker-replacement.d.ts +18 -0
  153. package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
  154. package/dist/serve/runtime/worker-replacement.js +29 -0
  155. package/dist/serve/runtime/worker-replacement.js.map +1 -0
  156. package/dist/serve/standalone-assets.d.ts +2 -2
  157. package/dist/serve/standalone-assets.d.ts.map +1 -1
  158. package/dist/serve/standalone-assets.js +1 -1
  159. package/dist/serve/standalone-assets.js.map +1 -1
  160. package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
  161. package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
  162. package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
  163. package/dist/serve/studio-ui/index.html +1 -1
  164. package/dist/serve/vite-ai-config.d.ts +25 -0
  165. package/dist/serve/vite-ai-config.d.ts.map +1 -0
  166. package/dist/serve/vite-ai-config.js +45 -0
  167. package/dist/serve/vite-ai-config.js.map +1 -0
  168. package/dist/serve/vite-plugin.d.ts +17 -36
  169. package/dist/serve/vite-plugin.d.ts.map +1 -1
  170. package/dist/serve/vite-plugin.js +38 -57
  171. package/dist/serve/vite-plugin.js.map +1 -1
  172. package/dist/serve/vite-rules-source.d.ts +8 -0
  173. package/dist/serve/vite-rules-source.d.ts.map +1 -0
  174. package/dist/serve/vite-rules-source.js +23 -0
  175. package/dist/serve/vite-rules-source.js.map +1 -0
  176. package/dist/serve/vite-worker-runtime.d.ts +20 -0
  177. package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
  178. package/dist/serve/vite-worker-runtime.js +25 -0
  179. package/dist/serve/vite-worker-runtime.js.map +1 -0
  180. package/dist/serve/worker/client/connection.d.ts +7 -2
  181. package/dist/serve/worker/client/connection.d.ts.map +1 -1
  182. package/dist/serve/worker/client/connection.js +13 -16
  183. package/dist/serve/worker/client/connection.js.map +1 -1
  184. package/dist/serve/worker/client/core.d.ts +7 -0
  185. package/dist/serve/worker/client/core.d.ts.map +1 -1
  186. package/dist/serve/worker/client/core.js +28 -0
  187. package/dist/serve/worker/client/core.js.map +1 -1
  188. package/dist/serve/worker/client/runtime-control.d.ts +8 -0
  189. package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
  190. package/dist/serve/worker/client/runtime-control.js +11 -0
  191. package/dist/serve/worker/client/runtime-control.js.map +1 -0
  192. package/dist/serve/worker/entry.js +44 -2
  193. package/dist/serve/worker/entry.js.map +1 -1
  194. package/dist/serve/worker/host/core.js +1 -1
  195. package/dist/serve/worker/host/core.js.map +1 -1
  196. package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
  197. package/dist/serve/worker/host/firestore-writes.js +3 -2
  198. package/dist/serve/worker/host/firestore-writes.js.map +1 -1
  199. package/dist/serve/worker/host/studio.js +1 -1
  200. package/dist/serve/worker/host/studio.js.map +1 -1
  201. package/dist/serve/worker/host-context.d.ts +1 -1
  202. package/dist/serve/worker/host-context.d.ts.map +1 -1
  203. package/dist/serve/worker/index.d.ts +2 -1
  204. package/dist/serve/worker/index.d.ts.map +1 -1
  205. package/dist/serve/worker/index.js +2 -1
  206. package/dist/serve/worker/index.js.map +1 -1
  207. package/dist/serve/worker/protocol.d.ts +15 -1
  208. package/dist/serve/worker/protocol.d.ts.map +1 -1
  209. package/dist/serve/worker/protocol.js +2 -2
  210. package/dist/serve/worker/protocol.js.map +1 -1
  211. package/dist/serve/worker/retirement.d.ts +21 -0
  212. package/dist/serve/worker/retirement.d.ts.map +1 -0
  213. package/dist/serve/worker/retirement.js +97 -0
  214. package/dist/serve/worker/retirement.js.map +1 -0
  215. package/dist/serve/worker/serve-init.d.ts.map +1 -1
  216. package/dist/serve/worker/serve-init.js +5 -6
  217. package/dist/serve/worker/serve-init.js.map +1 -1
  218. package/dist/vite.d.ts +1 -0
  219. package/dist/vite.d.ts.map +1 -1
  220. package/dist/vite.js.map +1 -1
  221. package/package.json +6 -4
  222. package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
  223. package/src/assurance/attachment.ts +211 -0
  224. package/src/assurance/browser.ts +80 -0
  225. package/src/assurance/campaign.ts +496 -0
  226. package/src/assurance/capabilities.ts +446 -0
  227. package/src/assurance/cases.ts +43 -0
  228. package/src/assurance/index.ts +75 -0
  229. package/src/assurance/runner.ts +885 -0
  230. package/src/assurance/tool-names.ts +15 -0
  231. package/src/assurance/tools.ts +830 -0
  232. package/src/assurance/types.ts +352 -0
  233. package/src/assurance/validation.ts +434 -0
  234. package/src/bridge/client/bridge.ts +550 -0
  235. package/src/bridge/client/dispatch.ts +137 -0
  236. package/src/bridge/client.ts +45 -0
  237. package/src/bridge/protocol.ts +351 -0
  238. package/src/bridge/server/audit.ts +53 -0
  239. package/src/bridge/server/bridge.ts +597 -0
  240. package/src/bridge/server/headless.ts +160 -0
  241. package/src/bridge/server/json-schema-to-zod.ts +108 -0
  242. package/src/bridge/server/local-bridge.ts +66 -0
  243. package/src/bridge/server/logger.ts +50 -0
  244. package/src/bridge/server/mcp-contract.ts +93 -0
  245. package/src/bridge/server/mcp.ts +129 -0
  246. package/src/bridge/server/peer.ts +232 -0
  247. package/src/bridge/server/standalone.ts +347 -0
  248. package/src/bridge/server/tool-metadata.ts +99 -0
  249. package/src/bridge/server.ts +28 -0
  250. package/src/cli/can-i-use.ts +50 -0
  251. package/src/cli/cli.test.ts +493 -0
  252. package/src/cli/database-rules.ts +329 -0
  253. package/src/cli/dev-runner.ts +282 -0
  254. package/src/cli/firebase-json.ts +100 -0
  255. package/src/cli/firestore-indexes.ts +70 -0
  256. package/src/cli/index.ts +425 -0
  257. package/src/cli/init.ts +260 -0
  258. package/src/cli/mcp-proxy.ts +196 -0
  259. package/src/cli/parse-args.ts +80 -0
  260. package/src/cli/rules.ts +259 -0
  261. package/src/cli/scope.ts +67 -0
  262. package/src/cli/serve.ts +996 -0
  263. package/src/cli/service-commands.ts +73 -0
  264. package/src/cli/snapshot.ts +164 -0
  265. package/src/cli/storage-rules.ts +172 -0
  266. package/src/cli/verify.ts +416 -0
  267. package/src/conformance/.generated/can-i-use-browser.ts +113 -0
  268. package/src/conformance/.generated/can-i-use.ts +117 -0
  269. package/src/conformance/.generated/conformance-docs.ts +14 -0
  270. package/src/conformance/browser.ts +24 -0
  271. package/src/conformance/can-i-use-tool.ts +51 -0
  272. package/src/conformance/can-i-use.ts +18 -0
  273. package/src/conformance/docs.ts +8 -0
  274. package/src/conformance/index.ts +20 -0
  275. package/src/conformance/tools.ts +16 -0
  276. package/src/credentials/core/memoize-ttl.ts +142 -0
  277. package/src/credentials/core/types.ts +10 -0
  278. package/src/credentials/node/from-adc.ts +96 -0
  279. package/src/credentials/node/from-service-account.ts +133 -0
  280. package/src/credentials/node/index.ts +6 -0
  281. package/src/discover/concurrency.ts +127 -0
  282. package/src/discover/crawler-adapter.ts +142 -0
  283. package/src/discover/crawler.ts +1127 -0
  284. package/src/discover/credential-free.ts +27 -0
  285. package/src/discover/findCollectionGroup.ts +131 -0
  286. package/src/discover/firestore-source.ts +59 -0
  287. package/src/discover/index.ts +5 -0
  288. package/src/discover/merge.ts +523 -0
  289. package/src/discover/session.ts +402 -0
  290. package/src/discover/tools.ts +201 -0
  291. package/src/discover/types.ts +187 -0
  292. package/src/discover/wire.ts +324 -0
  293. package/src/functions-rtdb/child.ts +382 -0
  294. package/src/functions-rtdb/delivery.ts +7 -0
  295. package/src/functions-rtdb/discovery.ts +136 -0
  296. package/src/functions-rtdb/event.ts +64 -0
  297. package/src/functions-rtdb/execution.ts +111 -0
  298. package/src/functions-rtdb/in-memory-delivery.ts +40 -0
  299. package/src/functions-rtdb/project.ts +74 -0
  300. package/src/functions-rtdb/projection.ts +104 -0
  301. package/src/functions-rtdb/reference-pattern.ts +20 -0
  302. package/src/functions-rtdb/remote-delivery.ts +23 -0
  303. package/src/pkg-version.ts +49 -0
  304. package/src/register/esm-exports.ts +63 -0
  305. package/src/register/hooks.ts +30 -0
  306. package/src/register/index.ts +150 -0
  307. package/src/register/mapping.ts +33 -0
  308. package/src/remote/index.ts +1050 -0
  309. package/src/rtdb/crawl-snapshot.ts +118 -0
  310. package/src/rtdb/inspection.ts +137 -0
  311. package/src/rtdb/load-rules-document.ts +56 -0
  312. package/src/rtdb/rules-generation-tool.ts +38 -0
  313. package/src/rtdb/rules-json.ts +29 -0
  314. package/src/serve/activity-guard.ts +29 -0
  315. package/src/serve/activity-route.ts +164 -0
  316. package/src/serve/activity-warning.ts +26 -0
  317. package/src/serve/bridge-mount.ts +200 -0
  318. package/src/serve/bundler.ts +519 -0
  319. package/src/serve/capture-store.ts +65 -0
  320. package/src/serve/discovery.ts +199 -0
  321. package/src/serve/entries/ai.ts +292 -0
  322. package/src/serve/entries/app-backend.ts +4 -0
  323. package/src/serve/entries/app-client.ts +26 -0
  324. package/src/serve/entries/app-session-store.ts +47 -0
  325. package/src/serve/entries/app.ts +11 -0
  326. package/src/serve/entries/auth-helper-core.ts +181 -0
  327. package/src/serve/entries/auth-helper-dom.ts +134 -0
  328. package/src/serve/entries/auth-helper-runtime.ts +20 -0
  329. package/src/serve/entries/auth.ts +255 -0
  330. package/src/serve/entries/bridge-url.ts +31 -0
  331. package/src/serve/entries/database.ts +129 -0
  332. package/src/serve/entries/firestore.ts +232 -0
  333. package/src/serve/entries/init.ts +49 -0
  334. package/src/serve/entries/keepalive.ts +46 -0
  335. package/src/serve/entries/messaging-sw.ts +75 -0
  336. package/src/serve/entries/messaging.ts +96 -0
  337. package/src/serve/entries/runtime.ts +556 -0
  338. package/src/serve/entries/session-store.ts +97 -0
  339. package/src/serve/entries/storage.ts +92 -0
  340. package/src/serve/entries/tab-sync-wiring.ts +274 -0
  341. package/src/serve/entries/worker-runtime.ts +135 -0
  342. package/src/serve/init-payload.ts +42 -0
  343. package/src/serve/namespace.ts +675 -0
  344. package/src/serve/open-browser.ts +68 -0
  345. package/src/serve/rules.ts +272 -0
  346. package/src/serve/runtime/chip-config.ts +30 -0
  347. package/src/serve/runtime/chip-install.ts +27 -0
  348. package/src/serve/runtime/chip.ts +282 -0
  349. package/src/serve/runtime/manifest.ts +37 -0
  350. package/src/serve/runtime/status.ts +216 -0
  351. package/src/serve/runtime/worker-connection.ts +14 -0
  352. package/src/serve/runtime/worker-generation.ts +75 -0
  353. package/src/serve/runtime/worker-replacement.ts +42 -0
  354. package/src/serve/sandbox-marker.ts +31 -0
  355. package/src/serve/server.ts +428 -0
  356. package/src/serve/standalone-assets.ts +180 -0
  357. package/src/serve/state-store.ts +166 -0
  358. package/src/serve/studio/disk-project-store.ts +185 -0
  359. package/src/serve/studio/disk-workspace.ts +162 -0
  360. package/src/serve/studio/index.ts +20 -0
  361. package/src/serve/studio/routes.ts +237 -0
  362. package/src/serve/studio/store-types.ts +48 -0
  363. package/src/serve/studio/studio-storage.test.ts +289 -0
  364. package/src/serve/vite-ai-config.ts +77 -0
  365. package/src/serve/vite-plugin.ts +1075 -0
  366. package/src/serve/vite-rules-source.ts +28 -0
  367. package/src/serve/vite-worker-runtime.ts +51 -0
  368. package/src/serve/worker/activity-bootstrap.ts +24 -0
  369. package/src/serve/worker/client/admin-firestore.ts +38 -0
  370. package/src/serve/worker/client/ai.ts +124 -0
  371. package/src/serve/worker/client/auth.ts +489 -0
  372. package/src/serve/worker/client/connection.ts +227 -0
  373. package/src/serve/worker/client/core.ts +384 -0
  374. package/src/serve/worker/client/disconnect.ts +45 -0
  375. package/src/serve/worker/client/firestore-reads.ts +181 -0
  376. package/src/serve/worker/client/firestore-refs.ts +226 -0
  377. package/src/serve/worker/client/firestore-writes.ts +232 -0
  378. package/src/serve/worker/client/handles.ts +91 -0
  379. package/src/serve/worker/client/messaging.ts +119 -0
  380. package/src/serve/worker/client/presence.ts +174 -0
  381. package/src/serve/worker/client/rtdb.ts +311 -0
  382. package/src/serve/worker/client/rules.ts +63 -0
  383. package/src/serve/worker/client/runtime-control.ts +22 -0
  384. package/src/serve/worker/client/service-worker-connection.ts +59 -0
  385. package/src/serve/worker/client/snapshots.ts +86 -0
  386. package/src/serve/worker/client/storage.ts +195 -0
  387. package/src/serve/worker/client/studio.ts +88 -0
  388. package/src/serve/worker/client.ts +57 -0
  389. package/src/serve/worker/durable-persistence.ts +137 -0
  390. package/src/serve/worker/entry.ts +223 -0
  391. package/src/serve/worker/host/admin-firestore.ts +88 -0
  392. package/src/serve/worker/host/connection.ts +179 -0
  393. package/src/serve/worker/host/core.ts +387 -0
  394. package/src/serve/worker/host/dispatch.ts +316 -0
  395. package/src/serve/worker/host/firestore-reads.ts +117 -0
  396. package/src/serve/worker/host/firestore-writes.ts +456 -0
  397. package/src/serve/worker/host/presence.ts +312 -0
  398. package/src/serve/worker/host/rtdb.ts +136 -0
  399. package/src/serve/worker/host/rules.ts +128 -0
  400. package/src/serve/worker/host/storage.ts +312 -0
  401. package/src/serve/worker/host/studio.ts +75 -0
  402. package/src/serve/worker/host/subscriptions.ts +238 -0
  403. package/src/serve/worker/host-ai.ts +165 -0
  404. package/src/serve/worker/host-auth.ts +469 -0
  405. package/src/serve/worker/host-context.ts +273 -0
  406. package/src/serve/worker/host-events.ts +94 -0
  407. package/src/serve/worker/host-messaging.ts +240 -0
  408. package/src/serve/worker/host.ts +47 -0
  409. package/src/serve/worker/index.ts +196 -0
  410. package/src/serve/worker/presence-timing.ts +15 -0
  411. package/src/serve/worker/protocol.ts +1124 -0
  412. package/src/serve/worker/retirement.ts +125 -0
  413. package/src/serve/worker/serve-init.ts +605 -0
  414. package/src/serve/worker/service-worker-channel.ts +33 -0
  415. package/src/serve/worker/service-worker-relay.ts +86 -0
  416. package/src/serve/writer-lock.ts +53 -0
  417. package/src/verify/cases.ts +233 -0
  418. package/src/verify/fixture.ts +258 -0
  419. package/src/verify/index.ts +519 -0
  420. package/src/verify/tools.ts +108 -0
  421. package/src/version/compat-target.ts +17 -0
  422. package/src/vite.ts +18 -0
  423. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
  424. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
@@ -0,0 +1,456 @@
1
+ /**
2
+ * SharedWorker host — Firestore write ops (single writes + batch + transaction).
3
+ *
4
+ * `setDoc`/`updateDoc`/`deleteDoc`/`addDoc`, the all-or-nothing `batchCommit`,
5
+ * and the full-fidelity `txnCommit` (read-set validation for multi-tab
6
+ * correctness). Owns the write-payload preparation helpers: sentinel
7
+ * reconstruction + marker rehydration (`prepareWriteData`), the batch-write
8
+ * applier, and the transaction read-set canonicalizer.
9
+ *
10
+ * Routed here by the host dispatcher (host/dispatch.ts) with the op's resolved
11
+ * Firestore handle (`db`). Never imports the dispatcher.
12
+ */
13
+
14
+ import {
15
+ doc as pyricDoc,
16
+ collection as pyricCollection,
17
+ setDoc,
18
+ updateDoc,
19
+ deleteDoc,
20
+ addDoc,
21
+ runTransaction,
22
+ writeBatch,
23
+ serverTimestamp,
24
+ increment,
25
+ arrayUnion,
26
+ arrayRemove,
27
+ deleteField,
28
+ type Firestore,
29
+ type DocumentReference,
30
+ type SetOptions,
31
+ } from 'pyric/firestore';
32
+ import { rehydrateDocValue } from 'pyric/firestore/internal/value-codec';
33
+
34
+ import type { OpMessage, WriteDescriptor, TxnReadEntry, SentinelMarker } from '../protocol.js';
35
+ import { serializeDocData, isSentinelMarker } from '../protocol.js';
36
+ import { type HostCtx, type PortLike, post, ok, fail, bestEffortFlush } from '../host-context.js';
37
+
38
+ // ─── Sentinel resolution ──────────────────────────────────────────────────
39
+
40
+ /**
41
+ * Walk a write payload and replace every `SentinelMarker` with the
42
+ * corresponding pyric/firestore FieldValue sentinel object.
43
+ *
44
+ * WHY: FieldValue class instances don't survive structured clone — they
45
+ * arrive as plain objects and lose their prototype, breaking sandbox
46
+ * sentinel detection. The client sends `{ __sentinel: 'serverTimestamp' }`
47
+ * etc. and we reconstruct the real FieldValue here before the sandbox sees
48
+ * the data.
49
+ *
50
+ * The sandbox's sentinel-capture code (sentinel-capture.ts) recognizes
51
+ * FieldValue objects by their internal `__type` property, which the
52
+ * factories produce correctly.
53
+ */
54
+ function resolveSentinels(value: unknown): unknown {
55
+ if (isSentinelMarker(value)) {
56
+ return resolveSentinel(value);
57
+ }
58
+ if (Array.isArray(value)) {
59
+ return value.map(resolveSentinels);
60
+ }
61
+ if (value !== null && typeof value === 'object') {
62
+ // Leave CLASS instances intact (Timestamp/Bytes/LatLng rehydrated by
63
+ // prepareWriteData below, or FieldValue objects): walking their entries
64
+ // into a plain object would strip the prototype the sandbox keys on.
65
+ // Sentinel markers only ever live in plain JSON containers.
66
+ const proto = Object.getPrototypeOf(value);
67
+ if (proto !== Object.prototype && proto !== null) {
68
+ return value;
69
+ }
70
+ const obj = value as Record<string, unknown>;
71
+ const out: Record<string, unknown> = {};
72
+ for (const [k, v] of Object.entries(obj)) {
73
+ out[k] = resolveSentinels(v);
74
+ }
75
+ return out;
76
+ }
77
+ return value;
78
+ }
79
+
80
+ /**
81
+ * Prepare an incoming WRITE payload for the sandbox: rehydrate marker-shaped
82
+ * scalars into REAL wrapper instances, then rebuild FieldValue sentinels.
83
+ *
84
+ * WHY REHYDRATE WRITES (spike gap 4): over the JSON relay legs a Node-side
85
+ * `Timestamp`/`Bytes`/`GeoPoint` arrives as its `toJSON()` marker
86
+ * (`{ type: 'firestore/timestamp/1.0', … }` or `{ __type: 'timestamp', … }`).
87
+ * Without rehydration the worker STORES the marker as a plain map — reads
88
+ * mask the bug (the read path rehydrates), but in-worker rules comparisons
89
+ * and `orderBy` over that field see a map, not a timestamp. `rehydrateDocValue`
90
+ * is the same canonical codec the read path / persistence uses, so the wire,
91
+ * store, and IDB formats stay one format.
92
+ *
93
+ * Order matters: rehydration first (it passes `__sentinel` markers through
94
+ * as plain objects, rehydrating any marker-shaped values nested inside
95
+ * arrayUnion/arrayRemove), then sentinel resolution (which now skips the
96
+ * freshly rehydrated class instances — see resolveSentinels).
97
+ */
98
+ export function prepareWriteData(value: unknown): unknown {
99
+ return resolveSentinels(rehydrateDocValue(value));
100
+ }
101
+
102
+ function resolveSentinel(marker: SentinelMarker): unknown {
103
+ switch (marker.__sentinel) {
104
+ case 'serverTimestamp': return serverTimestamp();
105
+ case 'increment': return increment(marker.n);
106
+ case 'arrayUnion': return arrayUnion(...marker.values);
107
+ case 'arrayRemove': return arrayRemove(...marker.values);
108
+ case 'deleteField': return deleteField();
109
+ }
110
+ }
111
+
112
+ // ─── Transaction read-set canonicalization helpers ─────────────────────────
113
+
114
+ /**
115
+ * Rebuild a JSON-flattened `Uint8Array` (an index-keyed `{ "0": n, … }`
116
+ * map) and encode it as base64url without padding — the exact form
117
+ * `pyric/rules`' `Bytes.toBase64()` emits. Backs the transaction read-set
118
+ * canonicalizer's handling of prototype-stripped `Bytes` clones.
119
+ */
120
+ /** Timestamp/Duration stripped-clone key set (they share field names). */
121
+ const TS_CLONE_KEYS = ['typeName', 'seconds', 'nanos'] as const;
122
+
123
+ /** Does `o` carry EXACTLY these own enumerable keys (no more, no fewer)?
124
+ * Backs the transaction canonicalizer's strict clone-shape matching. */
125
+ function hasExactKeys(o: Record<string, unknown>, keys: readonly string[]): boolean {
126
+ const own = Object.keys(o);
127
+ return own.length === keys.length && keys.every((k) => k in o);
128
+ }
129
+
130
+ function indexMapToBase64Url(data: unknown): string {
131
+ const map = (data ?? {}) as Record<string, number>;
132
+ const keys = Object.keys(map);
133
+ const bytes = new Uint8Array(keys.length);
134
+ for (let i = 0; i < keys.length; i++) bytes[i] = map[String(i)] ?? 0;
135
+ let binary = '';
136
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);
137
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
138
+ }
139
+
140
+ // ─── Batch write helper ───────────────────────────────────────────────────
141
+
142
+ type BatchHandle = {
143
+ set(ref: DocumentReference, data: Record<string, unknown>, options?: SetOptions): void;
144
+ update(ref: DocumentReference, data: Record<string, unknown>): void;
145
+ delete(ref: DocumentReference): void;
146
+ };
147
+
148
+ function applyWriteToBatch(
149
+ db: Firestore,
150
+ batch: unknown,
151
+ w: WriteDescriptor,
152
+ ): void {
153
+ const b = batch as BatchHandle;
154
+ const ref = pyricDoc(db, w.path);
155
+ if (w.method === 'set') {
156
+ const data = prepareWriteData(w.data) as Record<string, unknown>;
157
+ b.set(ref, data, w.options as SetOptions | undefined);
158
+ } else if (w.method === 'update') {
159
+ const data = prepareWriteData(w.data) as Record<string, unknown>;
160
+ b.update(ref, data);
161
+ } else if (w.method === 'delete') {
162
+ b.delete(ref);
163
+ }
164
+ }
165
+
166
+ // ─── Op handler ────────────────────────────────────────────────────────────
167
+
168
+ /** The write op methods routed to {@link handleFirestoreWriteOp}. */
169
+ const WRITE_METHODS = new Set<string>([
170
+ 'setDoc',
171
+ 'updateDoc',
172
+ 'deleteDoc',
173
+ 'addDoc',
174
+ 'batchCommit',
175
+ 'txnCommit',
176
+ ]);
177
+
178
+ export function isFirestoreWriteOp(method: OpMessage['method']): boolean {
179
+ return WRITE_METHODS.has(method);
180
+ }
181
+
182
+ export async function handleFirestoreWriteOp(
183
+ ctx: HostCtx,
184
+ port: PortLike,
185
+ msg: OpMessage,
186
+ db: Firestore,
187
+ ): Promise<void> {
188
+ switch (msg.method) {
189
+ case 'setDoc': {
190
+ try {
191
+ const ref = pyricDoc(db, msg.path);
192
+ const data = prepareWriteData(msg.data) as Record<string, unknown>;
193
+ await setDoc(ref, data, msg.options as SetOptions | undefined);
194
+ await bestEffortFlush(ctx);
195
+ ok(port, msg.id, null);
196
+ } catch (e) { fail(port, msg.id, e); }
197
+ break;
198
+ }
199
+
200
+ case 'updateDoc': {
201
+ try {
202
+ const ref = pyricDoc(db, msg.path);
203
+ const data = prepareWriteData(msg.data) as Record<string, unknown>;
204
+ await updateDoc(ref, data);
205
+ await bestEffortFlush(ctx);
206
+ ok(port, msg.id, null);
207
+ } catch (e) { fail(port, msg.id, e); }
208
+ break;
209
+ }
210
+
211
+ case 'deleteDoc': {
212
+ try {
213
+ const ref = pyricDoc(db, msg.path);
214
+ await deleteDoc(ref);
215
+ await bestEffortFlush(ctx);
216
+ ok(port, msg.id, null);
217
+ } catch (e) { fail(port, msg.id, e); }
218
+ break;
219
+ }
220
+
221
+ case 'addDoc': {
222
+ try {
223
+ const coll = pyricCollection(db, msg.collectionPath);
224
+ const data = prepareWriteData(msg.data) as Record<string, unknown>;
225
+ const ref = await addDoc(coll, data);
226
+ await bestEffortFlush(ctx);
227
+ ok(port, msg.id, { id: ref.id, path: ref.path });
228
+ } catch (e) { fail(port, msg.id, e); }
229
+ break;
230
+ }
231
+
232
+ case 'batchCommit': {
233
+ /**
234
+ * WHY: writeBatch in pyric/firestore buffers set/update/delete calls
235
+ * and commits them as one unit through the sandbox backend. We
236
+ * reconstruct the batch here from the wire write descriptors.
237
+ * Atomicity guarantee: the sandbox backend applies all writes or none
238
+ * (per-collection lock semantics of LocalEnvironment.batch()).
239
+ */
240
+ try {
241
+ const batch = writeBatch(db);
242
+ for (const w of msg.writes) {
243
+ applyWriteToBatch(db, batch, w);
244
+ }
245
+ await (batch as { commit(): Promise<void> }).commit();
246
+ await bestEffortFlush(ctx);
247
+ ok(port, msg.id, null);
248
+ } catch (e) { fail(port, msg.id, e); }
249
+ break;
250
+ }
251
+
252
+ case 'txnCommit': {
253
+ /**
254
+ * READ-SET VALIDATION FOR MULTI-TAB CORRECTNESS
255
+ * -----------------------------------------------
256
+ * The client sends `{ method: 'txnCommit', reads, writes }` after its
257
+ * `updateFn` callback has resolved. `reads` is the set of docs the
258
+ * client read during `updateFn` (each entry is `{ path, data }` where
259
+ * `data` is the serialized snapshot the client saw, or null if the doc
260
+ * was missing).
261
+ *
262
+ * WHY READ-SET VALIDATION IS NECESSARY
263
+ * -------------------------------------
264
+ * A transaction spans two messages: the client's `txn.get` RPC (sent
265
+ * during `updateFn`) and this `txnCommit` RPC. Between those messages
266
+ * the event loop is free — another tab's write to any of the read docs
267
+ * can land in that gap via a separate `setDoc` message. Without
268
+ * validation that would be a silent lost update: the client computed
269
+ * its writes using stale data, committed them, and the other tab's
270
+ * changes were silently overwritten.
271
+ *
272
+ * HOW WE VALIDATE (no per-doc version/updateTime in the sandbox)
273
+ * ---------------------------------------------------------------
274
+ * Inside a real `runTransaction(db, callback)`, the callback re-reads
275
+ * every doc from the read-set via `tx.get(ref)`, serializes its current
276
+ * data to the same JSON form the client produced (using `serializeDocData`
277
+ * on both sides), and compares the JSON strings. If any doc's current
278
+ * serialized form differs from what the client recorded, another tab
279
+ * wrote it between the client's read and this commit — we throw an abort
280
+ * sentinel so the sandbox rolls back the transaction, and we return
281
+ * `{ ok: false, error: { code: 'aborted' } }` on the wire. The client's
282
+ * retry loop then re-runs `updateFn` with fresh reads.
283
+ *
284
+ * SERIALIZED-FORM EQUALITY — VIA THE CANONICAL CODEC
285
+ * ---------------------------------------------------
286
+ * Both JSON strings are CANONICALIZED before comparison:
287
+ * `JSON.stringify(rehydrateDocValue(JSON.parse(json)))`. Raw string
288
+ * equality is NOT safe here even within one process, because the two
289
+ * read paths yield DIFFERENT wrapper classes for the same stored
290
+ * value: `getDoc` (what the client's read-set echoes) returns
291
+ * `firebase/firestore` classes whose `Timestamp.toJSON()` emits
292
+ * `{ seconds, nanoseconds, type }`, while the transaction's
293
+ * validation re-read comes through the admin-compat wrapper whose
294
+ * `Timestamp.toJSON()` emits `{ type, seconds, nanoseconds }` — same
295
+ * value, different key order, different string. Rehydrating both
296
+ * sides through the ONE shared codec
297
+ * (`pyric/firestore/internal/value-codec`)
298
+ * collapses every marker family into the same wrapper classes with
299
+ * deterministic `toJSON()` key order, so string equality ↔ value
300
+ * equality again — an unmodified doc can never phantom-abort (which
301
+ * would livelock the client's retry loop), while a real concurrent
302
+ * write still mismatches. Plain-map key order is preserved by both
303
+ * paths from the same stored object, so it stays comparable.
304
+ *
305
+ * NOTE ON tx.set/update/delete SIGNATURE
306
+ * ---------------------------------------
307
+ * The modular pyric/firestore Transaction (wrapping the admin-compat
308
+ * TransactionWrapper) takes `tx.set(docRef, data)` where `docRef` is a
309
+ * real `DocumentReference` — NOT a path string. We build refs via
310
+ * `pyricDoc(db, path)` for each write.
311
+ */
312
+
313
+ /** Sentinel thrown inside the transaction callback to signal an abort. */
314
+ const TXN_ABORT = Symbol('txn-abort');
315
+
316
+ /**
317
+ * Canonicalize a serialized doc-data JSON string (see
318
+ * SERIALIZED-FORM EQUALITY above). Two normalization passes:
319
+ *
320
+ * 1. `rehydrateDocValue` collapses the marker families (`__type`
321
+ * persistence markers and `firebase/firestore` `toJSON()`
322
+ * markers) into the one set of wrapper classes, whose
323
+ * `toJSON()` re-emits a single deterministic form.
324
+ * 2. The stringify replacer additionally normalizes PROTOTYPE-
325
+ * STRIPPED wrapper clones — the sandbox transaction's
326
+ * capture-by-value `structuredClone` turns a stored rules
327
+ * wrapper into a plain `{ typeName, … }` object that neither
328
+ * marker family matches, so without this pass an unmodified
329
+ * typed doc would never compare equal to the client's getDoc
330
+ * echo (a guaranteed phantom abort → retry livelock).
331
+ */
332
+ const canonicalDocJson = (json: string): string =>
333
+ JSON.stringify(rehydrateDocValue(JSON.parse(json)), (_key, v) => {
334
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) return v;
335
+ const o = v as Record<string, unknown>;
336
+ if (typeof o.typeName !== 'string' || o.__type !== undefined) return v;
337
+ // Re-shape a stripped rules-wrapper clone into the wrapper's own
338
+ // canonical toJSON marker form (kept in sync with pyric/rules'
339
+ // simulator/wrappers/* instance fields + toJSON()) — but ONLY
340
+ // when the key set EXACTLY matches that wrapper's own-field
341
+ // shape. A looser match would silently DROP extra keys from
342
+ // user data that merely resembles a clone, collapsing two
343
+ // genuinely different docs into one canonical form and letting
344
+ // a concurrent write commit undetected (false equality). A
345
+ // near-miss map passes through unchanged — worst case is a
346
+ // spurious abort + retry, never a lost update.
347
+ switch (o.typeName) {
348
+ case 'timestamp':
349
+ return hasExactKeys(o, TS_CLONE_KEYS)
350
+ ? { __type: 'timestamp', seconds: o.seconds, nanos: o.nanos }
351
+ : v;
352
+ case 'duration':
353
+ return hasExactKeys(o, TS_CLONE_KEYS)
354
+ ? { __type: 'duration', seconds: o.seconds, nanos: o.nanos }
355
+ : v;
356
+ case 'latlng':
357
+ return hasExactKeys(o, ['typeName', 'lat', 'lng'])
358
+ ? { __type: 'latlng', lat: o.lat, lng: o.lng }
359
+ : v;
360
+ case 'reference':
361
+ return hasExactKeys(o, ['typeName', 'path'])
362
+ ? { __type: 'reference', path: o.path }
363
+ : v;
364
+ case 'path':
365
+ return hasExactKeys(o, ['typeName', 'segments', 'bindings'])
366
+ ? { __type: 'path', segments: o.segments }
367
+ : v;
368
+ case 'bytes':
369
+ // The Uint8Array field serialized as an index-keyed map;
370
+ // rebuild and emit Bytes.toJSON()'s base64url form.
371
+ return hasExactKeys(o, ['typeName', 'data'])
372
+ ? { __type: 'bytes', base64: indexMapToBase64Url(o.data) }
373
+ : v;
374
+ default:
375
+ return v;
376
+ }
377
+ });
378
+
379
+ try {
380
+ await runTransaction(db, async (tx) => {
381
+ // ── Step 1: validate the read-set ──────────────────────────────
382
+ // Re-read each doc the client touched and compare its current
383
+ // serialized form against what the client recorded at read time.
384
+ const modularTx = tx as {
385
+ get(ref: DocumentReference): Promise<{ exists: boolean | (() => boolean); data(): Record<string, unknown> | undefined }>;
386
+ set(ref: DocumentReference, data: Record<string, unknown>, opts?: SetOptions): void;
387
+ update(ref: DocumentReference, data: Record<string, unknown>): void;
388
+ delete(ref: DocumentReference): void;
389
+ };
390
+
391
+ for (const r of (msg as { reads: TxnReadEntry[]; writes: typeof msg.writes }).reads) {
392
+ const ref = pyricDoc(db, r.path);
393
+ const currentSnap = await modularTx.get(ref);
394
+ const existsBool = typeof currentSnap.exists === 'function'
395
+ ? (currentSnap.exists as () => boolean)()
396
+ : currentSnap.exists as boolean;
397
+
398
+ // Compute the serialized form of the current doc state.
399
+ const currentSerialized = existsBool && currentSnap.data()
400
+ ? serializeDocData(currentSnap.data() as Record<string, unknown>)
401
+ : null;
402
+
403
+ // Compare against what the client recorded:
404
+ // both null → ok (doc still doesn't exist)
405
+ // both present + same JSON → ok
406
+ // anything else → conflict
407
+ const clientHadNull = r.data === null;
408
+ const workerHasNull = currentSerialized === null;
409
+
410
+ if (clientHadNull !== workerHasNull) {
411
+ // Existence changed (created or deleted by another tab).
412
+ throw TXN_ABORT;
413
+ }
414
+ if (
415
+ !clientHadNull &&
416
+ !workerHasNull &&
417
+ canonicalDocJson(r.data!.json) !== canonicalDocJson(currentSerialized!.json)
418
+ ) {
419
+ // Data changed by another tab.
420
+ throw TXN_ABORT;
421
+ }
422
+ }
423
+
424
+ // ── Step 2: apply the queued writes ────────────────────────────
425
+ for (const w of msg.writes) {
426
+ const ref = pyricDoc(db, w.path);
427
+ if (w.method === 'set') {
428
+ const data = prepareWriteData(w.data) as Record<string, unknown>;
429
+ modularTx.set(ref, data, w.options as SetOptions | undefined);
430
+ } else if (w.method === 'update') {
431
+ const data = prepareWriteData(w.data) as Record<string, unknown>;
432
+ modularTx.update(ref, data);
433
+ } else if (w.method === 'delete') {
434
+ modularTx.delete(ref);
435
+ }
436
+ }
437
+ });
438
+ await bestEffortFlush(ctx);
439
+ ok(port, msg.id, null);
440
+ } catch (e) {
441
+ if (e === TXN_ABORT) {
442
+ // Read-set conflict — signal the client to retry its updateFn.
443
+ const abortErr = { code: 'aborted', message: 'Transaction read-set conflict: a concurrent write invalidated the read snapshot.' };
444
+ post(port, { t: 'res', id: msg.id, ok: false, error: abortErr });
445
+ } else {
446
+ fail(port, msg.id, e);
447
+ }
448
+ }
449
+ break;
450
+ }
451
+
452
+ default: {
453
+ fail(port, msg.id, new Error(`Unknown method: ${String((msg as { method: unknown }).method)}`));
454
+ }
455
+ }
456
+ }