trellis 3.1.35 → 3.2.2

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 (287) hide show
  1. package/CHANGELOG.md +23 -5
  2. package/README.md +63 -156
  3. package/bin/trellis.mjs +2 -75
  4. package/dist/{better-sqlite-backend-ahx5p0br.js → better-sqlite-backend-2P4F6AOJ.js} +91 -29
  5. package/dist/boot-middleware-MHLHFCUC.js +9 -0
  6. package/dist/browser/index.d.ts +18 -0
  7. package/dist/browser/index.d.ts.map +1 -0
  8. package/dist/browser/index.js +95 -0
  9. package/dist/{index-53f3b8p8.js → chunk-2W3MHTMG.js} +552 -213
  10. package/dist/chunk-44FFQKQX.js +142 -0
  11. package/dist/chunk-4HYPLFJD.js +150 -0
  12. package/dist/chunk-4XIVMVHC.js +361 -0
  13. package/dist/{index-yp88he8n.js → chunk-6CTA6MCE.js} +88 -64
  14. package/dist/chunk-6GEZ6DN5.js +394 -0
  15. package/dist/chunk-7ZFRVCUE.js +211 -0
  16. package/dist/chunk-CAG4FULI.js +121 -0
  17. package/dist/chunk-FF36CQHZ.js +61 -0
  18. package/dist/{index-h32txmxe.js → chunk-FKQO5A3H.js} +6 -13
  19. package/dist/{index-nq520y6k.js → chunk-FWRS4CSC.js} +46 -32
  20. package/dist/chunk-GFZCJ4EH.js +108 -0
  21. package/dist/chunk-GLM4HGN4.js +55 -0
  22. package/dist/{index-4cdr7x2x.js → chunk-GU374KWZ.js} +182 -111
  23. package/dist/chunk-H6JB3PZ3.js +45 -0
  24. package/dist/chunk-IZX2PLIB.js +1240 -0
  25. package/dist/chunk-J2TXUFCZ.js +289 -0
  26. package/dist/chunk-J3WYZO3Q.js +211 -0
  27. package/dist/{index-xzym9w0m.js → chunk-JA7AIHRK.js} +12 -7
  28. package/dist/chunk-JHHEXEDM.js +162 -0
  29. package/dist/chunk-JUEMDWLU.js +233 -0
  30. package/dist/chunk-KFQGP6VL.js +33 -0
  31. package/dist/chunk-KIJITUZB.js +419 -0
  32. package/dist/{index-4wxa8xz4.js → chunk-KQ4A2D2P.js} +162 -23
  33. package/dist/chunk-KSU2GW22.js +958 -0
  34. package/dist/{index-rv1p47gp.js → chunk-L5N5PR2S.js} +3015 -2480
  35. package/dist/{index-h9e2efx4.js → chunk-LNUIGRDZ.js} +71 -55
  36. package/dist/{index-skhn0agf.js → chunk-MLC5BUYO.js} +19 -20
  37. package/dist/chunk-MPXUVGT3.js +104 -0
  38. package/dist/chunk-N6MYZT4O.js +68 -0
  39. package/dist/chunk-O2AV5WMN.js +48 -0
  40. package/dist/chunk-PZ4XYZ4J.js +143 -0
  41. package/dist/chunk-QB5ISE47.js +419 -0
  42. package/dist/chunk-RSWUFTO2.js +857 -0
  43. package/dist/chunk-S3XLFZ27.js +1095 -0
  44. package/dist/{index-w5wccer3.js → chunk-S6GBJ2ZB.js} +28 -12
  45. package/dist/{index-7pjz3tsy.js → chunk-SGMDTWJJ.js} +57 -15
  46. package/dist/chunk-SPSPV5BC.js +390 -0
  47. package/dist/chunk-TCKXSOBP.js +123 -0
  48. package/dist/chunk-UBGUDMUV.js +59 -0
  49. package/dist/{index-wt8rz4gn.js → chunk-UQISRW6T.js} +97 -26
  50. package/dist/chunk-USEINYUL.js +12 -0
  51. package/dist/{index-bmyt7k8n.js → chunk-UZRUP7QW.js} +12 -4
  52. package/dist/{index-xhcp6xrn.js → chunk-VBYUTRXX.js} +695 -514
  53. package/dist/{index-vkpkfwhq.js → chunk-VDAKEOML.js} +234 -194
  54. package/dist/chunk-WXSWA3MV.js +0 -0
  55. package/dist/{index-n9f2qyh5.js → chunk-X5FLTRUB.js} +55 -33
  56. package/dist/{index-h7zxhhhh.js → chunk-XMQ7G77X.js} +46 -38
  57. package/dist/{index-w7ng765c.js → chunk-YCM7ZYEZ.js} +102 -49
  58. package/dist/cli/deploy-cli.d.ts +18 -0
  59. package/dist/cli/deploy-cli.d.ts.map +1 -0
  60. package/dist/cli/errors.d.ts +6 -0
  61. package/dist/cli/errors.d.ts.map +1 -0
  62. package/dist/cli/examples.d.ts +23 -0
  63. package/dist/cli/examples.d.ts.map +1 -0
  64. package/dist/cli/index.d.ts +1 -1
  65. package/dist/cli/index.d.ts.map +1 -1
  66. package/dist/cli/index.js +2426 -6225
  67. package/dist/client/index.browser.d.ts +29 -0
  68. package/dist/client/index.browser.d.ts.map +1 -0
  69. package/dist/client/index.browser.js +64 -0
  70. package/dist/client/index.d.ts +6 -1
  71. package/dist/client/index.d.ts.map +1 -1
  72. package/dist/client/index.js +41 -21
  73. package/dist/client/live.d.ts +67 -0
  74. package/dist/client/live.d.ts.map +1 -0
  75. package/dist/client/reactive.d.ts +31 -0
  76. package/dist/client/reactive.d.ts.map +1 -0
  77. package/dist/client/sdk.browser.d.ts +104 -0
  78. package/dist/client/sdk.browser.d.ts.map +1 -0
  79. package/dist/client/sdk.browser.js +9 -0
  80. package/dist/client/sdk.d.ts +26 -2
  81. package/dist/client/sdk.d.ts.map +1 -1
  82. package/dist/client/sdk.js +9 -0
  83. package/dist/client/vcs-client.d.ts +129 -0
  84. package/dist/client/vcs-client.d.ts.map +1 -0
  85. package/dist/cms/index.js +171 -163
  86. package/dist/{config-8hczw0rs.js → config-PFTP67TR.js} +8 -9
  87. package/dist/core/computation/index.d.ts +5 -0
  88. package/dist/core/computation/index.d.ts.map +1 -0
  89. package/dist/core/computation/rollup.d.ts +18 -0
  90. package/dist/core/computation/rollup.d.ts.map +1 -0
  91. package/dist/core/index.d.ts +5 -0
  92. package/dist/core/index.d.ts.map +1 -1
  93. package/dist/core/index.js +171 -104
  94. package/dist/core/kernel/boot-middleware.d.ts +10 -0
  95. package/dist/core/kernel/boot-middleware.d.ts.map +1 -0
  96. package/dist/core/kernel/logic-middleware.d.ts +7 -1
  97. package/dist/core/kernel/logic-middleware.d.ts.map +1 -1
  98. package/dist/core/kernel/trellis-kernel.d.ts.map +1 -1
  99. package/dist/core/ontology/types.d.ts +5 -0
  100. package/dist/core/ontology/types.d.ts.map +1 -1
  101. package/dist/core/persist/better-sqlite-backend.d.ts +9 -2
  102. package/dist/core/persist/better-sqlite-backend.d.ts.map +1 -1
  103. package/dist/core/persist/factory.d.ts +6 -7
  104. package/dist/core/persist/factory.d.ts.map +1 -1
  105. package/dist/core/persist/factory.js +2 -3
  106. package/dist/core/persist/sqljs-backend.js +2 -3
  107. package/dist/db/index.d.ts +4 -1
  108. package/dist/db/index.d.ts.map +1 -1
  109. package/dist/db/index.js +67 -54
  110. package/dist/db/inspector.js +11 -19
  111. package/dist/db/trellis.css +1 -1
  112. package/dist/decisions/index.js +9 -10
  113. package/dist/deploy-LQ7GUGJ5.js +9 -0
  114. package/dist/deploy-YNVG5E4E.js +9 -0
  115. package/dist/deploy-cli-24BCHAXY.js +67 -0
  116. package/dist/deploy-cli-C35HTITO.js +67 -0
  117. package/dist/embeddings/auto-embed.d.ts +2 -2
  118. package/dist/embeddings/auto-embed.d.ts.map +1 -1
  119. package/dist/embeddings/index.js +20 -22
  120. package/dist/embeddings/search.d.ts +2 -1
  121. package/dist/embeddings/search.d.ts.map +1 -1
  122. package/dist/embeddings/store.d.ts +25 -4
  123. package/dist/embeddings/store.d.ts.map +1 -1
  124. package/dist/engine.d.ts +59 -19
  125. package/dist/engine.d.ts.map +1 -1
  126. package/dist/import-UZEKXU3B.js +10 -0
  127. package/dist/index.js +102 -93
  128. package/dist/launch-explorer-HZ4ZJA4X.js +95 -0
  129. package/dist/links/index.js +24 -26
  130. package/dist/mcp/server.d.ts.map +1 -1
  131. package/dist/plugins/agent-memory/index.js +316 -0
  132. package/dist/plugins/idea-garden/index.js +165 -0
  133. package/dist/plugins/plan-approval/index.js +579 -0
  134. package/dist/plugins/proactive-watcher/index.js +194 -0
  135. package/dist/{index-2r4jxwnb.js → query-XYLTJYSA.js} +14 -15
  136. package/dist/react/index.js +39 -70
  137. package/dist/react/realtime.js +11 -0
  138. package/dist/react/schema-hooks.js +87 -0
  139. package/dist/realtime/broadcast-channel-transport.d.ts +43 -0
  140. package/dist/realtime/broadcast-channel-transport.d.ts.map +1 -0
  141. package/dist/realtime/durable-object-relay-transport.d.ts +96 -0
  142. package/dist/realtime/durable-object-relay-transport.d.ts.map +1 -0
  143. package/dist/realtime/index.d.ts +44 -0
  144. package/dist/realtime/index.d.ts.map +1 -0
  145. package/dist/realtime/index.js +40 -0
  146. package/dist/realtime/memory-hub.d.ts +32 -0
  147. package/dist/realtime/memory-hub.d.ts.map +1 -0
  148. package/dist/realtime/persistent-channel.d.ts +105 -0
  149. package/dist/realtime/persistent-channel.d.ts.map +1 -0
  150. package/dist/realtime/presence.d.ts +73 -0
  151. package/dist/realtime/presence.d.ts.map +1 -0
  152. package/dist/realtime/relay-persistence.d.ts +37 -0
  153. package/dist/realtime/relay-persistence.d.ts.map +1 -0
  154. package/dist/realtime/relay-persistence.js +107 -0
  155. package/dist/realtime/relay-server.d.ts +103 -0
  156. package/dist/realtime/relay-server.d.ts.map +1 -0
  157. package/dist/realtime/room.d.ts +98 -0
  158. package/dist/realtime/room.d.ts.map +1 -0
  159. package/dist/realtime/text.d.ts +73 -0
  160. package/dist/realtime/text.d.ts.map +1 -0
  161. package/dist/realtime/types.d.ts +89 -0
  162. package/dist/realtime/types.d.ts.map +1 -0
  163. package/dist/realtime/websocket-relay-transport.d.ts +28 -0
  164. package/dist/realtime/websocket-relay-transport.d.ts.map +1 -0
  165. package/dist/realtime.bundle.js +1100 -0
  166. package/dist/{remote-manager-xvbg4230.js → remote-manager-ODE3T2KR.js} +25 -15
  167. package/dist/scaffold/seed.d.ts.map +1 -1
  168. package/dist/scaffold/write.d.ts +1 -1
  169. package/dist/scaffold/write.d.ts.map +1 -1
  170. package/dist/schema/define.d.ts +137 -0
  171. package/dist/schema/define.d.ts.map +1 -0
  172. package/dist/schema/entity-projection.d.ts +34 -0
  173. package/dist/schema/entity-projection.d.ts.map +1 -0
  174. package/dist/schema/eql.d.ts +46 -0
  175. package/dist/schema/eql.d.ts.map +1 -0
  176. package/dist/schema/index.d.ts +21 -0
  177. package/dist/schema/index.d.ts.map +1 -0
  178. package/dist/schema/index.js +46 -0
  179. package/dist/schema/kernel-resolve.d.ts +21 -0
  180. package/dist/schema/kernel-resolve.d.ts.map +1 -0
  181. package/dist/schema/mutations.d.ts +20 -0
  182. package/dist/schema/mutations.d.ts.map +1 -0
  183. package/dist/schema/resolve.d.ts +21 -0
  184. package/dist/schema/resolve.d.ts.map +1 -0
  185. package/dist/server/deploy-meta.d.ts +25 -0
  186. package/dist/server/deploy-meta.d.ts.map +1 -0
  187. package/dist/server/deploy.d.ts +7 -2
  188. package/dist/server/deploy.d.ts.map +1 -1
  189. package/dist/server/index.d.ts +7 -2
  190. package/dist/server/index.d.ts.map +1 -1
  191. package/dist/server/index.js +100 -58
  192. package/dist/server/node-adapter.js +5 -104
  193. package/dist/server/realtime.d.ts +14 -3
  194. package/dist/server/realtime.d.ts.map +1 -1
  195. package/dist/server/server.d.ts +8 -16
  196. package/dist/server/server.d.ts.map +1 -1
  197. package/dist/server/sprites.d.ts +18 -1
  198. package/dist/server/sprites.d.ts.map +1 -1
  199. package/dist/server/tenancy.d.ts +4 -0
  200. package/dist/server/tenancy.d.ts.map +1 -1
  201. package/dist/server/usage-meter.d.ts +53 -0
  202. package/dist/server/usage-meter.d.ts.map +1 -0
  203. package/dist/server-MB3OPSPI.js +18 -0
  204. package/dist/server-ZVWGLLRF.js +18 -0
  205. package/dist/sprites-KQGTGM6W.js +27 -0
  206. package/dist/{sprites-vc4qbrp1.js → sprites-RU3E4XQN.js} +6 -7
  207. package/dist/svelte/index.d.ts +17 -0
  208. package/dist/svelte/index.d.ts.map +1 -0
  209. package/dist/svelte/index.js +51 -0
  210. package/dist/svelte/schema-hooks.d.ts +45 -0
  211. package/dist/svelte/schema-hooks.d.ts.map +1 -0
  212. package/dist/svelte/schema-hooks.js +71 -0
  213. package/dist/svelte/stores.d.ts +78 -0
  214. package/dist/svelte/stores.d.ts.map +1 -0
  215. package/dist/sync/http-transport.d.ts +3 -3
  216. package/dist/sync/http-transport.d.ts.map +1 -1
  217. package/dist/sync/index.d.ts +15 -1
  218. package/dist/sync/index.d.ts.map +1 -1
  219. package/dist/sync/index.js +617 -0
  220. package/dist/sync/memory-room.d.ts +37 -0
  221. package/dist/sync/memory-room.d.ts.map +1 -0
  222. package/dist/sync/memory-transport.d.ts +3 -3
  223. package/dist/sync/memory-transport.d.ts.map +1 -1
  224. package/dist/sync/partykit-room.d.ts +40 -0
  225. package/dist/sync/partykit-room.d.ts.map +1 -0
  226. package/dist/sync/partykit-transport.d.ts +84 -0
  227. package/dist/sync/partykit-transport.d.ts.map +1 -0
  228. package/dist/sync/room-core.d.ts +53 -0
  229. package/dist/sync/room-core.d.ts.map +1 -0
  230. package/dist/sync/sync-engine.d.ts +40 -2
  231. package/dist/sync/sync-engine.d.ts.map +1 -1
  232. package/dist/sync/sync-room-server.d.ts +39 -0
  233. package/dist/sync/sync-room-server.d.ts.map +1 -0
  234. package/dist/sync/types.d.ts +70 -3
  235. package/dist/sync/types.d.ts.map +1 -1
  236. package/dist/sync/vcs-sync-peer.d.ts +63 -0
  237. package/dist/sync/vcs-sync-peer.d.ts.map +1 -0
  238. package/dist/sync/ws-transport.d.ts +2 -2
  239. package/dist/sync/ws-transport.d.ts.map +1 -1
  240. package/dist/tenancy-NBLLGCNJ.js +14 -0
  241. package/dist/tenancy-POMWQP6M.js +16 -0
  242. package/dist/ui/client.html +355 -67
  243. package/dist/ui/launch-explorer.d.ts +22 -0
  244. package/dist/ui/launch-explorer.d.ts.map +1 -0
  245. package/dist/ui/server.d.ts +1 -1
  246. package/dist/ui/server.d.ts.map +1 -1
  247. package/dist/vcs/blob-store.d.ts.map +1 -1
  248. package/dist/vcs/idb-op-log.d.ts +67 -0
  249. package/dist/vcs/idb-op-log.d.ts.map +1 -0
  250. package/dist/vcs/index.d.ts +1 -0
  251. package/dist/vcs/index.d.ts.map +1 -1
  252. package/dist/vcs/index.js +81 -72
  253. package/dist/vcs/issue.d.ts +22 -5
  254. package/dist/vcs/issue.d.ts.map +1 -1
  255. package/dist/vcs/lane-materialize.d.ts +5 -0
  256. package/dist/vcs/lane-materialize.d.ts.map +1 -1
  257. package/dist/vcs/op-log.d.ts +28 -2
  258. package/dist/vcs/op-log.d.ts.map +1 -1
  259. package/dist/vcs/ops.d.ts +10 -0
  260. package/dist/vcs/ops.d.ts.map +1 -1
  261. package/dist/vcs/types.d.ts +2 -0
  262. package/dist/vcs/types.d.ts.map +1 -1
  263. package/dist/{vm-config-6xhsj6b3.js → vm-config-JI4OXKDQ.js} +9 -10
  264. package/dist/vue/hooks.d.ts +64 -0
  265. package/dist/vue/hooks.d.ts.map +1 -0
  266. package/dist/vue/index.d.ts +19 -0
  267. package/dist/vue/index.d.ts.map +1 -0
  268. package/dist/vue/index.js +59 -0
  269. package/dist/vue/schema-hooks.d.ts +41 -0
  270. package/dist/vue/schema-hooks.d.ts.map +1 -0
  271. package/dist/vue/schema-hooks.js +64 -0
  272. package/package.json +130 -17
  273. package/dist/deploy-999q207z.js +0 -10
  274. package/dist/engine-y0724kjq.js +0 -8
  275. package/dist/import-s2b8e0ft.js +0 -11
  276. package/dist/index-a76rekgs.js +0 -67
  277. package/dist/index-c9h37r6h.js +0 -1
  278. package/dist/index-hr9qvv77.js +0 -860
  279. package/dist/index-k5b0xskw.js +0 -1
  280. package/dist/index-nzb3am4f.js +0 -80
  281. package/dist/index-wncptktd.js +0 -292
  282. package/dist/index-y6a4kj0p.js +0 -43
  283. package/dist/index-yhwjgfvj.js +0 -342
  284. package/dist/sdk-bepky0xs.js +0 -16
  285. package/dist/server-szdjx0nt.js +0 -13
  286. package/dist/sqlite-backend-0vsmc6qj.js +0 -8
  287. package/dist/tenancy-pjm32b4v.js +0 -14
