@noy-db/hub 0.1.0-pre.3

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/dist/aggregate/index.cjs +476 -0
  4. package/dist/aggregate/index.cjs.map +1 -0
  5. package/dist/aggregate/index.d.cts +38 -0
  6. package/dist/aggregate/index.d.ts +38 -0
  7. package/dist/aggregate/index.js +53 -0
  8. package/dist/aggregate/index.js.map +1 -0
  9. package/dist/blobs/index.cjs +1480 -0
  10. package/dist/blobs/index.cjs.map +1 -0
  11. package/dist/blobs/index.d.cts +45 -0
  12. package/dist/blobs/index.d.ts +45 -0
  13. package/dist/blobs/index.js +48 -0
  14. package/dist/blobs/index.js.map +1 -0
  15. package/dist/bundle/index.cjs +436 -0
  16. package/dist/bundle/index.cjs.map +1 -0
  17. package/dist/bundle/index.d.cts +7 -0
  18. package/dist/bundle/index.d.ts +7 -0
  19. package/dist/bundle/index.js +40 -0
  20. package/dist/bundle/index.js.map +1 -0
  21. package/dist/chunk-2QR2PQTT.js +217 -0
  22. package/dist/chunk-2QR2PQTT.js.map +1 -0
  23. package/dist/chunk-4OWFYIDQ.js +79 -0
  24. package/dist/chunk-4OWFYIDQ.js.map +1 -0
  25. package/dist/chunk-5AATM2M2.js +90 -0
  26. package/dist/chunk-5AATM2M2.js.map +1 -0
  27. package/dist/chunk-ACLDOTNQ.js +543 -0
  28. package/dist/chunk-ACLDOTNQ.js.map +1 -0
  29. package/dist/chunk-BTDCBVJW.js +160 -0
  30. package/dist/chunk-BTDCBVJW.js.map +1 -0
  31. package/dist/chunk-CIMZBAZB.js +72 -0
  32. package/dist/chunk-CIMZBAZB.js.map +1 -0
  33. package/dist/chunk-E445ICYI.js +365 -0
  34. package/dist/chunk-E445ICYI.js.map +1 -0
  35. package/dist/chunk-EXQRC2L4.js +722 -0
  36. package/dist/chunk-EXQRC2L4.js.map +1 -0
  37. package/dist/chunk-FZU343FL.js +32 -0
  38. package/dist/chunk-FZU343FL.js.map +1 -0
  39. package/dist/chunk-GJILMRPO.js +354 -0
  40. package/dist/chunk-GJILMRPO.js.map +1 -0
  41. package/dist/chunk-GOUT6DND.js +1285 -0
  42. package/dist/chunk-GOUT6DND.js.map +1 -0
  43. package/dist/chunk-J66GRPNH.js +111 -0
  44. package/dist/chunk-J66GRPNH.js.map +1 -0
  45. package/dist/chunk-M2F2JAWB.js +464 -0
  46. package/dist/chunk-M2F2JAWB.js.map +1 -0
  47. package/dist/chunk-M5INGEFC.js +84 -0
  48. package/dist/chunk-M5INGEFC.js.map +1 -0
  49. package/dist/chunk-M62XNWRA.js +72 -0
  50. package/dist/chunk-M62XNWRA.js.map +1 -0
  51. package/dist/chunk-MR4424N3.js +275 -0
  52. package/dist/chunk-MR4424N3.js.map +1 -0
  53. package/dist/chunk-NPC4LFV5.js +132 -0
  54. package/dist/chunk-NPC4LFV5.js.map +1 -0
  55. package/dist/chunk-NXFEYLVG.js +311 -0
  56. package/dist/chunk-NXFEYLVG.js.map +1 -0
  57. package/dist/chunk-R36SIKES.js +79 -0
  58. package/dist/chunk-R36SIKES.js.map +1 -0
  59. package/dist/chunk-TDR6T5CJ.js +381 -0
  60. package/dist/chunk-TDR6T5CJ.js.map +1 -0
  61. package/dist/chunk-UF3BUNQZ.js +1 -0
  62. package/dist/chunk-UF3BUNQZ.js.map +1 -0
  63. package/dist/chunk-UQFSPSWG.js +1109 -0
  64. package/dist/chunk-UQFSPSWG.js.map +1 -0
  65. package/dist/chunk-USKYUS74.js +793 -0
  66. package/dist/chunk-USKYUS74.js.map +1 -0
  67. package/dist/chunk-XCL3WP6J.js +121 -0
  68. package/dist/chunk-XCL3WP6J.js.map +1 -0
  69. package/dist/chunk-XHFOENR2.js +680 -0
  70. package/dist/chunk-XHFOENR2.js.map +1 -0
  71. package/dist/chunk-ZFKD4QMV.js +430 -0
  72. package/dist/chunk-ZFKD4QMV.js.map +1 -0
  73. package/dist/chunk-ZLMV3TUA.js +490 -0
  74. package/dist/chunk-ZLMV3TUA.js.map +1 -0
  75. package/dist/chunk-ZRG4V3F5.js +17 -0
  76. package/dist/chunk-ZRG4V3F5.js.map +1 -0
  77. package/dist/consent/index.cjs +204 -0
  78. package/dist/consent/index.cjs.map +1 -0
  79. package/dist/consent/index.d.cts +24 -0
  80. package/dist/consent/index.d.ts +24 -0
  81. package/dist/consent/index.js +23 -0
  82. package/dist/consent/index.js.map +1 -0
  83. package/dist/crdt/index.cjs +152 -0
  84. package/dist/crdt/index.cjs.map +1 -0
  85. package/dist/crdt/index.d.cts +30 -0
  86. package/dist/crdt/index.d.ts +30 -0
  87. package/dist/crdt/index.js +24 -0
  88. package/dist/crdt/index.js.map +1 -0
  89. package/dist/crypto-IVKU7YTT.js +44 -0
  90. package/dist/crypto-IVKU7YTT.js.map +1 -0
  91. package/dist/delegation-XDJCBTI2.js +16 -0
  92. package/dist/delegation-XDJCBTI2.js.map +1 -0
  93. package/dist/dev-unlock-CeXic1xC.d.cts +263 -0
  94. package/dist/dev-unlock-KrKkcqD3.d.ts +263 -0
  95. package/dist/hash-9KO1BGxh.d.cts +63 -0
  96. package/dist/hash-ChfJjRjQ.d.ts +63 -0
  97. package/dist/history/index.cjs +1215 -0
  98. package/dist/history/index.cjs.map +1 -0
  99. package/dist/history/index.d.cts +62 -0
  100. package/dist/history/index.d.ts +62 -0
  101. package/dist/history/index.js +79 -0
  102. package/dist/history/index.js.map +1 -0
  103. package/dist/i18n/index.cjs +746 -0
  104. package/dist/i18n/index.cjs.map +1 -0
  105. package/dist/i18n/index.d.cts +38 -0
  106. package/dist/i18n/index.d.ts +38 -0
  107. package/dist/i18n/index.js +55 -0
  108. package/dist/i18n/index.js.map +1 -0
  109. package/dist/index-BRHBCmLt.d.ts +1940 -0
  110. package/dist/index-C8kQtmOk.d.ts +380 -0
  111. package/dist/index-DN-J-5wT.d.cts +1940 -0
  112. package/dist/index-DhjMjz7L.d.cts +380 -0
  113. package/dist/index.cjs +14756 -0
  114. package/dist/index.cjs.map +1 -0
  115. package/dist/index.d.cts +269 -0
  116. package/dist/index.d.ts +269 -0
  117. package/dist/index.js +6085 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/indexing/index.cjs +736 -0
  120. package/dist/indexing/index.cjs.map +1 -0
  121. package/dist/indexing/index.d.cts +36 -0
  122. package/dist/indexing/index.d.ts +36 -0
  123. package/dist/indexing/index.js +77 -0
  124. package/dist/indexing/index.js.map +1 -0
  125. package/dist/lazy-builder-BwEoBQZ9.d.ts +304 -0
  126. package/dist/lazy-builder-CZVLKh0Z.d.cts +304 -0
  127. package/dist/ledger-2NX4L7PN.js +33 -0
  128. package/dist/ledger-2NX4L7PN.js.map +1 -0
  129. package/dist/mime-magic-CBBSOkjm.d.cts +50 -0
  130. package/dist/mime-magic-CBBSOkjm.d.ts +50 -0
  131. package/dist/periods/index.cjs +1035 -0
  132. package/dist/periods/index.cjs.map +1 -0
  133. package/dist/periods/index.d.cts +21 -0
  134. package/dist/periods/index.d.ts +21 -0
  135. package/dist/periods/index.js +25 -0
  136. package/dist/periods/index.js.map +1 -0
  137. package/dist/predicate-SBHmi6D0.d.cts +161 -0
  138. package/dist/predicate-SBHmi6D0.d.ts +161 -0
  139. package/dist/query/index.cjs +1957 -0
  140. package/dist/query/index.cjs.map +1 -0
  141. package/dist/query/index.d.cts +3 -0
  142. package/dist/query/index.d.ts +3 -0
  143. package/dist/query/index.js +62 -0
  144. package/dist/query/index.js.map +1 -0
  145. package/dist/session/index.cjs +487 -0
  146. package/dist/session/index.cjs.map +1 -0
  147. package/dist/session/index.d.cts +45 -0
  148. package/dist/session/index.d.ts +45 -0
  149. package/dist/session/index.js +44 -0
  150. package/dist/session/index.js.map +1 -0
  151. package/dist/shadow/index.cjs +133 -0
  152. package/dist/shadow/index.cjs.map +1 -0
  153. package/dist/shadow/index.d.cts +16 -0
  154. package/dist/shadow/index.d.ts +16 -0
  155. package/dist/shadow/index.js +20 -0
  156. package/dist/shadow/index.js.map +1 -0
  157. package/dist/store/index.cjs +1069 -0
  158. package/dist/store/index.cjs.map +1 -0
  159. package/dist/store/index.d.cts +491 -0
  160. package/dist/store/index.d.ts +491 -0
  161. package/dist/store/index.js +34 -0
  162. package/dist/store/index.js.map +1 -0
  163. package/dist/strategy-BSxFXGzb.d.cts +110 -0
  164. package/dist/strategy-BSxFXGzb.d.ts +110 -0
  165. package/dist/strategy-D-SrOLCl.d.cts +548 -0
  166. package/dist/strategy-D-SrOLCl.d.ts +548 -0
  167. package/dist/sync/index.cjs +1062 -0
  168. package/dist/sync/index.cjs.map +1 -0
  169. package/dist/sync/index.d.cts +42 -0
  170. package/dist/sync/index.d.ts +42 -0
  171. package/dist/sync/index.js +28 -0
  172. package/dist/sync/index.js.map +1 -0
  173. package/dist/team/index.cjs +1233 -0
  174. package/dist/team/index.cjs.map +1 -0
  175. package/dist/team/index.d.cts +117 -0
  176. package/dist/team/index.d.ts +117 -0
  177. package/dist/team/index.js +39 -0
  178. package/dist/team/index.js.map +1 -0
  179. package/dist/tx/index.cjs +212 -0
  180. package/dist/tx/index.cjs.map +1 -0
  181. package/dist/tx/index.d.cts +20 -0
  182. package/dist/tx/index.d.ts +20 -0
  183. package/dist/tx/index.js +20 -0
  184. package/dist/tx/index.js.map +1 -0
  185. package/dist/types-BZpCZB8N.d.ts +7526 -0
  186. package/dist/types-Bfs0qr5F.d.cts +7526 -0
  187. package/dist/ulid-COREQ2RQ.js +9 -0
  188. package/dist/ulid-COREQ2RQ.js.map +1 -0
  189. package/dist/util/index.cjs +230 -0
  190. package/dist/util/index.cjs.map +1 -0
  191. package/dist/util/index.d.cts +77 -0
  192. package/dist/util/index.d.ts +77 -0
  193. package/dist/util/index.js +190 -0
  194. package/dist/util/index.js.map +1 -0
  195. package/package.json +244 -0
