js-bao 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.cjs CHANGED
@@ -93,37 +93,388 @@ var init_yjsValues = __esm({
93
93
  }
94
94
  });
95
95
 
96
+ // src/models/writeLifecycle.ts
97
+ function registerFunctionDefaultGenerator(name, fn) {
98
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
99
+ }
100
+ var Y2, import_ulid, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS;
101
+ var init_writeLifecycle = __esm({
102
+ "src/models/writeLifecycle.ts"() {
103
+ "use strict";
104
+ Y2 = __toESM(require("yjs"), 1);
105
+ import_ulid = require("ulid");
106
+ KNOWN_FIELD_TYPE_MAP = {
107
+ string: true,
108
+ number: true,
109
+ boolean: true,
110
+ date: true,
111
+ id: true,
112
+ stringset: true
113
+ };
114
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
115
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
116
+ registerFunctionDefaultGenerator("generate_ulid", () => (0, import_ulid.ulid)());
117
+ }
118
+ });
119
+
96
120
  // src/models/metaSync.ts
97
121
  function registerFunctionDefault(fn, name) {
98
122
  KNOWN_FUNCTION_DEFAULTS.set(fn, name);
99
123
  }
100
- var Y2, KNOWN_FUNCTION_DEFAULTS;
124
+ var Y3, KNOWN_FUNCTION_DEFAULTS;
101
125
  var init_metaSync = __esm({
102
126
  "src/models/metaSync.ts"() {
103
127
  "use strict";
104
- Y2 = __toESM(require("yjs"), 1);
128
+ Y3 = __toESM(require("yjs"), 1);
105
129
  init_yjsValues();
130
+ init_writeLifecycle();
106
131
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
107
132
  }
108
133
  });
109
134
 