@@ -0,0 +1,1100 @@
1
+ // src/client/reactive.ts
2
+ var Signal = class {
3
+ _value;
4
+ _subs = /* @__PURE__ */ new Set();
5
+ constructor(initial) {
6
+ this._value = initial;
7
+ }
8
+ get value() {
9
+ return this._value;
10
+ }
11
+ set value(v) {
12
+ if (Object.is(this._value, v)) return;
13
+ this._value = v;
14
+ for (const fn of this._subs) {
15
+ try {
16
+ fn(v);
17
+ } catch {
18
+ }
19
+ }
20
+ }
21
+ /** Subscribe to changes. Immediately called with current value. */
22
+ subscribe(fn) {
23
+ this._subs.add(fn);
24
+ try {
25
+ fn(this._value);
26
+ } catch {
27
+ }
28
+ return () => {
29
+ this._subs.delete(fn);
30
+ };
31
+ }
32
+ /** Read without tracking. */
33
+ peek() {
34
+ return this._value;
35
+ }
36
+ /** Whether any subscriber is registered. */
37
+ hasSubscribers() {
38
+ return this._subs.size > 0;
39
+ }
40
+ };
41
+
42
+ // src/realtime/room.ts
43
+ var RealtimeRoom = class _RealtimeRoom {
44
+ id;
45
+ transport;
46
+ myState;
47
+ peers = /* @__PURE__ */ new Map();
48
+ /** Sender timestamps from presence messages (not local lastSeen). */
49
+ presenceTsByPeer = /* @__PURE__ */ new Map();
50
+ channelHandlers = /* @__PURE__ */ new Map();
51
+ _presence = new Signal([]);
52
+ now;
53
+ heartbeatMs;
54
+ timeoutMs;
55
+ heartbeatTimer;
56
+ unsubscribe;
57
+ closed = false;
58
+ pendingReplay = null;
59
+ /** Ids of broadcasts already delivered — keeps replay idempotent (G-Set). */
60
+ seenMsgIds = /* @__PURE__ */ new Set();
61
+ constructor(opts) {
62
+ this.transport = opts.transport;
63
+ this.id = opts.transport.id;
64
+ this.myState = opts.initialPresence ?? {};
65
+ this.now = opts.now ?? (() => Date.now());
66
+ this.heartbeatMs = opts.heartbeatMs ?? 2e3;
67
+ this.timeoutMs = opts.timeoutMs ?? 6e3;
68
+ this.unsubscribe = this.transport.onMessage((m) => this.handle(m));
69
+ this.recomputePresence();
70
+ }
71
+ /** Join a room and announce presence. */
72
+ static join(opts) {
73
+ const room = new _RealtimeRoom(opts);
74
+ room.announceHello();
75
+ room.announcePresence();
76
+ room.startHeartbeat();
77
+ return room;
78
+ }
79
+ // -------------------------------------------------------------------------
80
+ // Presence
81
+ // -------------------------------------------------------------------------
82
+ /** The local peer id. */
83
+ get selfId() {
84
+ return this.id;
85
+ }
86
+ /** Current local presence state. */
87
+ getSelfState() {
88
+ return this.myState;
89
+ }
90
+ /** Merge a partial update into local presence and broadcast it. */
91
+ setPresence(partial) {
92
+ this.myState = { ...this.myState, ...partial };
93
+ this.recomputePresence();
94
+ this.announcePresence();
95
+ }
96
+ /** Replace local presence wholesale and broadcast it. */
97
+ replacePresence(state) {
98
+ this.myState = state;
99
+ this.recomputePresence();
100
+ this.announcePresence();
101
+ }
102
+ /** All peers including self (self first). */
103
+ getPresence() {
104
+ return this._presence.value;
105
+ }
106
+ /** Peers excluding self. */
107
+ getOthers() {
108
+ return this._presence.value.filter((p) => !p.self);
109
+ }
110
+ /** Subscribe to presence changes. Called immediately with current peers. */
111
+ onPresence(cb) {
112
+ const unsub = this._presence.subscribe(cb);
113
+ this.flushPendingReplay();
114
+ return unsub;
115
+ }
116
+ /** Reactive presence signal (for framework adapters). */
117
+ get presenceSignal() {
118
+ return this._presence;
119
+ }
120
+ // -------------------------------------------------------------------------
121
+ // Broadcast pub/sub
122
+ // -------------------------------------------------------------------------
123
+ /**
124
+ * Fire-and-forget broadcast to all other peers on a channel. Returns the
125
+ * stable message id assigned to this broadcast — persist it alongside an
126
+ * optimistic local render so an echoed copy (relay replay, reconnect) is
127
+ * deduplicated by id rather than re-rendered.
128
+ */
129
+ broadcast(channel, event, payload) {
130
+ const id = this.newMsgId();
131
+ if (this.closed) return id;
132
+ this.seenMsgIds.add(id);
133
+ this.transport.send({
134
+ v: 1,
135
+ t: "msg",
136
+ from: this.id,
137
+ channel,
138
+ event,
139
+ payload,
140
+ ts: this.now(),
141
+ id
142
+ });
143
+ return id;
144
+ }
145
+ newMsgId() {
146
+ const rand = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : Math.random().toString(36).slice(2);
147
+ return `${this.id}:${rand}`;
148
+ }
149
+ /** Subscribe to broadcasts on a channel. Returns an unsubscribe fn. */
150
+ on(channel, handler) {
151
+ let set = this.channelHandlers.get(channel);
152
+ if (!set) {
153
+ set = /* @__PURE__ */ new Set();
154
+ this.channelHandlers.set(channel, set);
155
+ }
156
+ set.add(handler);
157
+ this.flushPendingReplay();
158
+ return () => {
159
+ set.delete(handler);
160
+ };
161
+ }
162
+ // -------------------------------------------------------------------------
163
+ // Lifecycle
164
+ // -------------------------------------------------------------------------
165
+ /**
166
+ * Apply a relay replay batch (chat history, text snapshot, presence).
167
+ * Used when reconnecting to a hub with {@link RelayPersistence}.
168
+ */
169
+ replay(messages) {
170
+ for (const message of messages) {
171
+ this.integrateRemote(message);
172
+ }
173
+ }
174
+ /** Announce departure and tear down. */
175
+ leave() {
176
+ if (this.closed) return;
177
+ this.closed = true;
178
+ try {
179
+ this.transport.send({ v: 1, t: "bye", from: this.id, ts: this.now() });
180
+ } catch {
181
+ }
182
+ if (this.heartbeatTimer !== void 0) {
183
+ clearInterval(this.heartbeatTimer);
184
+ this.heartbeatTimer = void 0;
185
+ }
186
+ this.unsubscribe();
187
+ this.transport.close();
188
+ }
189
+ // -------------------------------------------------------------------------
190
+ // Internal
191
+ // -------------------------------------------------------------------------
192
+ handle(message) {
193
+ if (this.closed) return;
194
+ if (message.t === "replay") {
195
+ this.pendingReplay = message.messages;
196
+ this.flushPendingReplay();
197
+ return;
198
+ }
199
+ if (message.from === this.id) return;
200
+ this.integrateRemote(message);
201
+ }
202
+ flushPendingReplay() {
203
+ if (!this.pendingReplay || this.pendingReplay.length === 0) return;
204
+ if (!this.hasActiveSubscribers()) return;
205
+ const batch = this.pendingReplay;
206
+ this.pendingReplay = null;
207
+ this.replay(batch);
208
+ }
209
+ hasActiveSubscribers() {
210
+ if (this._presence.hasSubscribers()) return true;
211
+ for (const handlers of this.channelHandlers.values()) {
212
+ if (handlers.size > 0) return true;
213
+ }
214
+ return false;
215
+ }
216
+ integrateRemote(message) {
217
+ switch (message.t) {
218
+ case "hello":
219
+ this.announcePresence();
220
+ break;
221
+ case "presence":
222
+ this.upsertPeer(message.from, message.state, message.ts);
223
+ break;
224
+ case "bye":
225
+ if (this.peers.delete(message.from)) {
226
+ this.presenceTsByPeer.delete(message.from);
227
+ this.recomputePresence();
228
+ }
229
+ break;
230
+ case "msg": {
231
+ if (message.id !== void 0 && this.seenMsgIds.has(message.id)) return;
232
+ const handlers = this.channelHandlers.get(message.channel);
233
+ if (!handlers || handlers.size === 0) return;
234
+ if (message.id !== void 0) this.seenMsgIds.add(message.id);
235
+ const event = {
236
+ from: message.from,
237
+ channel: message.channel,
238
+ event: message.event,
239
+ payload: message.payload,
240
+ ts: message.ts,
241
+ id: message.id
242
+ };
243
+ for (const handler of handlers) {
244
+ try {
245
+ handler(event);
246
+ } catch {
247
+ }
248
+ }
249
+ break;
250
+ }
251
+ case "replay":
252
+ break;
253
+ }
254
+ }
255
+ upsertPeer(id, state, ts) {
256
+ const prevTs = this.presenceTsByPeer.get(id) ?? 0;
257
+ if (ts < prevTs) {
258
+ const existing = this.peers.get(id);
259
+ if (existing) existing.lastSeen = this.now();
260
+ return;
261
+ }
262
+ this.presenceTsByPeer.set(id, ts);
263
+ this.peers.set(id, { id, state, lastSeen: this.now(), self: false });
264
+ this.recomputePresence();
265
+ }
266
+ announceHello() {
267
+ this.transport.send({ v: 1, t: "hello", from: this.id });
268
+ }
269
+ announcePresence() {
270
+ this.transport.send({
271
+ v: 1,
272
+ t: "presence",
273
+ from: this.id,
274
+ state: this.myState,
275
+ ts: this.now()
276
+ });
277
+ }
278
+ startHeartbeat() {
279
+ if (this.heartbeatMs <= 0 || typeof setInterval !== "function") {
280
+ return;
281
+ }
282
+ this.heartbeatTimer = setInterval(() => {
283
+ this.announcePresence();
284
+ this.pruneExpired();
285
+ }, this.heartbeatMs);
286
+ }
287
+ pruneExpired() {
288
+ const cutoff = this.now() - this.timeoutMs;
289
+ let changed = false;
290
+ for (const [id, peer] of this.peers) {
291
+ if (peer.lastSeen < cutoff) {
292
+ this.peers.delete(id);
293
+ this.presenceTsByPeer.delete(id);
294
+ changed = true;
295
+ }
296
+ }
297
+ if (changed) this.recomputePresence();
298
+ }
299
+ recomputePresence() {
300
+ const self = {
301
+ id: this.id,
302
+ state: this.myState,
303
+ lastSeen: this.now(),
304
+ self: true
305
+ };
306
+ const others = [...this.peers.values()].sort(
307
+ (a, b) => a.id.localeCompare(b.id)
308
+ );
309
+ this._presence.value = [self, ...others];
310
+ }
311
+ };
312
+
313
+ // src/realtime/memory-hub.ts
314
+ var MemoryHub = class {
315
+ transports = /* @__PURE__ */ new Set();
316
+ /** Create a transport bound to this hub for the given peer id. */
317
+ connect(id) {
318
+ const transport = new MemoryRealtimeTransport(this, id);
319
+ this.transports.add(transport);
320
+ return transport;
321
+ }
322
+ /** Number of currently connected transports. */
323
+ size() {
324
+ return this.transports.size;
325
+ }
326
+ /** @internal */
327
+ _broadcast(from, message) {
328
+ for (const transport of this.transports) {
329
+ if (transport === from) continue;
330
+ transport._deliver(message);
331
+ }
332
+ }
333
+ /** @internal */
334
+ _remove(transport) {
335
+ this.transports.delete(transport);
336
+ }
337
+ };
338
+ var MemoryRealtimeTransport = class {
339
+ id;
340
+ hub;
341
+ handlers = /* @__PURE__ */ new Set();
342
+ closed = false;
343
+ constructor(hub, id) {
344
+ this.hub = hub;
345
+ this.id = id;
346
+ }
347
+ send(message) {
348
+ if (this.closed) return;
349
+ this.hub._broadcast(this, message);
350
+ }
351
+ onMessage(handler) {
352
+ this.handlers.add(handler);
353
+ return () => {
354
+ this.handlers.delete(handler);
355
+ };
356
+ }
357
+ close() {
358
+ if (this.closed) return;
359
+ this.closed = true;
360
+ this.hub._remove(this);
361
+ this.handlers.clear();
362
+ }
363
+ /** @internal */
364
+ _deliver(message) {
365
+ if (this.closed) return;
366
+ for (const handler of this.handlers) {
367
+ try {
368
+ handler(message);
369
+ } catch {
370
+ }
371
+ }
372
+ }
373
+ };
374
+
375
+ // src/realtime/broadcast-channel-transport.ts
376
+ var BroadcastChannelTransport = class {
377
+ id;
378
+ bc;
379
+ handlers = /* @__PURE__ */ new Set();
380
+ closed = false;
381
+ constructor(opts) {
382
+ this.id = opts.id;
383
+ const Impl = opts.BroadcastChannelImpl ?? globalThis.BroadcastChannel;
384
+ if (!Impl) {
385
+ throw new Error(
386
+ "BroadcastChannelTransport requires BroadcastChannel or opts.BroadcastChannelImpl."
387
+ );
388
+ }
389
+ this.bc = new Impl(opts.channel);
390
+ const onIncoming = (event) => {
391
+ const message = event.data;
392
+ if (!message || typeof message !== "object" || message.v !== 1) return;
393
+ for (const handler of this.handlers) {
394
+ try {
395
+ handler(message);
396
+ } catch {
397
+ }
398
+ }
399
+ };
400
+ if (this.bc.addEventListener) {
401
+ this.bc.addEventListener("message", onIncoming);
402
+ } else {
403
+ this.bc.onmessage = onIncoming;
404
+ }
405
+ }
406
+ send(message) {
407
+ if (this.closed) return;
408
+ this.bc.postMessage(message);
409
+ }
410
+ onMessage(handler) {
411
+ this.handlers.add(handler);
412
+ return () => {
413
+ this.handlers.delete(handler);
414
+ };
415
+ }
416
+ close() {
417
+ if (this.closed) return;
418
+ this.closed = true;
419
+ this.handlers.clear();
420
+ try {
421
+ this.bc.close();
422
+ } catch {
423
+ }
424
+ }
425
+ };
426
+
427
+ // src/realtime/websocket-relay-transport.ts
428
+ var WebSocketRelayTransport = class {
429
+ id;
430
+ ws;
431
+ handlers = /* @__PURE__ */ new Set();
432
+ closed = false;
433
+ pending = [];
434
+ constructor(opts) {
435
+ this.id = opts.id;
436
+ const WS = opts.WebSocketImpl ?? globalThis.WebSocket;
437
+ if (!WS) {
438
+ throw new Error(
439
+ "WebSocketRelayTransport requires WebSocket or opts.WebSocketImpl."
440
+ );
441
+ }
442
+ this.ws = new WS(opts.url);
443
+ this.ws.addEventListener("open", () => this.flushPending());
444
+ this.ws.addEventListener("message", (event) => {
445
+ let message;
446
+ try {
447
+ message = JSON.parse(String(event.data));
448
+ } catch {
449
+ return;
450
+ }
451
+ if (!message || typeof message !== "object" || message.v !== 1) return;
452
+ for (const handler of this.handlers) {
453
+ try {
454
+ handler(message);
455
+ } catch {
456
+ }
457
+ }
458
+ });
459
+ this.ws.addEventListener("close", () => {
460
+ if (!this.closed) this.pending = [];
461
+ });
462
+ }
463
+ send(message) {
464
+ if (this.closed) return;
465
+ if (this.ws.readyState === 1) {
466
+ this.ws.send(JSON.stringify(message));
467
+ return;
468
+ }
469
+ this.pending.push(message);
470
+ }
471
+ onMessage(handler) {
472
+ this.handlers.add(handler);
473
+ return () => {
474
+ this.handlers.delete(handler);
475
+ };
476
+ }
477
+ close() {
478
+ if (this.closed) return;
479
+ this.closed = true;
480
+ this.pending = [];
481
+ this.handlers.clear();
482
+ try {
483
+ this.ws.close();
484
+ } catch {
485
+ }
486
+ }
487
+ flushPending() {
488
+ if (this.ws.readyState !== 1) return;
489
+ const batch = this.pending.splice(0);
490
+ for (const message of batch) {
491
+ this.ws.send(JSON.stringify(message));
492
+ }
493
+ }
494
+ };
495
+
496
+ // src/realtime/durable-object-relay-transport.ts
497
+ var DEFAULT_RECONNECT = {
498
+ maxAttempts: 0,
499
+ baseDelayMs: 500,
500
+ maxDelayMs: 1e4
501
+ };
502
+ function defaultBuildUrl(opts) {
503
+ let url = opts.url;
504
+ if (opts.room) {
505
+ url = `${url.replace(/\/$/, "")}/${encodeURIComponent(opts.room)}`;
506
+ }
507
+ if (opts.auth) {
508
+ url += `${url.includes("?") ? "&" : "?"}token=${encodeURIComponent(opts.auth)}`;
509
+ }
510
+ return url;
511
+ }
512
+ var DurableObjectRelayTransport = class {
513
+ id;
514
+ url;
515
+ WS;
516
+ reconnect;
517
+ maxPending;
518
+ ws = null;
519
+ handlers = /* @__PURE__ */ new Set();
520
+ pending = [];
521
+ closed = false;
522
+ attempts = 0;
523
+ reconnectTimer = null;
524
+ constructor(opts) {
525
+ this.id = opts.id;
526
+ const WS = opts.WebSocketImpl ?? globalThis.WebSocket;
527
+ if (!WS) {
528
+ throw new Error(
529
+ "DurableObjectRelayTransport requires WebSocket or opts.WebSocketImpl."
530
+ );
531
+ }
532
+ this.WS = WS;
533
+ this.url = (opts.buildUrl ?? defaultBuildUrl)({
534
+ url: opts.url,
535
+ room: opts.room,
536
+ auth: opts.auth
537
+ });
538
+ this.reconnect = opts.reconnect === false ? null : { ...DEFAULT_RECONNECT, ...opts.reconnect ?? {} };
539
+ this.maxPending = opts.maxPending ?? 128;
540
+ this.open();
541
+ }
542
+ send(message) {
543
+ if (this.closed) return;
544
+ if (this.ws && this.ws.readyState === 1) {
545
+ this.ws.send(JSON.stringify(message));
546
+ return;
547
+ }
548
+ this.pending.push(message);
549
+ if (this.pending.length > this.maxPending) {
550
+ this.pending.splice(0, this.pending.length - this.maxPending);
551
+ }
552
+ }
553
+ onMessage(handler) {
554
+ this.handlers.add(handler);
555
+ return () => {
556
+ this.handlers.delete(handler);
557
+ };
558
+ }
559
+ close() {
560
+ if (this.closed) return;
561
+ this.closed = true;
562
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
563
+ this.reconnectTimer = null;
564
+ this.pending = [];
565
+ this.handlers.clear();
566
+ try {
567
+ this.ws?.close();
568
+ } catch {
569
+ }
570
+ this.ws = null;
571
+ }
572
+ open() {
573
+ if (this.closed) return;
574
+ const ws = new this.WS(this.url);
575
+ this.ws = ws;
576
+ ws.addEventListener("open", () => {
577
+ if (this.closed) return;
578
+ this.attempts = 0;
579
+ try {
580
+ ws.send(JSON.stringify({ v: 1, t: "hello", from: this.id }));
581
+ } catch {
582
+ }
583
+ this.flushPending();
584
+ });
585
+ ws.addEventListener("message", (event) => {
586
+ const data = event.data;
587
+ let message;
588
+ try {
589
+ message = JSON.parse(String(data));
590
+ } catch {
591
+ return;
592
+ }
593
+ if (!message || typeof message !== "object" || message.v !== 1) return;
594
+ for (const handler of this.handlers) {
595
+ try {
596
+ handler(message);
597
+ } catch {
598
+ }
599
+ }
600
+ });
601
+ ws.addEventListener("close", () => {
602
+ if (this.ws === ws) this.ws = null;
603
+ this.scheduleReconnect();
604
+ });
605
+ ws.addEventListener("error", () => {
606
+ });
607
+ }
608
+ scheduleReconnect() {
609
+ if (this.closed || !this.reconnect || this.reconnectTimer) return;
610
+ if (this.reconnect.maxAttempts > 0 && this.attempts >= this.reconnect.maxAttempts) {
611
+ return;
612
+ }
613
+ const delay = Math.min(
614
+ this.reconnect.baseDelayMs * 2 ** this.attempts,
615
+ this.reconnect.maxDelayMs
616
+ );
617
+ this.attempts += 1;
618
+ this.reconnectTimer = setTimeout(() => {
619
+ this.reconnectTimer = null;
620
+ this.open();
621
+ }, delay);
622
+ this.reconnectTimer?.unref?.();
623
+ }
624
+ flushPending() {
625
+ if (!this.ws || this.ws.readyState !== 1) return;
626
+ const batch = this.pending.splice(0);
627
+ for (const message of batch) {
628
+ this.ws.send(JSON.stringify(message));
629
+ }
630
+ }
631
+ };
632
+
633
+ // src/realtime/presence.ts
634
+ function createPresenceTransport(opts) {
635
+ if (opts.transport) return opts.transport;
636
+ if (opts.relayUrl) {
637
+ return new DurableObjectRelayTransport({
638
+ id: opts.peerId,
639
+ url: opts.relayUrl,
640
+ room: opts.room,
641
+ auth: opts.auth,
642
+ WebSocketImpl: opts.WebSocketImpl
643
+ });
644
+ }
645
+ return new BroadcastChannelTransport({
646
+ id: opts.peerId,
647
+ channel: `presence:${opts.room}`,
648
+ BroadcastChannelImpl: opts.BroadcastChannelImpl
649
+ });
650
+ }
651
+ function joinPresence(opts) {
652
+ return RealtimeRoom.join({
653
+ transport: createPresenceTransport(opts),
654
+ initialPresence: opts.initialPresence,
655
+ heartbeatMs: opts.heartbeatMs,
656
+ timeoutMs: opts.timeoutMs,
657
+ now: opts.now
658
+ });
659
+ }
660
+
661
+ // src/realtime/relay-persistence.ts
662
+ var DEFAULT_MAX_CHAT = 200;
663
+ var DEFAULT_MAX_TEXT_OPS = 2e3;
664
+ function chatKey(msg) {
665
+ return msg.id ?? `${msg.from}:${msg.ts}`;
666
+ }
667
+ function tieKey(msg) {
668
+ if (msg.t === "msg") return chatKey(msg);
669
+ if (msg.t === "presence" || msg.t === "bye") return msg.from;
670
+ return "";
671
+ }
672
+ function messageTs(msg) {
673
+ if (msg.t === "presence" || msg.t === "bye" || msg.t === "msg") {
674
+ return msg.ts;
675
+ }
676
+ return 0;
677
+ }
678
+ var RelayPersistence = class {
679
+ maxChat;
680
+ maxTextOps;
681
+ presence = /* @__PURE__ */ new Map();
682
+ /** Chat is a grow-only set keyed by {@link chatKey} — dedup on record. */
683
+ chatLog = [];
684
+ chatKeys = /* @__PURE__ */ new Set();
685
+ textSnapshot = null;
686
+ textOps = [];
687
+ constructor(opts = {}) {
688
+ this.maxChat = opts.maxChat ?? DEFAULT_MAX_CHAT;
689
+ this.maxTextOps = opts.maxTextOps ?? DEFAULT_MAX_TEXT_OPS;
690
+ }
691
+ /** Record an inbound client message (skip `hello` / `replay`). */
692
+ record(message) {
693
+ if (message.v !== 1) return;
694
+ switch (message.t) {
695
+ case "hello":
696
+ return;
697
+ case "replay":
698
+ return;
699
+ case "presence":
700
+ this.presence.set(message.from, message);
701
+ return;
702
+ case "bye":
703
+ this.presence.delete(message.from);
704
+ return;
705
+ case "msg":
706
+ this.recordBroadcast(message);
707
+ return;
708
+ }
709
+ }
710
+ /** Ordered messages to replay to a peer that just connected. */
711
+ buildReplay() {
712
+ const out = [
713
+ ...this.presence.values(),
714
+ ...this.chatLog
715
+ ];
716
+ if (this.textSnapshot) {
717
+ out.push(this.textSnapshot);
718
+ } else {
719
+ out.push(...this.textOps);
720
+ }
721
+ out.sort((a, b) => {
722
+ const dt = messageTs(a) - messageTs(b);
723
+ if (dt !== 0) return dt;
724
+ return tieKey(a).localeCompare(tieKey(b));
725
+ });
726
+ return out;
727
+ }
728
+ getPresenceCount() {
729
+ return this.presence.size;
730
+ }
731
+ getChatCount() {
732
+ return this.chatLog.length;
733
+ }
734
+ hasTextSnapshot() {
735
+ return this.textSnapshot !== null;
736
+ }
737
+ recordBroadcast(message) {
738
+ if (message.channel === "chat" && message.event === "message") {
739
+ const key = chatKey(message);
740
+ if (this.chatKeys.has(key)) return;
741
+ this.chatKeys.add(key);
742
+ this.chatLog.push(message);
743
+ if (this.chatLog.length > this.maxChat) {
744
+ const evicted = this.chatLog.splice(0, this.chatLog.length - this.maxChat);
745
+ for (const m of evicted) this.chatKeys.delete(chatKey(m));
746
+ }
747
+ return;
748
+ }
749
+ if (message.channel === "text" && message.event === "state") {
750
+ this.textSnapshot = message;
751
+ this.textOps = [];
752
+ return;
753
+ }
754
+ if (message.channel === "text" && message.event === "op") {
755
+ if (this.textSnapshot) return;
756
+ this.textOps.push(message);
757
+ if (this.textOps.length > this.maxTextOps) {
758
+ this.textOps.splice(0, this.textOps.length - this.maxTextOps);
759
+ }
760
+ }
761
+ }
762
+ };
763
+
764
+ // src/realtime/persistent-channel.ts
765
+ var DEFAULT_MAX_RECORDS = 200;
766
+ var PersistentChannel = class _PersistentChannel {
767
+ /** Reactive, deduped, `(ts, id)`-sorted message list. */
768
+ messages = new Signal([]);
769
+ room;
770
+ channel;
771
+ event;
772
+ store;
773
+ max;
774
+ resolveMeta;
775
+ now;
776
+ records = [];
777
+ byId = /* @__PURE__ */ new Map();
778
+ unsubscribe;
779
+ disposed = false;
780
+ constructor(room, channel, opts) {
781
+ this.room = room;
782
+ this.channel = channel;
783
+ this.event = opts.event ?? "message";
784
+ this.store = opts.store;
785
+ this.max = opts.max ?? DEFAULT_MAX_RECORDS;
786
+ this.resolveMeta = opts.resolveMeta;
787
+ this.now = opts.now ?? (() => Date.now());
788
+ this.unsubscribe = this.room.on(this.channel, this.handleEvent);
789
+ void this.hydrate();
790
+ }
791
+ /** Create a persistent view over `room`'s `channel`. */
792
+ static create(room, channel, opts = {}) {
793
+ return new _PersistentChannel(room, channel, opts);
794
+ }
795
+ /**
796
+ * Optimistically record locally, broadcast to peers, and persist. Returns the
797
+ * stable message id. The room dedups its own id, so the broadcast never
798
+ * echoes back into this channel.
799
+ */
800
+ send(payload, meta) {
801
+ const id = this.room.broadcast(this.channel, this.event, payload);
802
+ this.commitOne({ id, from: this.room.selfId, ts: this.now(), payload, meta });
803
+ return id;
804
+ }
805
+ /** Current records (deduped + sorted). Read without subscribing. */
806
+ snapshot() {
807
+ return this.messages.peek();
808
+ }
809
+ /** Stop listening. Does not clear the durable store. */
810
+ dispose() {
811
+ if (this.disposed) return;
812
+ this.disposed = true;
813
+ this.unsubscribe();
814
+ }
815
+ // --- internal --------------------------------------------------------------
816
+ handleEvent = (event) => {
817
+ if (event.event !== this.event) return;
818
+ this.commitOne({
819
+ id: event.id ?? `${event.from}:${event.ts}`,
820
+ from: event.from,
821
+ ts: event.ts,
822
+ payload: event.payload,
823
+ meta: this.resolveMeta?.(event)
824
+ });
825
+ };
826
+ async hydrate() {
827
+ if (!this.store) return;
828
+ let loaded;
829
+ try {
830
+ loaded = await this.store.load();
831
+ } catch {
832
+ return;
833
+ }
834
+ if (this.disposed) return;
835
+ let changed = false;
836
+ for (const rec of loaded) {
837
+ if (this.add(rec)) changed = true;
838
+ }
839
+ if (changed) this.commit();
840
+ }
841
+ /** Insert one record and publish if it was new. */
842
+ commitOne(rec) {
843
+ if (this.add(rec)) this.commit();
844
+ }
845
+ /** Pure insert with dedup + cap. Returns true when a new record was added. */
846
+ add(rec) {
847
+ if (!rec || !rec.id || this.byId.has(rec.id)) return false;
848
+ this.byId.set(rec.id, rec);
849
+ this.records.push(rec);
850
+ this.records.sort((a, b) => a.ts - b.ts || a.id.localeCompare(b.id));
851
+ if (this.records.length > this.max) {
852
+ const evicted = this.records.splice(0, this.records.length - this.max);
853
+ for (const m of evicted) this.byId.delete(m.id);
854
+ }
855
+ return true;
856
+ }
857
+ commit() {
858
+ this.messages.value = [...this.records];
859
+ void this.store?.save(this.records);
860
+ }
861
+ };
862
+ function localStorageChannelStore(key, storage = globalThis.localStorage) {
863
+ return {
864
+ load() {
865
+ try {
866
+ const raw = storage?.getItem(key);
867
+ const parsed = raw ? JSON.parse(raw) : [];
868
+ return Array.isArray(parsed) ? parsed : [];
869
+ } catch {
870
+ return [];
871
+ }
872
+ },
873
+ save(records) {
874
+ try {
875
+ storage?.setItem(key, JSON.stringify(records));
876
+ } catch {
877
+ }
878
+ }
879
+ };
880
+ }
881
+
882
+ // src/realtime/text.ts
883
+ function parseId(id) {
884
+ const at = id.indexOf("@");
885
+ return {
886
+ counter: Number(id.slice(0, at)),
887
+ peer: id.slice(at + 1)
888
+ };
889
+ }
890
+ function compareSiblings(a, b) {
891
+ const pa = parseId(a);
892
+ const pb = parseId(b);
893
+ if (pa.counter !== pb.counter) return pb.counter - pa.counter;
894
+ return pb.peer.localeCompare(pa.peer);
895
+ }
896
+ var RealtimeText = class {
897
+ nodes = /* @__PURE__ */ new Map();
898
+ counter = 0;
899
+ peerId;
900
+ listeners = /* @__PURE__ */ new Set();
901
+ room;
902
+ channel;
903
+ unsubscribe;
904
+ constructor(opts) {
905
+ this.peerId = opts.peerId;
906
+ this.room = opts.room;
907
+ this.channel = opts.channel ?? "text";
908
+ if (this.room) {
909
+ this.unsubscribe = this.room.on(this.channel, (e) => {
910
+ if (e.from === this.peerId) return;
911
+ this.onRemote(e.event, e.payload);
912
+ });
913
+ this.room.broadcast(this.channel, "state-req", {});
914
+ }
915
+ }
916
+ /** Materialized visible text. */
917
+ toString() {
918
+ return this.visibleNodes().map((n) => n.ch).join("");
919
+ }
920
+ /** Current length of visible text. */
921
+ get length() {
922
+ return this.visibleNodes().length;
923
+ }
924
+ /** Insert a string at a visible index. Broadcasts ops when room-bound. */
925
+ insert(index, str) {
926
+ if (str.length === 0) return [];
927
+ const visible = this.visibleNodes();
928
+ let after = index <= 0 ? null : visible[index - 1]?.id ?? null;
929
+ const ops = [];
930
+ for (const ch of str) {
931
+ const id = this.nextId();
932
+ const node = { id, ch, after, deleted: false };
933
+ this.nodes.set(id, node);
934
+ ops.push({ op: "ins", id, ch, after });
935
+ after = id;
936
+ }
937
+ this.emit();
938
+ this.publish(ops);
939
+ return ops;
940
+ }
941
+ /** Delete `count` visible characters starting at `index`. */
942
+ delete(index, count = 1) {
943
+ if (count <= 0) return [];
944
+ const visible = this.visibleNodes();
945
+ const ops = [];
946
+ for (let k = 0; k < count; k++) {
947
+ const target = visible[index + k];
948
+ if (!target) break;
949
+ const node = this.nodes.get(target.id);
950
+ if (node && !node.deleted) {
951
+ node.deleted = true;
952
+ ops.push({ op: "del", id: node.id });
953
+ }
954
+ }
955
+ if (ops.length > 0) {
956
+ this.emit();
957
+ this.publish(ops);
958
+ }
959
+ return ops;
960
+ }
961
+ /** Apply a single remote op. Returns true if the document changed. */
962
+ applyOp(op) {
963
+ if (op.op === "ins") {
964
+ if (this.nodes.has(op.id)) return false;
965
+ this.bumpCounter(op.id);
966
+ this.nodes.set(op.id, {
967
+ id: op.id,
968
+ ch: op.ch,
969
+ after: op.after,
970
+ deleted: false
971
+ });
972
+ return true;
973
+ }
974
+ const node = this.nodes.get(op.id);
975
+ if (!node || node.deleted) return false;
976
+ node.deleted = true;
977
+ return true;
978
+ }
979
+ /** Apply a batch of remote ops, emitting once. */
980
+ applyOps(ops) {
981
+ let changed = false;
982
+ for (const op of ops) {
983
+ if (this.applyOp(op)) changed = true;
984
+ }
985
+ if (changed) this.emit();
986
+ }
987
+ /** Snapshot of all nodes (including tombstones) for state sync. */
988
+ getNodes() {
989
+ return [...this.nodes.values()];
990
+ }
991
+ /** Merge a remote snapshot of nodes. */
992
+ mergeNodes(nodes) {
993
+ let changed = false;
994
+ for (const incoming of nodes) {
995
+ this.bumpCounter(incoming.id);
996
+ const existing = this.nodes.get(incoming.id);
997
+ if (!existing) {
998
+ this.nodes.set(incoming.id, { ...incoming });
999
+ changed = true;
1000
+ } else if (incoming.deleted && !existing.deleted) {
1001
+ existing.deleted = true;
1002
+ changed = true;
1003
+ }
1004
+ }
1005
+ if (changed) this.emit();
1006
+ }
1007
+ /** Subscribe to text changes. Returns an unsubscribe fn. */
1008
+ onChange(cb) {
1009
+ this.listeners.add(cb);
1010
+ return () => {
1011
+ this.listeners.delete(cb);
1012
+ };
1013
+ }
1014
+ /** Detach room bindings. */
1015
+ dispose() {
1016
+ this.unsubscribe?.();
1017
+ this.listeners.clear();
1018
+ }
1019
+ // -------------------------------------------------------------------------
1020
+ // Internal
1021
+ // -------------------------------------------------------------------------
1022
+ onRemote(event, payload) {
1023
+ switch (event) {
1024
+ case "op":
1025
+ this.applyOps(payload);
1026
+ break;
1027
+ case "state-req":
1028
+ this.room?.broadcast(this.channel, "state", this.getNodes());
1029
+ break;
1030
+ case "state":
1031
+ this.mergeNodes(payload);
1032
+ break;
1033
+ }
1034
+ }
1035
+ publish(ops) {
1036
+ this.room?.broadcast(this.channel, "op", ops);
1037
+ }
1038
+ visibleNodes() {
1039
+ const children = /* @__PURE__ */ new Map();
1040
+ for (const node of this.nodes.values()) {
1041
+ const list = children.get(node.after);
1042
+ if (list) list.push(node);
1043
+ else children.set(node.after, [node]);
1044
+ }
1045
+ for (const list of children.values()) {
1046
+ list.sort((a, b) => compareSiblings(a.id, b.id));
1047
+ }
1048
+ const result = [];
1049
+ const stack = [...children.get(null) ?? []].reverse();
1050
+ while (stack.length > 0) {
1051
+ const node = stack.pop();
1052
+ if (!node.deleted) result.push(node);
1053
+ const kids = children.get(node.id);
1054
+ if (kids) {
1055
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
1056
+ }
1057
+ }
1058
+ return result;
1059
+ }
1060
+ nextId() {
1061
+ this.counter += 1;
1062
+ return `${this.counter}@${this.peerId}`;
1063
+ }
1064
+ bumpCounter(id) {
1065
+ const { counter } = parseId(id);
1066
+ if (Number.isFinite(counter) && counter > this.counter) {
1067
+ this.counter = counter;
1068
+ }
1069
+ }
1070
+ emit() {
1071
+ const text = this.toString();
1072
+ for (const cb of this.listeners) {
1073
+ try {
1074
+ cb(text);
1075
+ } catch {
1076
+ }
1077
+ }
1078
+ }
1079
+ };
1080
+
1081
+ // src/realtime/types.ts
1082
+ var REALTIME_PROTOCOL = 1;
1083
+ export {
1084
+ BroadcastChannelTransport,
1085
+ DEFAULT_MAX_CHAT,
1086
+ DEFAULT_MAX_RECORDS,
1087
+ DEFAULT_MAX_TEXT_OPS,
1088
+ DurableObjectRelayTransport,
1089
+ MemoryHub,
1090
+ MemoryRealtimeTransport,
1091
+ PersistentChannel,
1092
+ REALTIME_PROTOCOL,
1093
+ RealtimeRoom,
1094
+ RealtimeText,
1095
+ RelayPersistence,
1096
+ WebSocketRelayTransport,
1097
+ createPresenceTransport,
1098
+ joinPresence,
1099
+ localStorageChannelStore
1100
+ };