@@ -0,0 +1,1062 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/sync/index.ts
21
+ var sync_exports = {};
22
+ __export(sync_exports, {
23
+ withSync: () => withSync
24
+ });
25
+ module.exports = __toCommonJS(sync_exports);
26
+
27
+ // src/types.ts
28
+ var NOYDB_SYNC_VERSION = 1;
29
+
30
+ // src/errors.ts
31
+ var NoydbError = class extends Error {
32
+ /** Machine-readable error code. Stable across library versions. */
33
+ code;
34
+ constructor(code, message) {
35
+ super(message);
36
+ this.name = "NoydbError";
37
+ this.code = code;
38
+ }
39
+ };
40
+ var DecryptionError = class extends NoydbError {
41
+ constructor(message = "Decryption failed") {
42
+ super("DECRYPTION_FAILED", message);
43
+ this.name = "DecryptionError";
44
+ }
45
+ };
46
+ var TamperedError = class extends NoydbError {
47
+ constructor(message = "Data integrity check failed \u2014 record may have been tampered with") {
48
+ super("TAMPERED", message);
49
+ this.name = "TamperedError";
50
+ }
51
+ };
52
+ var ConflictError = class extends NoydbError {
53
+ /** The actual stored version at the time of conflict. */
54
+ version;
55
+ constructor(version, message = "Version conflict") {
56
+ super("CONFLICT", message);
57
+ this.name = "ConflictError";
58
+ this.version = version;
59
+ }
60
+ };
61
+
62
+ // src/store/sync-policy.ts
63
+ var SyncScheduler = class {
64
+ policy;
65
+ callbacks;
66
+ _state = "idle";
67
+ _lastPushAt = null;
68
+ _lastPullAt = null;
69
+ _lastError = null;
70
+ _lastPushTime = 0;
71
+ // monotonic ms for minIntervalMs enforcement
72
+ // Timers
73
+ debounceTimer = null;
74
+ pushIntervalTimer = null;
75
+ pullIntervalTimer = null;
76
+ // Bound handlers for cleanup
77
+ boundOnVisibilityChange = null;
78
+ boundOnBeforeExit = null;
79
+ boundOnPageHide = null;
80
+ started = false;
81
+ constructor(policy, callbacks) {
82
+ this.policy = policy;
83
+ this.callbacks = callbacks;
84
+ if (this.shouldRegisterUnload()) {
85
+ this.boundOnVisibilityChange = this.handleVisibilityChange.bind(this);
86
+ this.boundOnPageHide = this.handlePageHide.bind(this);
87
+ this.boundOnBeforeExit = this.handleBeforeExit.bind(this);
88
+ }
89
+ }
90
+ /** Current scheduler status snapshot. */
91
+ get status() {
92
+ return {
93
+ state: this._state,
94
+ lastPushAt: this._lastPushAt,
95
+ lastPullAt: this._lastPullAt,
96
+ lastError: this._lastError,
97
+ pendingWrites: this.callbacks.getDirtyCount()
98
+ };
99
+ }
100
+ /** Start the scheduler — registers timers, event listeners. */
101
+ start() {
102
+ if (this.started) return;
103
+ this.started = true;
104
+ if (this.policy.push.mode === "interval" && this.policy.push.intervalMs) {
105
+ this.pushIntervalTimer = setInterval(() => {
106
+ void this.executePush();
107
+ }, this.policy.push.intervalMs);
108
+ }
109
+ if (this.policy.pull.mode === "interval" && this.policy.pull.intervalMs) {
110
+ this.pullIntervalTimer = setInterval(() => {
111
+ void this.executePull();
112
+ }, this.policy.pull.intervalMs);
113
+ }
114
+ if (this.policy.pull.mode === "on-focus" && typeof document !== "undefined") {
115
+ document.addEventListener("visibilitychange", this.handleFocusPull);
116
+ }
117
+ if (this.shouldRegisterUnload()) {
118
+ if (typeof document !== "undefined" && this.boundOnVisibilityChange) {
119
+ document.addEventListener("visibilitychange", this.boundOnVisibilityChange);
120
+ }
121
+ if (typeof globalThis.addEventListener === "function" && this.boundOnPageHide) {
122
+ globalThis.addEventListener("pagehide", this.boundOnPageHide);
123
+ }
124
+ if (typeof process !== "undefined" && this.boundOnBeforeExit) {
125
+ process.on("beforeExit", this.boundOnBeforeExit);
126
+ }
127
+ }
128
+ }
129
+ /** Stop the scheduler — clears timers, removes event listeners. */
130
+ stop() {
131
+ if (!this.started) return;
132
+ this.started = false;
133
+ if (this.debounceTimer) {
134
+ clearTimeout(this.debounceTimer);
135
+ this.debounceTimer = null;
136
+ }
137
+ if (this.pushIntervalTimer) {
138
+ clearInterval(this.pushIntervalTimer);
139
+ this.pushIntervalTimer = null;
140
+ }
141
+ if (this.pullIntervalTimer) {
142
+ clearInterval(this.pullIntervalTimer);
143
+ this.pullIntervalTimer = null;
144
+ }
145
+ if (this.policy.pull.mode === "on-focus" && typeof document !== "undefined") {
146
+ document.removeEventListener("visibilitychange", this.handleFocusPull);
147
+ }
148
+ if (typeof document !== "undefined" && this.boundOnVisibilityChange) {
149
+ document.removeEventListener("visibilitychange", this.boundOnVisibilityChange);
150
+ }
151
+ if (typeof globalThis.removeEventListener === "function" && this.boundOnPageHide) {
152
+ globalThis.removeEventListener("pagehide", this.boundOnPageHide);
153
+ }
154
+ if (typeof process !== "undefined" && this.boundOnBeforeExit) {
155
+ process.removeListener("beforeExit", this.boundOnBeforeExit);
156
+ }
157
+ }
158
+ /**
159
+ * Notify the scheduler that a local write occurred.
160
+ * For `on-change` mode: triggers immediate push (respecting minIntervalMs).
161
+ * For `debounce` mode: resets the debounce timer.
162
+ * For `manual` / `interval`: no-op.
163
+ */
164
+ notifyChange() {
165
+ if (!this.started) return;
166
+ if (this.policy.push.mode === "on-change") {
167
+ void this.executePush();
168
+ } else if (this.policy.push.mode === "debounce") {
169
+ this.resetDebounce();
170
+ }
171
+ }
172
+ /** Force an immediate push, bypassing the scheduler. */
173
+ async forcePush() {
174
+ await this.executePush();
175
+ }
176
+ /** Force an immediate pull, bypassing the scheduler. */
177
+ async forcePull() {
178
+ await this.executePull();
179
+ }
180
+ // ─── Internal ─────────────────────────────────────────────────────
181
+ async executePush() {
182
+ if (this._state === "pushing") return;
183
+ const minInterval = this.policy.push.minIntervalMs ?? 0;
184
+ if (minInterval > 0) {
185
+ const elapsed = Date.now() - this._lastPushTime;
186
+ if (elapsed < minInterval) {
187
+ if (this.policy.push.mode === "debounce") {
188
+ this.scheduleDebounce(minInterval - elapsed);
189
+ }
190
+ return;
191
+ }
192
+ }
193
+ if (this.callbacks.getDirtyCount() === 0) {
194
+ this._state = "idle";
195
+ return;
196
+ }
197
+ this._state = "pushing";
198
+ try {
199
+ await this.callbacks.push();
200
+ this._lastPushAt = (/* @__PURE__ */ new Date()).toISOString();
201
+ this._lastPushTime = Date.now();
202
+ this._lastError = null;
203
+ this._state = this.callbacks.getDirtyCount() > 0 ? "pending" : "idle";
204
+ } catch (err) {
205
+ this._lastError = err instanceof Error ? err : new Error(String(err));
206
+ this._state = "error";
207
+ }
208
+ }
209
+ async executePull() {
210
+ if (this._state === "pulling") return;
211
+ const previousState = this._state;
212
+ this._state = "pulling";
213
+ try {
214
+ await this.callbacks.pull();
215
+ this._lastPullAt = (/* @__PURE__ */ new Date()).toISOString();
216
+ this._lastError = null;
217
+ this._state = previousState === "pending" ? "pending" : "idle";
218
+ } catch (err) {
219
+ this._lastError = err instanceof Error ? err : new Error(String(err));
220
+ this._state = "error";
221
+ }
222
+ }
223
+ resetDebounce() {
224
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
225
+ const ms = this.policy.push.debounceMs ?? 3e4;
226
+ this._state = "pending";
227
+ this.scheduleDebounce(ms);
228
+ }
229
+ scheduleDebounce(ms) {
230
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
231
+ this.debounceTimer = setTimeout(() => {
232
+ this.debounceTimer = null;
233
+ void this.executePush();
234
+ }, ms);
235
+ }
236
+ shouldRegisterUnload() {
237
+ const onUnload = this.policy.push.onUnload;
238
+ if (onUnload !== void 0) return onUnload;
239
+ return this.policy.push.mode !== "manual";
240
+ }
241
+ // ─── Event handlers ───────────────────────────────────────────────
242
+ handleVisibilityChange() {
243
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
244
+ this.fireUnloadPush();
245
+ }
246
+ }
247
+ handlePageHide() {
248
+ this.fireUnloadPush();
249
+ }
250
+ handleBeforeExit() {
251
+ this.fireUnloadPush();
252
+ }
253
+ handleFocusPull = () => {
254
+ if (typeof document !== "undefined" && document.visibilityState === "visible") {
255
+ void this.executePull();
256
+ }
257
+ };
258
+ fireUnloadPush() {
259
+ if (this.callbacks.getDirtyCount() === 0) return;
260
+ void this.callbacks.push().catch(() => {
261
+ });
262
+ }
263
+ };
264
+
265
+ // src/team/sync.ts
266
+ var SyncEngine = class {
267
+ local;
268
+ remote;
269
+ strategy;
270
+ emitter;
271
+ vault;
272
+ role;
273
+ label;
274
+ dirty = [];
275
+ lastPush = null;
276
+ lastPull = null;
277
+ loaded = false;
278
+ autoSyncInterval = null;
279
+ isOnline = true;
280
+ /** Sync scheduler. Manages push/pull timing. */
281
+ scheduler;
282
+ /** Per-collection conflict resolvers registered by Collection instances. */
283
+ conflictResolvers = /* @__PURE__ */ new Map();
284
+ constructor(opts) {
285
+ this.local = opts.local;
286
+ this.remote = opts.remote;
287
+ this.vault = opts.vault;
288
+ this.strategy = opts.strategy;
289
+ this.emitter = opts.emitter;
290
+ this.role = opts.role ?? "sync-peer";
291
+ this.label = opts.label;
292
+ const policy = opts.syncPolicy;
293
+ if (policy && policy.push.mode !== "manual") {
294
+ this.scheduler = new SyncScheduler(policy, {
295
+ push: () => this.push().then(() => {
296
+ }),
297
+ pull: () => this.pull().then(() => {
298
+ }),
299
+ getDirtyCount: () => this.dirty.length
300
+ });
301
+ } else {
302
+ this.scheduler = null;
303
+ }
304
+ }
305
+ /** Start the sync scheduler. Called after vault is fully opened. */
306
+ startScheduler() {
307
+ this.scheduler?.start();
308
+ }
309
+ /** Stop the sync scheduler. Called on close. */
310
+ stopScheduler() {
311
+ this.scheduler?.stop();
312
+ }
313
+ /**
314
+ * Register a per-collection conflict resolver.
315
+ * Called by Collection when `conflictPolicy` is set.
316
+ */
317
+ registerConflictResolver(collection, resolver) {
318
+ this.conflictResolvers.set(collection, resolver);
319
+ }
320
+ /** Record a local change for later push. */
321
+ async trackChange(collection, id, action, version) {
322
+ await this.ensureLoaded();
323
+ const idx = this.dirty.findIndex((d) => d.collection === collection && d.id === id);
324
+ const entry = {
325
+ vault: this.vault,
326
+ collection,
327
+ id,
328
+ action,
329
+ version,
330
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
331
+ };
332
+ if (idx >= 0) {
333
+ this.dirty[idx] = entry;
334
+ } else {
335
+ this.dirty.push(entry);
336
+ }
337
+ await this.persistMeta();
338
+ this.scheduler?.notifyChange();
339
+ }
340
+ /** Push dirty records to remote adapter. Accepts optional `PushOptions` for partial sync. */
341
+ async push(options) {
342
+ await this.ensureLoaded();
343
+ let pushed = 0;
344
+ const conflicts = [];
345
+ const errors = [];
346
+ const completed = [];
347
+ for (let i = 0; i < this.dirty.length; i++) {
348
+ const entry = this.dirty[i];
349
+ if (options?.collections && !options.collections.includes(entry.collection)) {
350
+ continue;
351
+ }
352
+ try {
353
+ if (entry.action === "delete") {
354
+ await this.remote.delete(this.vault, entry.collection, entry.id);
355
+ completed.push(i);
356
+ pushed++;
357
+ } else {
358
+ const envelope = await this.local.get(this.vault, entry.collection, entry.id);
359
+ if (!envelope) {
360
+ completed.push(i);
361
+ continue;
362
+ }
363
+ try {
364
+ await this.remote.put(
365
+ this.vault,
366
+ entry.collection,
367
+ entry.id,
368
+ envelope,
369
+ entry.version - 1
370
+ );
371
+ completed.push(i);
372
+ pushed++;
373
+ } catch (err) {
374
+ if (err instanceof ConflictError) {
375
+ const remoteEnvelope = await this.remote.get(this.vault, entry.collection, entry.id);
376
+ if (remoteEnvelope) {
377
+ const { handled, conflict } = await this.handleConflict(
378
+ entry.collection,
379
+ entry.id,
380
+ envelope,
381
+ remoteEnvelope,
382
+ "push"
383
+ );
384
+ conflicts.push(conflict);
385
+ if (handled === "local") {
386
+ await this.remote.put(this.vault, entry.collection, entry.id, conflict.local);
387
+ completed.push(i);
388
+ pushed++;
389
+ } else if (handled === "remote") {
390
+ await this.local.put(this.vault, entry.collection, entry.id, conflict.remote);
391
+ completed.push(i);
392
+ } else if (handled === "merged" && conflict.local !== envelope) {
393
+ const merged = conflict.local;
394
+ await this.remote.put(this.vault, entry.collection, entry.id, merged);
395
+ await this.local.put(this.vault, entry.collection, entry.id, merged);
396
+ completed.push(i);
397
+ pushed++;
398
+ }
399
+ }
400
+ } else {
401
+ throw err;
402
+ }
403
+ }
404
+ }
405
+ } catch (err) {
406
+ errors.push(err instanceof Error ? err : new Error(String(err)));
407
+ }
408
+ }
409
+ for (const i of completed.sort((a, b) => b - a)) {
410
+ this.dirty.splice(i, 1);
411
+ }
412
+ this.lastPush = (/* @__PURE__ */ new Date()).toISOString();
413
+ await this.persistMeta();
414
+ const result = { pushed, conflicts, errors };
415
+ this.emitter.emit("sync:push", result);
416
+ return result;
417
+ }
418
+ /** Pull remote records to local adapter. Accepts optional `PullOptions` for partial sync. */
419
+ async pull(options) {
420
+ await this.ensureLoaded();
421
+ let pulled = 0;
422
+ const conflicts = [];
423
+ const errors = [];
424
+ try {
425
+ const remoteSnapshot = await this.remote.loadAll(this.vault);
426
+ for (const [collName, records] of Object.entries(remoteSnapshot)) {
427
+ if (options?.collections && !options.collections.includes(collName)) {
428
+ continue;
429
+ }
430
+ for (const [id, remoteEnvelope] of Object.entries(records)) {
431
+ if (options?.modifiedSince && remoteEnvelope._ts <= options.modifiedSince) {
432
+ continue;
433
+ }
434
+ try {
435
+ const localEnvelope = await this.local.get(this.vault, collName, id);
436
+ if (!localEnvelope) {
437
+ await this.local.put(this.vault, collName, id, remoteEnvelope);
438
+ pulled++;
439
+ } else if (remoteEnvelope._v > localEnvelope._v) {
440
+ const isDirty = this.dirty.some((d) => d.collection === collName && d.id === id);
441
+ if (isDirty) {
442
+ const { handled, conflict } = await this.handleConflict(
443
+ collName,
444
+ id,
445
+ localEnvelope,
446
+ remoteEnvelope,
447
+ "pull"
448
+ );
449
+ conflicts.push(conflict);
450
+ if (handled === "remote") {
451
+ await this.local.put(this.vault, collName, id, conflict.remote);
452
+ this.dirty = this.dirty.filter((d) => !(d.collection === collName && d.id === id));
453
+ pulled++;
454
+ } else if (handled === "merged" && conflict.local !== localEnvelope) {
455
+ const merged = conflict.local;
456
+ await this.local.put(this.vault, collName, id, merged);
457
+ this.dirty = this.dirty.filter((d) => !(d.collection === collName && d.id === id));
458
+ pulled++;
459
+ }
460
+ } else {
461
+ await this.local.put(this.vault, collName, id, remoteEnvelope);
462
+ pulled++;
463
+ }
464
+ }
465
+ } catch (err) {
466
+ errors.push(err instanceof Error ? err : new Error(String(err)));
467
+ }
468
+ }
469
+ }
470
+ } catch (err) {
471
+ errors.push(err instanceof Error ? err : new Error(String(err)));
472
+ }
473
+ this.lastPull = (/* @__PURE__ */ new Date()).toISOString();
474
+ await this.persistMeta();
475
+ const result = { pulled, conflicts, errors };
476
+ this.emitter.emit("sync:pull", result);
477
+ return result;
478
+ }
479
+ /** Bidirectional sync: pull then push. */
480
+ async sync(options) {
481
+ const pullResult = await this.pull(options?.pull);
482
+ const pushResult = await this.push(options?.push);
483
+ return { pull: pullResult, push: pushResult };
484
+ }
485
+ /**
486
+ * Push a specific subset of dirty entries (for sync transactions, ).
487
+ * Entries are matched by collection+id from the dirty log; matched entries
488
+ * are removed from the dirty log on success.
489
+ */
490
+ async pushFiltered(predicate) {
491
+ await this.ensureLoaded();
492
+ let pushed = 0;
493
+ const conflicts = [];
494
+ const errors = [];
495
+ const completed = [];
496
+ for (let i = 0; i < this.dirty.length; i++) {
497
+ const entry = this.dirty[i];
498
+ if (!predicate(entry)) continue;
499
+ try {
500
+ if (entry.action === "delete") {
501
+ await this.remote.delete(this.vault, entry.collection, entry.id);
502
+ completed.push(i);
503
+ pushed++;
504
+ } else {
505
+ const envelope = await this.local.get(this.vault, entry.collection, entry.id);
506
+ if (!envelope) {
507
+ completed.push(i);
508
+ continue;
509
+ }
510
+ try {
511
+ await this.remote.put(
512
+ this.vault,
513
+ entry.collection,
514
+ entry.id,
515
+ envelope,
516
+ entry.version - 1
517
+ );
518
+ completed.push(i);
519
+ pushed++;
520
+ } catch (err) {
521
+ if (err instanceof ConflictError) {
522
+ const remoteEnvelope = await this.remote.get(this.vault, entry.collection, entry.id);
523
+ if (remoteEnvelope) {
524
+ const { handled, conflict } = await this.handleConflict(
525
+ entry.collection,
526
+ entry.id,
527
+ envelope,
528
+ remoteEnvelope,
529
+ "push"
530
+ );
531
+ conflicts.push(conflict);
532
+ if (handled === "local") {
533
+ await this.remote.put(this.vault, entry.collection, entry.id, conflict.local);
534
+ completed.push(i);
535
+ pushed++;
536
+ } else if (handled === "remote") {
537
+ await this.local.put(this.vault, entry.collection, entry.id, conflict.remote);
538
+ completed.push(i);
539
+ } else if (handled === "merged" && conflict.local !== envelope) {
540
+ const merged = conflict.local;
541
+ await this.remote.put(this.vault, entry.collection, entry.id, merged);
542
+ await this.local.put(this.vault, entry.collection, entry.id, merged);
543
+ completed.push(i);
544
+ pushed++;
545
+ }
546
+ }
547
+ } else {
548
+ throw err;
549
+ }
550
+ }
551
+ }
552
+ } catch (err) {
553
+ errors.push(err instanceof Error ? err : new Error(String(err)));
554
+ }
555
+ }
556
+ for (const i of completed.sort((a, b) => b - a)) {
557
+ this.dirty.splice(i, 1);
558
+ }
559
+ this.lastPush = (/* @__PURE__ */ new Date()).toISOString();
560
+ await this.persistMeta();
561
+ const result = { pushed, conflicts, errors };
562
+ this.emitter.emit("sync:push", result);
563
+ return result;
564
+ }
565
+ /** Get current sync status. */
566
+ status() {
567
+ return {
568
+ dirty: this.dirty.length,
569
+ lastPush: this.lastPush,
570
+ lastPull: this.lastPull,
571
+ online: this.isOnline
572
+ };
573
+ }
574
+ // ─── Auto-Sync ───────────────────────────────────────────────────
575
+ /** Start auto-sync: listen for online/offline events, optional periodic sync. */
576
+ startAutoSync(intervalMs) {
577
+ if (typeof globalThis.addEventListener === "function") {
578
+ globalThis.addEventListener("online", this.handleOnline);
579
+ globalThis.addEventListener("offline", this.handleOffline);
580
+ }
581
+ if (intervalMs && intervalMs > 0) {
582
+ this.autoSyncInterval = setInterval(() => {
583
+ if (this.isOnline) {
584
+ void this.sync();
585
+ }
586
+ }, intervalMs);
587
+ }
588
+ }
589
+ /** Stop auto-sync and scheduler. */
590
+ stopAutoSync() {
591
+ this.stopScheduler();
592
+ if (typeof globalThis.removeEventListener === "function") {
593
+ globalThis.removeEventListener("online", this.handleOnline);
594
+ globalThis.removeEventListener("offline", this.handleOffline);
595
+ }
596
+ if (this.autoSyncInterval) {
597
+ clearInterval(this.autoSyncInterval);
598
+ this.autoSyncInterval = null;
599
+ }
600
+ }
601
+ handleOnline = () => {
602
+ this.isOnline = true;
603
+ this.emitter.emit("sync:online", void 0);
604
+ void this.sync();
605
+ };
606
+ handleOffline = () => {
607
+ this.isOnline = false;
608
+ this.emitter.emit("sync:offline", void 0);
609
+ };
610
+ /**
611
+ * Resolve a conflict, checking per-collection resolvers first,
612
+ * then falling back to the db-level `ConflictStrategy`.
613
+ *
614
+ * Returns the resolved `Conflict` object (possibly with `resolve` set for
615
+ * manual mode) and a `handled` discriminant:
616
+ * - `'local'` — keep the local envelope; push it to remote.
617
+ * - `'remote'` — keep the remote envelope; update local.
618
+ * - `'merged'` — a custom merge fn produced a new envelope stored as `conflict.local`.
619
+ * - `'deferred'` — manual mode, resolve was not called synchronously.
620
+ */
621
+ async handleConflict(collection, id, local, remote, _phase) {
622
+ const resolver = this.conflictResolvers.get(collection);
623
+ if (resolver) {
624
+ const winner = await resolver(id, local, remote);
625
+ const base = {
626
+ vault: this.vault,
627
+ collection,
628
+ id,
629
+ local,
630
+ remote,
631
+ localVersion: local._v,
632
+ remoteVersion: remote._v
633
+ };
634
+ if (winner === null) return { handled: "deferred", conflict: base };
635
+ if (winner === local) return { handled: "local", conflict: base };
636
+ if (winner === remote) return { handled: "remote", conflict: base };
637
+ return {
638
+ handled: "merged",
639
+ conflict: { ...base, local: winner, localVersion: winner._v }
640
+ };
641
+ }
642
+ const baseConflict = {
643
+ vault: this.vault,
644
+ collection,
645
+ id,
646
+ local,
647
+ remote,
648
+ localVersion: local._v,
649
+ remoteVersion: remote._v
650
+ };
651
+ this.emitter.emit("sync:conflict", baseConflict);
652
+ const side = this.legacyResolve(baseConflict);
653
+ return { handled: side, conflict: baseConflict };
654
+ }
655
+ /** DB-level ConflictStrategy resolution (legacy, kept for backward compat). */
656
+ legacyResolve(conflict) {
657
+ if (typeof this.strategy === "function") {
658
+ return this.strategy(conflict);
659
+ }
660
+ switch (this.strategy) {
661
+ case "local-wins":
662
+ return "local";
663
+ case "remote-wins":
664
+ return "remote";
665
+ case "version":
666
+ default:
667
+ return conflict.localVersion >= conflict.remoteVersion ? "local" : "remote";
668
+ }
669
+ }
670
+ // ─── Persistence ─────────────────────────────────────────────────
671
+ async ensureLoaded() {
672
+ if (this.loaded) return;
673
+ const envelope = await this.local.get(this.vault, "_sync", "meta");
674
+ if (envelope) {
675
+ const meta = JSON.parse(envelope._data);
676
+ this.dirty = [...meta.dirty];
677
+ this.lastPush = meta.last_push;
678
+ this.lastPull = meta.last_pull;
679
+ }
680
+ this.loaded = true;
681
+ }
682
+ async persistMeta() {
683
+ const meta = {
684
+ _noydb_sync: NOYDB_SYNC_VERSION,
685
+ last_push: this.lastPush,
686
+ last_pull: this.lastPull,
687
+ dirty: this.dirty
688
+ };
689
+ const envelope = {
690
+ _noydb: 1,
691
+ _v: 1,
692
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
693
+ _iv: "",
694
+ _data: JSON.stringify(meta)
695
+ };
696
+ await this.local.put(this.vault, "_sync", "meta", envelope);
697
+ }
698
+ };
699
+
700
+ // src/team/sync-transaction.ts
701
+ var SyncTransaction = class {
702
+ comp;
703
+ engine;
704
+ ops = [];
705
+ /** @internal — constructed by `Noydb.transaction()` */
706
+ constructor(comp, engine) {
707
+ this.comp = comp;
708
+ this.engine = engine;
709
+ }
710
+ /** Stage a record write. Does not write to any adapter until `commit()`. */
711
+ put(collection, id, record) {
712
+ this.ops.push({ type: "put", collection, id, record });
713
+ return this;
714
+ }
715
+ /** Stage a record delete. Does not write to any adapter until `commit()`. */
716
+ delete(collection, id) {
717
+ this.ops.push({ type: "delete", collection, id });
718
+ return this;
719
+ }
720
+ /**
721
+ * Commit the transaction.
722
+ *
723
+ * Phase 1 — writes all staged operations to the local adapter via the
724
+ * collection layer (encryption + dirty-log tracking).
725
+ *
726
+ * Phase 2 — pushes only the records that were written in this
727
+ * transaction to the remote adapter. Existing dirty entries from
728
+ * outside this transaction are not affected.
729
+ *
730
+ * If any record conflicts during the push, `status` is `'conflict'`
731
+ * and `conflicts` lists the affected records. No automatic rollback is
732
+ * performed.
733
+ */
734
+ async commit() {
735
+ for (const op of this.ops) {
736
+ if (op.type === "put") {
737
+ await this.comp.collection(op.collection).put(op.id, op.record);
738
+ } else {
739
+ await this.comp.collection(op.collection).delete(op.id);
740
+ }
741
+ }
742
+ const opSet = /* @__PURE__ */ new Set();
743
+ for (const op of this.ops) {
744
+ opSet.add(`${op.collection}::${op.id}`);
745
+ }
746
+ const pushResult = await this.engine.pushFiltered(
747
+ (entry) => opSet.has(`${entry.collection}::${entry.id}`)
748
+ );
749
+ return {
750
+ status: pushResult.conflicts.length > 0 ? "conflict" : "committed",
751
+ pushed: pushResult.pushed,
752
+ conflicts: pushResult.conflicts
753
+ };
754
+ }
755
+ };
756
+
757
+ // src/crypto.ts
758
+ var IV_BYTES = 12;
759
+ var KEY_BITS = 256;
760
+ var subtle = globalThis.crypto.subtle;
761
+ async function encrypt(plaintext, dek) {
762
+ const iv = generateIV();
763
+ const encoded = new TextEncoder().encode(plaintext);
764
+ const ciphertext = await subtle.encrypt(
765
+ { name: "AES-GCM", iv },
766
+ dek,
767
+ encoded
768
+ );
769
+ return {
770
+ iv: bufferToBase64(iv),
771
+ data: bufferToBase64(ciphertext)
772
+ };
773
+ }
774
+ async function decrypt(ivBase64, dataBase64, dek) {
775
+ const iv = base64ToBuffer(ivBase64);
776
+ const ciphertext = base64ToBuffer(dataBase64);
777
+ try {
778
+ const plaintext = await subtle.decrypt(
779
+ { name: "AES-GCM", iv },
780
+ dek,
781
+ ciphertext
782
+ );
783
+ return new TextDecoder().decode(plaintext);
784
+ } catch (err) {
785
+ if (err instanceof Error && err.name === "OperationError") {
786
+ throw new TamperedError();
787
+ }
788
+ throw new DecryptionError(
789
+ err instanceof Error ? err.message : "Decryption failed"
790
+ );
791
+ }
792
+ }
793
+ async function derivePresenceKey(dek, collectionName) {
794
+ const rawDek = await subtle.exportKey("raw", dek);
795
+ const hkdfKey = await subtle.importKey(
796
+ "raw",
797
+ rawDek,
798
+ "HKDF",
799
+ false,
800
+ ["deriveBits"]
801
+ );
802
+ const salt = new TextEncoder().encode("noydb-presence");
803
+ const info = new TextEncoder().encode(collectionName);
804
+ const bits = await subtle.deriveBits(
805
+ { name: "HKDF", hash: "SHA-256", salt, info },
806
+ hkdfKey,
807
+ KEY_BITS
808
+ );
809
+ return subtle.importKey(
810
+ "raw",
811
+ bits,
812
+ { name: "AES-GCM", length: KEY_BITS },
813
+ false,
814
+ ["encrypt", "decrypt"]
815
+ );
816
+ }
817
+ function generateIV() {
818
+ return globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
819
+ }
820
+ function bufferToBase64(buffer) {
821
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
822
+ let binary = "";
823
+ for (let i = 0; i < bytes.length; i++) {
824
+ binary += String.fromCharCode(bytes[i]);
825
+ }
826
+ return btoa(binary);
827
+ }
828
+ function base64ToBuffer(base64) {
829
+ const binary = atob(base64);
830
+ const bytes = new Uint8Array(binary.length);
831
+ for (let i = 0; i < binary.length; i++) {
832
+ bytes[i] = binary.charCodeAt(i);
833
+ }
834
+ return bytes;
835
+ }
836
+
837
+ // src/team/presence.ts
838
+ var PresenceHandle = class {
839
+ adapter;
840
+ syncAdapter;
841
+ vault;
842
+ collectionName;
843
+ userId;
844
+ encrypted;
845
+ getDEK;
846
+ staleMs;
847
+ pollIntervalMs;
848
+ channel;
849
+ storageCollection;
850
+ presenceKey = null;
851
+ subscribers = [];
852
+ unsubscribePubSub = null;
853
+ pollTimer = null;
854
+ stopped = false;
855
+ constructor(opts) {
856
+ this.adapter = opts.adapter;
857
+ this.syncAdapter = opts.syncAdapter;
858
+ this.vault = opts.vault;
859
+ this.collectionName = opts.collectionName;
860
+ this.userId = opts.userId;
861
+ this.encrypted = opts.encrypted;
862
+ this.getDEK = opts.getDEK;
863
+ this.staleMs = opts.staleMs ?? 3e4;
864
+ this.pollIntervalMs = opts.pollIntervalMs ?? 5e3;
865
+ this.channel = `${opts.vault}:${opts.collectionName}:presence`;
866
+ this.storageCollection = `_presence_${opts.collectionName}`;
867
+ }
868
+ /**
869
+ * Announce yourself (or update your cursor/status).
870
+ * Encrypts `payload` with the presence key and publishes it.
871
+ */
872
+ async update(payload) {
873
+ if (this.stopped) return;
874
+ const key = await this.getPresenceKey();
875
+ const now = (/* @__PURE__ */ new Date()).toISOString();
876
+ const plaintext = JSON.stringify({ userId: this.userId, lastSeen: now, payload });
877
+ let encryptedPayload;
878
+ if (this.encrypted && key) {
879
+ const iv = generateIV();
880
+ const ivB64 = bufferToBase64(iv);
881
+ const { data } = await encrypt(plaintext, key);
882
+ encryptedPayload = JSON.stringify({ iv: ivB64, data });
883
+ } else {
884
+ encryptedPayload = plaintext;
885
+ }
886
+ const pubAdapter = this.getPubSubAdapter();
887
+ if (pubAdapter?.presencePublish) {
888
+ await pubAdapter.presencePublish(this.channel, encryptedPayload);
889
+ }
890
+ await this.writeStorageRecord(payload, now);
891
+ }
892
+ /**
893
+ * Subscribe to presence updates. The callback receives a filtered, decrypted
894
+ * list of all currently-active peers (excluding yourself, excluding stale).
895
+ *
896
+ * Returns an unsubscribe function. Also call `stop()` to release all resources.
897
+ */
898
+ subscribe(cb) {
899
+ if (this.stopped) return () => {
900
+ };
901
+ this.subscribers.push(cb);
902
+ if (this.subscribers.length === 1) {
903
+ this.startListening();
904
+ }
905
+ return () => {
906
+ this.subscribers = this.subscribers.filter((s) => s !== cb);
907
+ if (this.subscribers.length === 0) this.stopListening();
908
+ };
909
+ }
910
+ /** Stop all listening and clear resources. */
911
+ stop() {
912
+ this.stopped = true;
913
+ this.stopListening();
914
+ this.subscribers = [];
915
+ }
916
+ // ─── Private ────────────────────────────────────────────────────────
917
+ async getPresenceKey() {
918
+ if (!this.encrypted) return null;
919
+ if (!this.presenceKey) {
920
+ try {
921
+ const dek = await this.getDEK(this.collectionName);
922
+ this.presenceKey = await derivePresenceKey(dek, this.collectionName);
923
+ } catch {
924
+ }
925
+ }
926
+ return this.presenceKey;
927
+ }
928
+ getPubSubAdapter() {
929
+ if (this.syncAdapter?.presencePublish) return this.syncAdapter;
930
+ if (this.adapter.presencePublish) return this.adapter;
931
+ return void 0;
932
+ }
933
+ startListening() {
934
+ const pubAdapter = this.getPubSubAdapter();
935
+ if (pubAdapter?.presenceSubscribe) {
936
+ this.unsubscribePubSub = pubAdapter.presenceSubscribe(
937
+ this.channel,
938
+ (encryptedPayload) => {
939
+ void this.handlePubSubMessage(encryptedPayload);
940
+ }
941
+ );
942
+ } else {
943
+ this.pollTimer = setInterval(
944
+ () => {
945
+ void this.pollStoragePresence();
946
+ },
947
+ this.pollIntervalMs
948
+ );
949
+ void this.pollStoragePresence();
950
+ }
951
+ }
952
+ stopListening() {
953
+ if (this.unsubscribePubSub) {
954
+ this.unsubscribePubSub();
955
+ this.unsubscribePubSub = null;
956
+ }
957
+ if (this.pollTimer) {
958
+ clearInterval(this.pollTimer);
959
+ this.pollTimer = null;
960
+ }
961
+ }
962
+ async handlePubSubMessage(encryptedPayload) {
963
+ try {
964
+ const peer = await this.decryptPresencePayload(encryptedPayload);
965
+ if (!peer || peer.userId === this.userId) return;
966
+ const cutoff = new Date(Date.now() - this.staleMs).toISOString();
967
+ if (peer.lastSeen < cutoff) return;
968
+ await this.pollStoragePresence();
969
+ } catch {
970
+ }
971
+ }
972
+ async decryptPresencePayload(encryptedPayload) {
973
+ const key = await this.getPresenceKey();
974
+ if (!this.encrypted || !key) {
975
+ return JSON.parse(encryptedPayload);
976
+ }
977
+ const { iv: ivB64, data } = JSON.parse(encryptedPayload);
978
+ const plaintext = await decrypt(ivB64, data, key);
979
+ return JSON.parse(plaintext);
980
+ }
981
+ async writeStorageRecord(payload, now) {
982
+ const key = await this.getPresenceKey();
983
+ const plaintext = JSON.stringify(payload);
984
+ let iv = "";
985
+ let data;
986
+ if (this.encrypted && key) {
987
+ const ivBytes = generateIV();
988
+ iv = bufferToBase64(ivBytes);
989
+ const result = await encrypt(plaintext, key);
990
+ data = result.data;
991
+ } else {
992
+ data = plaintext;
993
+ }
994
+ const record = { userId: this.userId, lastSeen: now, iv, data };
995
+ const json = JSON.stringify(record);
996
+ const storeAdapter = this.syncAdapter ?? this.adapter;
997
+ const envelope = {
998
+ _noydb: 1,
999
+ _v: 1,
1000
+ _ts: now,
1001
+ _iv: "",
1002
+ _data: json
1003
+ };
1004
+ try {
1005
+ await storeAdapter.put(
1006
+ this.vault,
1007
+ this.storageCollection,
1008
+ this.userId,
1009
+ envelope
1010
+ );
1011
+ } catch {
1012
+ }
1013
+ }
1014
+ async pollStoragePresence() {
1015
+ if (this.stopped || this.subscribers.length === 0) return;
1016
+ try {
1017
+ const storeAdapter = this.syncAdapter ?? this.adapter;
1018
+ const ids = await storeAdapter.list(this.vault, this.storageCollection);
1019
+ const cutoff = new Date(Date.now() - this.staleMs).toISOString();
1020
+ const peers = [];
1021
+ for (const id of ids) {
1022
+ if (id === this.userId) continue;
1023
+ const envelope = await storeAdapter.get(this.vault, this.storageCollection, id);
1024
+ if (!envelope) continue;
1025
+ const record = JSON.parse(envelope._data);
1026
+ if (record.lastSeen < cutoff) continue;
1027
+ let peerPayload;
1028
+ if (this.encrypted && this.presenceKey && record.iv) {
1029
+ const plaintext = await decrypt(record.iv, record.data, this.presenceKey);
1030
+ peerPayload = JSON.parse(plaintext);
1031
+ } else {
1032
+ peerPayload = JSON.parse(record.data);
1033
+ }
1034
+ peers.push({ userId: record.userId, payload: peerPayload, lastSeen: record.lastSeen });
1035
+ }
1036
+ for (const cb of this.subscribers) {
1037
+ cb(peers);
1038
+ }
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ };
1043
+
1044
+ // src/team/sync-active.ts
1045
+ function withSync() {
1046
+ return {
1047
+ buildSyncEngine(opts) {
1048
+ return new SyncEngine(opts);
1049
+ },
1050
+ buildSyncTransaction(vault, engine) {
1051
+ return new SyncTransaction(vault, engine);
1052
+ },
1053
+ buildPresence(opts) {
1054
+ return new PresenceHandle(opts);
1055
+ }
1056
+ };
1057
+ }
1058
+ // Annotate the CommonJS export names for ESM import in node:
1059
+ 0 && (module.exports = {
1060
+ withSync
1061
+ });
1062
+ //# sourceMappingURL=index.cjs.map