135
+ // src/utils/uniqueIndex.ts
136
+ var Y4;
137
+ var init_uniqueIndex = __esm({
138
+ "src/utils/uniqueIndex.ts"() {
139
+ "use strict";
140
+ Y4 = __toESM(require("yjs"), 1);
141
+ }
142
+ });
143
+
144
+ // src/utils/largeDocuments.ts
145
+ var Y5;
146
+ var init_largeDocuments = __esm({
147
+ "src/utils/largeDocuments.ts"() {
148
+ "use strict";
149
+ Y5 = __toESM(require("yjs"), 1);
150
+ init_yjsValues();
151
+ }
152
+ });
153
+
154
+ // src/models/OrmContext.ts
155
+ var OrmContext, defaultOrmContext, liveOrmContexts;
156
+ var init_OrmContext = __esm({
157
+ "src/models/OrmContext.ts"() {
158
+ "use strict";
159
+ OrmContext = class _OrmContext {
160
+ /** Declared before the field that reads it: static initialization is ordered. */
161
+ static instanceCounter = 0;
162
+ /**
163
+ * Which instance this is, for diagnostics. A process holding several clients
164
+ * needs a way to say WHICH one a warning is about, so every context takes the
165
+ * next number as it is created and keeps it for its lifetime.
166
+ */
167
+ instanceId = ++_OrmContext.instanceCounter;
168
+ /** The engine this instance's models read and write through. */
169
+ dbEngine = null;
170
+ connectedDocuments = /* @__PURE__ */ new Map();
171
+ /** `docId_modelName` → the Y.Map that document holds for that model. */
172
+ documentYMaps = /* @__PURE__ */ new Map();
173
+ /** `subscribe()` callbacks, by model name. */
174
+ listenersMap = /* @__PURE__ */ new Map();
175
+ // Default-document resolution
176
+ modelNameToDefaultDocId = /* @__PURE__ */ new Map();
177
+ globalDefaultDocId = void 0;
178
+ defaultDocChangedListeners = /* @__PURE__ */ new Set();
179
+ modelDocMappingChangedListeners = /* @__PURE__ */ new Set();
180
+ /** Format-2 ("large document", #2816) record stores by document id. */
181
+ format2Documents = /* @__PURE__ */ new Map();
182
+ /** Format-2 epoch moves in flight (#2816). */
183
+ format2Transitions = /* @__PURE__ */ new Map();
184
+ /**
185
+ * Nested-Y.Map stringset fields this context has already observed. Per
186
+ * context, not global: a Y.Doc reconnected to a fresh instance has to be
187
+ * observed again by that instance, and a global guard would silently
188
+ * suppress it.
189
+ */
190
+ observedStringSetMaps = /* @__PURE__ */ new WeakSet();
191
+ /**
192
+ * The registry this instance's schema lookups use. `null` on the standing
193
+ * default context, where callers fall back to the global singleton.
194
+ */
195
+ modelRegistry = null;
196
+ /** Model classes routed to this context. */
197
+ boundModels = /* @__PURE__ */ new Set();
198
+ /** True once {@link destroy} has run; bound classes keep pointing here. */
199
+ destroyed = false;
200
+ /** Yjs `unobserve` thunks by owning document id. */
201
+ observerDisposers = /* @__PURE__ */ new Map();
202
+ /** Teardown the owner (the document manager) registers for `destroy()`. */
203
+ teardownHooks = /* @__PURE__ */ new Set();
204
+ static NO_DOCUMENT_KEY = "__no_document__";
205
+ /**
206
+ * Record how to remove an observer this context installed. Every `observe()`
207
+ * in the ORM pairs with one of these: without it a torn-down instance's
208
+ * callbacks keep firing on a Y.Doc that outlives it.
209
+ */
210
+ registerObserverDisposer(docId, dispose) {
211
+ const key = docId ?? _OrmContext.NO_DOCUMENT_KEY;
212
+ const list = this.observerDisposers.get(key);
213
+ if (list) list.push(dispose);
214
+ else this.observerDisposers.set(key, [dispose]);
215
+ }
216
+ /** Remove every observer this context installed for `docId`. */
217
+ disposeObserversForDocument(docId) {
218
+ const list = this.observerDisposers.get(docId);
219
+ if (!list) return;
220
+ this.observerDisposers.delete(docId);
221
+ for (const dispose of list) {
222
+ try {
223
+ dispose();
224
+ } catch {
225
+ }
226
+ }
227
+ }
228
+ disposeAllObservers() {
229
+ const buckets = Array.from(this.observerDisposers.values());
230
+ this.observerDisposers.clear();
231
+ for (const list of buckets) {
232
+ for (const dispose of list) {
233
+ try {
234
+ dispose();
235
+ } catch {
236
+ }
237
+ }
238
+ }
239
+ }
240
+ /** Run `hook` as part of `destroy()` (the format-2 binding registers here). */
241
+ onDestroy(hook) {
242
+ this.teardownHooks.add(hook);
243
+ }
244
+ /** The record store for `docId`, or `null` when the document is format 1. */
245
+ getFormat2Store(docId) {
246
+ if (!docId) return null;
247
+ return this.format2Documents.get(docId) ?? null;
248
+ }
249
+ /**
250
+ * The epoch move a write to `docId` has to wait out (#2816, behavior 10), or
251
+ * `null` when none is in flight.
252
+ */
253
+ format2WritesHeld(docId) {
254
+ if (!docId) return null;
255
+ return this.format2Transitions.get(docId) ?? null;
256
+ }
257
+ /** Run an epoch move with `docId`'s local writes held back (#2816). */
258
+ async holdFormat2Writes(docId, run) {
259
+ const inFlight = this.format2Transitions.get(docId);
260
+ if (inFlight) await inFlight.catch(() => {
261
+ });
262
+ let release;
263
+ const held = new Promise((resolve) => {
264
+ release = resolve;
265
+ });
266
+ this.format2Transitions.set(docId, held);
267
+ try {
268
+ return await run();
269
+ } finally {
270
+ if (this.format2Transitions.get(docId) === held) {
271
+ this.format2Transitions.delete(docId);
272
+ }
273
+ release();
274
+ }
275
+ }
276
+ /** Notify `subscribe()` callbacks registered on this context for `modelName`. */
277
+ notifyListeners(modelName, log, onError) {
278
+ const listeners = this.listenersMap.get(modelName);
279
+ if (!listeners) return;
280
+ log?.(`Notifying ${listeners.size} listeners for ${modelName}`);
281
+ listeners.forEach((callback) => {
282
+ try {
283
+ callback();
284
+ } catch (e) {
285
+ onError?.(e);
286
+ }
287
+ });
288
+ }
289
+ // ---- Default-document resolution (the `BaseModel` statics delegate here) ----
290
+ setModelDefaultDocumentId(modelName, docId) {
291
+ const previous = this.modelNameToDefaultDocId.get(modelName);
292
+ this.modelNameToDefaultDocId.set(modelName, docId);
293
+ this.announceModelDocMapping(modelName, previous, docId);
294
+ }
295
+ removeModelDefaultDocumentId(modelName) {
296
+ const previous = this.modelNameToDefaultDocId.get(modelName);
297
+ this.modelNameToDefaultDocId.delete(modelName);
298
+ this.announceModelDocMapping(modelName, previous, void 0);
299
+ }
300
+ clearModelDefaultDocumentIds() {
301
+ const entries = Array.from(this.modelNameToDefaultDocId.entries());
302
+ this.modelNameToDefaultDocId.clear();
303
+ for (const [modelName, previous] of entries) {
304
+ this.announceModelDocMapping(modelName, previous, void 0);
305
+ }
306
+ }
307
+ setGlobalDefaultDocumentId(docId) {
308
+ const previous = this.globalDefaultDocId;
309
+ this.globalDefaultDocId = docId;
310
+ this.announceDefaultDoc(previous, docId);
311
+ }
312
+ clearGlobalDefaultDocumentId() {
313
+ const previous = this.globalDefaultDocId;
314
+ this.globalDefaultDocId = void 0;
315
+ this.announceDefaultDoc(previous, void 0);
316
+ }
317
+ getModelDefaultDocumentMapping() {
318
+ return Object.fromEntries(this.modelNameToDefaultDocId.entries());
319
+ }
320
+ getDocumentIdForModel(modelName) {
321
+ return this.modelNameToDefaultDocId.get(modelName);
322
+ }
323
+ getGlobalDefaultDocumentId() {
324
+ return this.globalDefaultDocId;
325
+ }
326
+ onDefaultDocChanged(listener) {
327
+ this.defaultDocChangedListeners.add(listener);
328
+ return () => this.defaultDocChangedListeners.delete(listener);
329
+ }
330
+ onModelDocMappingChanged(listener) {
331
+ this.modelDocMappingChangedListeners.add(listener);
332
+ return () => this.modelDocMappingChangedListeners.delete(listener);
333
+ }
334
+ /** Drop the model→document defaults that point at a disconnected document. */
335
+ clearMappingsForDocId(docId) {
336
+ const toRemove = [];
337
+ for (const [modelName, mappedDocId] of this.modelNameToDefaultDocId) {
338
+ if (mappedDocId === docId) toRemove.push(modelName);
339
+ }
340
+ for (const modelName of toRemove) {
341
+ this.removeModelDefaultDocumentId(modelName);
342
+ }
343
+ if (this.globalDefaultDocId === docId) {
344
+ this.clearGlobalDefaultDocumentId();
345
+ }
346
+ }
347
+ announceDefaultDoc(previous, current) {
348
+ for (const listener of this.defaultDocChangedListeners) {
349
+ try {
350
+ listener({ previous, current });
351
+ } catch {
352
+ }
353
+ }
354
+ }
355
+ announceModelDocMapping(modelName, previous, current) {
356
+ for (const listener of this.modelDocMappingChangedListeners) {
357
+ try {
358
+ listener({ modelName, previous, current });
359
+ } catch {
360
+ }
361
+ }
362
+ }
363
+ /**
364
+ * Tear this instance down: observers off, engine destroyed, state cleared.
365
+ *
366
+ * Deliberately does NOT unbind its model classes. They keep resolving this
367
+ * context, whose engine is now null, so an operation through them throws the
368
+ * ordinary not-initialized error instead of silently landing on whatever
369
+ * other instance happens to be live (#3039, behavior 7). Only a later
370
+ * `initJsBao` may rebind a class whose context is destroyed.
371
+ */
372
+ async destroy() {
373
+ if (this.destroyed) return;
374
+ this.destroyed = true;
375
+ this.disposeAllObservers();
376
+ const hooks = Array.from(this.teardownHooks);
377
+ this.teardownHooks.clear();
378
+ for (const hook of hooks) {
379
+ try {
380
+ await hook();
381
+ } catch {
382
+ }
383
+ }
384
+ const engine = this.dbEngine;
385
+ this.dbEngine = null;
386
+ if (engine) {
387
+ try {
388
+ await engine.destroy();
389
+ } catch {
390
+ }
391
+ }
392
+ this.connectedDocuments.clear();
393
+ this.documentYMaps.clear();
394
+ this.listenersMap.clear();
395
+ this.modelNameToDefaultDocId.clear();
396
+ this.globalDefaultDocId = void 0;
397
+ this.defaultDocChangedListeners.clear();
398
+ this.modelDocMappingChangedListeners.clear();
399
+ this.format2Documents.clear();
400
+ this.format2Transitions.clear();
401
+ this.observedStringSetMaps = /* @__PURE__ */ new WeakSet();
402
+ liveOrmContexts.delete(this);
403
+ if (defaultOrmContext === this) {
404
+ defaultOrmContext = new _OrmContext();
405
+ }
406
+ }
407
+ };
408
+ defaultOrmContext = new OrmContext();
409
+ liveOrmContexts = /* @__PURE__ */ new Set();
410
+ }
411
+ });
412
+
413
+ // src/models/format2QueryProjection.ts
414
+ var FORMAT2_QUERY_PROJECTION_DDL;
415
+ var init_format2QueryProjection = __esm({
416
+ "src/models/format2QueryProjection.ts"() {
417
+ "use strict";
418
+ FORMAT2_QUERY_PROJECTION_DDL = `
419
+ CREATE TABLE IF NOT EXISTS _query_projection (
420
+ doc_id TEXT NOT NULL,
421
+ model TEXT NOT NULL,
422
+ PRIMARY KEY (doc_id, model)
423
+ )`.trim();
424
+ }
425
+ });
426
+
427
+ // src/models/format2Observer.ts
428
+ var Y6;
429
+ var init_format2Observer = __esm({
430
+ "src/models/format2Observer.ts"() {
431
+ "use strict";
432
+ Y6 = __toESM(require("yjs"), 1);
433
+ init_largeDocuments();
434
+ }
435
+ });
436
+
437
+ // src/models/format2ClientWrite.ts
438
+ var init_format2ClientWrite = __esm({
439
+ "src/models/format2ClientWrite.ts"() {
440
+ "use strict";
441
+ }
442
+ });
443
+
444
+ // src/models/format2OfflineWindow.ts
445
+ var DAY_MS;
446
+ var init_format2OfflineWindow = __esm({
447
+ "src/models/format2OfflineWindow.ts"() {
448
+ "use strict";
449
+ DAY_MS = 24 * 60 * 60 * 1e3;
450
+ }
451
+ });
452
+
110
453
  // src/models/BaseModel.ts
111
454
  function generateULID() {
112
- return (0, import_ulid.ulid)();
455
+ return (0, import_ulid2.ulid)();
113
456
  }
114
- var Y3, import_ulid, Logger;
457
+ var Y7, import_ulid2, Logger;
115
458
  var init_BaseModel = __esm({
116
459
  "src/models/BaseModel.ts"() {
117
460
  "use strict";
118
- Y3 = __toESM(require("yjs"), 1);
119
- import_ulid = require("ulid");
461
+ Y7 = __toESM(require("yjs"), 1);
462
+ import_ulid2 = require("ulid");
120
463
  init_StringSet();
121
464
  init_documentTypes();
122
465
  init_DocumentQueryTranslator();
123
466
  init_CursorManager();
124
467
  init_sql();
125
468
  init_metaSync();
469
+ init_writeLifecycle();
126
470
  init_yjsValues();
471
+ init_uniqueIndex();
472
+ init_largeDocuments();
473
+ init_OrmContext();
474
+ init_format2QueryProjection();
475
+ init_format2Observer();
476
+ init_format2ClientWrite();
477
+ init_format2OfflineWindow();
127
478
  Logger = class {
128
479
  static _logLevel = 1 /* ERROR */;
129
480
  static _logCallback = null;
@@ -239,6 +590,32 @@ var DatabaseEngine = class {
239
590
  getTableName(_modelName) {
240
591
  throw new Error("Method not implemented.");
241
592
  }
593
+ /**
594
+ * A SYNCHRONOUS SQL executor over this engine's database, used by the
595
+ * large-document (format 2) record store. That store commits a merged
596
+ * record row and its pending-operation entry as one transaction — the
597
+ * barrier a `save()` resolves behind — which the async `query()` API cannot
598
+ * express. Engines whose underlying SQLite is synchronous (better-sqlite3,
599
+ * SQL.js) provide it; anything else leaves it undefined and cannot host a
600
+ * large document.
601
+ */
602
+ createFormat2Sql;
603
+ /**
604
+ * Whether what {@link createFormat2Sql} writes survives this process.
605
+ *
606
+ * On a large document the merged record view IS the document: the Y.Doc
607
+ * holds one epoch's overlay, and everything else — including a record a
608
+ * `save()` has already resolved, and the pending write that still owes it to
609
+ * the server — lives only in this store. An engine whose database is in
610
+ * memory therefore cannot host one: a reload does not cost a cache, it costs
611
+ * the document and the writes nobody has acknowledged yet.
612
+ *
613
+ * The platform is asked separately about its storage quota; this answers the
614
+ * half only the engine knows. An engine that does not implement it is taken
615
+ * at the platform's word, which is what keeps hosts with storage of their
616
+ * own — a worker handed a file store, a test — working unchanged.
617
+ */
618
+ format2StorageIsDurable;
242
619
  // Transaction support
243
620
  async withTransaction(_callback) {
244
621
  throw new Error("Method not implemented.");