js-bao 0.7.0-alpha.6 → 0.7.0-alpha.7
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/browser.cjs +685 -35
- package/dist/browser.d.cts +240 -2
- package/dist/browser.d.ts +240 -2
- package/dist/browser.js +685 -35
- package/dist/client.cjs +64 -0
- package/dist/client.d.cts +180 -1
- package/dist/client.d.ts +180 -1
- package/dist/client.js +64 -0
- package/dist/cloudflare-do.cjs +93 -20
- package/dist/cloudflare-do.d.cts +216 -13
- package/dist/cloudflare-do.d.ts +216 -13
- package/dist/cloudflare-do.js +93 -20
- package/dist/cloudflare.cjs +104 -5
- package/dist/cloudflare.d.cts +207 -2
- package/dist/cloudflare.d.ts +207 -2
- package/dist/cloudflare.js +104 -5
- package/dist/codegen-v2.cjs +95 -6
- package/dist/index.cjs +686 -35
- package/dist/index.d.cts +240 -2
- package/dist/index.d.ts +240 -2
- package/dist/index.js +685 -35
- package/dist/node.cjs +686 -35
- package/dist/node.d.cts +240 -2
- package/dist/node.d.ts +240 -2
- package/dist/node.js +685 -35
- package/package.json +1 -1
package/dist/browser.cjs
CHANGED
|
@@ -5225,6 +5225,19 @@ var init_OrmContext = __esm({
|
|
|
5225
5225
|
format2Documents = /* @__PURE__ */ new Map();
|
|
5226
5226
|
/** Format-2 epoch moves in flight (#2816). */
|
|
5227
5227
|
format2Transitions = /* @__PURE__ */ new Map();
|
|
5228
|
+
/**
|
|
5229
|
+
* This instance's document observers' database writes that have not finished
|
|
5230
|
+
* yet, by document id (#3023).
|
|
5231
|
+
*
|
|
5232
|
+
* A Yjs observer fires synchronously with the update but writes to the
|
|
5233
|
+
* database asynchronously, so an update that has been APPLIED to a document
|
|
5234
|
+
* is not yet QUERYABLE. Whoever needs the difference — an `open()` that
|
|
5235
|
+
* promises its caller the synced records — waits on
|
|
5236
|
+
* {@link whenDocumentIndexed}. Per instance, not process-wide: two clients
|
|
5237
|
+
* holding the same document id write to their own engines, and neither
|
|
5238
|
+
* should be held up by the other's writes.
|
|
5239
|
+
*/
|
|
5240
|
+
documentIndexingWork = /* @__PURE__ */ new Map();
|
|
5228
5241
|
/**
|
|
5229
5242
|
* Who this ORM instance is in a format-2 store's unacknowledged-write log
|
|
5230
5243
|
* (#3431).
|
|
@@ -5402,6 +5415,56 @@ var init_OrmContext = __esm({
|
|
|
5402
5415
|
release();
|
|
5403
5416
|
}
|
|
5404
5417
|
}
|
|
5418
|
+
/**
|
|
5419
|
+
* Record one observer's asynchronous write of `docId` (#3023), so a caller
|
|
5420
|
+
* that needs the document queryable can wait for it. Failures are the
|
|
5421
|
+
* observer's own to log; a tracked promise never rejects, because the point
|
|
5422
|
+
* of the barrier is only to know when the write is over.
|
|
5423
|
+
*
|
|
5424
|
+
* A destroyed context tracks nothing: its observers are already inert, and a
|
|
5425
|
+
* torn-down instance must not leave a barrier that never drains.
|
|
5426
|
+
*/
|
|
5427
|
+
trackDocumentIndexing(docId, work) {
|
|
5428
|
+
if (this.destroyed) return;
|
|
5429
|
+
let inFlight = this.documentIndexingWork.get(docId);
|
|
5430
|
+
if (!inFlight) {
|
|
5431
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
5432
|
+
this.documentIndexingWork.set(docId, inFlight);
|
|
5433
|
+
}
|
|
5434
|
+
const pending = work.catch(() => {
|
|
5435
|
+
}).then(() => {
|
|
5436
|
+
inFlight.delete(pending);
|
|
5437
|
+
if (inFlight.size === 0 && this.documentIndexingWork.get(docId) === inFlight) {
|
|
5438
|
+
this.documentIndexingWork.delete(docId);
|
|
5439
|
+
}
|
|
5440
|
+
});
|
|
5441
|
+
inFlight.add(pending);
|
|
5442
|
+
}
|
|
5443
|
+
/**
|
|
5444
|
+
* Resolve once `docId`'s applied changes have reached this instance's query
|
|
5445
|
+
* tables (#3023). Answers `true` when the work drained, `false` when it gave
|
|
5446
|
+
* up waiting — the caller owns the logging, because this module holds no
|
|
5447
|
+
* runtime imports.
|
|
5448
|
+
*
|
|
5449
|
+
* An update applied to the Y.Doc is visible to a reader of the document
|
|
5450
|
+
* immediately, but the query tables only catch up when the observers' writes
|
|
5451
|
+
* land. An `open()` that waited for the initial sync waits for this too —
|
|
5452
|
+
* otherwise it resolves on the sync signal and the caller's first `query()`
|
|
5453
|
+
* sees a partly-written table.
|
|
5454
|
+
*
|
|
5455
|
+
* A write can enqueue more work (a nested map observed as it arrives), so
|
|
5456
|
+
* this drains in passes rather than once, and gives up after enough of them
|
|
5457
|
+
* that only a document under continuous change could still be busy — a
|
|
5458
|
+
* barrier is not a reason to hang.
|
|
5459
|
+
*/
|
|
5460
|
+
async whenDocumentIndexed(docId) {
|
|
5461
|
+
for (let pass = 0; pass < 50; pass++) {
|
|
5462
|
+
const inFlight = this.documentIndexingWork.get(docId);
|
|
5463
|
+
if (!inFlight || inFlight.size === 0) return true;
|
|
5464
|
+
await Promise.all([...inFlight]);
|
|
5465
|
+
}
|
|
5466
|
+
return false;
|
|
5467
|
+
}
|
|
5405
5468
|
/** Notify `subscribe()` callbacks registered on this context for `modelName`. */
|
|
5406
5469
|
notifyListeners(modelName, log, onError) {
|
|
5407
5470
|
const listeners = this.listenersMap.get(modelName);
|
|
@@ -5527,6 +5590,7 @@ var init_OrmContext = __esm({
|
|
|
5527
5590
|
this.modelDocMappingChangedListeners.clear();
|
|
5528
5591
|
this.format2Documents.clear();
|
|
5529
5592
|
this.format2Transitions.clear();
|
|
5593
|
+
this.documentIndexingWork.clear();
|
|
5530
5594
|
this.observedStringSetMaps = /* @__PURE__ */ new WeakSet();
|
|
5531
5595
|
liveOrmContexts.delete(this);
|
|
5532
5596
|
if (defaultOrmContext === this) {
|
|
@@ -5897,6 +5961,10 @@ var init_ModelRegistry = __esm({
|
|
|
5897
5961
|
* undefined the legacy session semantics are kept exactly: the global
|
|
5898
5962
|
* registry is SNAPSHOT at init time, so a model registered afterwards is not
|
|
5899
5963
|
* silently adopted by an already-running instance.
|
|
5964
|
+
*
|
|
5965
|
+
* The one way a running instance's registry grows is {@link adoptModel},
|
|
5966
|
+
* behind `registerModel` on the init result (#3615): an EXPLICIT call on the
|
|
5967
|
+
* instance the app means, not a relaxation of the snapshot rule.
|
|
5900
5968
|
*/
|
|
5901
5969
|
static createInstanceRegistry(modelClasses) {
|
|
5902
5970
|
const registry = new _ModelRegistry();
|
|
@@ -5920,6 +5988,64 @@ var init_ModelRegistry = __esm({
|
|
|
5920
5988
|
registry.setExplicitModelsForSession(modelClasses);
|
|
5921
5989
|
return registry;
|
|
5922
5990
|
}
|
|
5991
|
+
/**
|
|
5992
|
+
* The name `modelClass` may be adopted under on THIS instance, or a refusal
|
|
5993
|
+
* that names what to change (#3615).
|
|
5994
|
+
*
|
|
5995
|
+
* Both checks are made before anything is mutated, so a registration that
|
|
5996
|
+
* cannot go through leaves the registry exactly as it was.
|
|
5997
|
+
*/
|
|
5998
|
+
assertAdoptable(modelClass, instanceId) {
|
|
5999
|
+
const schema = modelClass?.getSchema?.();
|
|
6000
|
+
const modelName = modelClass?.modelName ?? schema?.options?.name;
|
|
6001
|
+
if (!modelName || !schema?.options?.name || !schema?.resolvedUniqueConstraints) {
|
|
6002
|
+
throw new Error(
|
|
6003
|
+
`[initJsBao] Cannot register a model class with no name. Build it with defineModelSchema and attachAndRegisterModel (or createModelClass) so it carries a static modelName and a schema with resolvedUniqueConstraints.`
|
|
6004
|
+
);
|
|
6005
|
+
}
|
|
6006
|
+
const existing = this.models.get(modelName);
|
|
6007
|
+
if (existing && existing !== modelClass) {
|
|
6008
|
+
throw new Error(
|
|
6009
|
+
`[initJsBao] Model '${modelName}' is already registered on js-bao instance #${instanceId} with a different class. Register the existing class, or give the new model a different name.`
|
|
6010
|
+
);
|
|
6011
|
+
}
|
|
6012
|
+
return modelName;
|
|
6013
|
+
}
|
|
6014
|
+
/**
|
|
6015
|
+
* Adopt `modelClass` into THIS instance's registry at runtime (#3615).
|
|
6016
|
+
*
|
|
6017
|
+
* The counterpart to the snapshot rule on
|
|
6018
|
+
* {@link ModelRegistry.createInstanceRegistry}: an instance grows only where
|
|
6019
|
+
* the app says so, and the global singleton is deliberately not touched —
|
|
6020
|
+
* registering here makes the model visible to this instance and to no other.
|
|
6021
|
+
*
|
|
6022
|
+
* Returns `{ added: false }` when the very same class is already ACTIVE on
|
|
6023
|
+
* this instance, which is what makes a repeat `registerModel` a no-op rather
|
|
6024
|
+
* than a warning about overwriting. Active, not merely present: an instance
|
|
6025
|
+
* built with `models: []` carries the whole global registry in `models` with
|
|
6026
|
+
* nothing active, and a class only the backing snapshot holds has neither
|
|
6027
|
+
* been validated for this session nor bound to its context, so it still has
|
|
6028
|
+
* an adoption to go through.
|
|
6029
|
+
*/
|
|
6030
|
+
adoptModel(modelClass, options = {}) {
|
|
6031
|
+
const modelName = this.assertAdoptable(modelClass, options.instanceId);
|
|
6032
|
+
if (this.getActiveModels().get(modelName) === modelClass) {
|
|
6033
|
+
return { added: false };
|
|
6034
|
+
}
|
|
6035
|
+
const candidate = new Map(this.getActiveModels());
|
|
6036
|
+
candidate.set(modelName, modelClass);
|
|
6037
|
+
this.validateModels(candidate);
|
|
6038
|
+
const schema = modelClass.getSchema?.();
|
|
6039
|
+
this.models.set(modelName, modelClass);
|
|
6040
|
+
if (schema?.options) this.modelOptions.set(modelName, schema.options);
|
|
6041
|
+
if (this.activeSessionModels) {
|
|
6042
|
+
this.activeSessionModels.set(modelName, modelClass);
|
|
6043
|
+
}
|
|
6044
|
+
Logger.debug(
|
|
6045
|
+
`[ModelRegistry] Model "${modelName}" adopted by instance #${options.instanceId} at runtime.`
|
|
6046
|
+
);
|
|
6047
|
+
return { added: true };
|
|
6048
|
+
}
|
|
5923
6049
|
registerModel(modelClass, options, fields) {
|
|
5924
6050
|
if (!options.name) {
|
|
5925
6051
|
throw new Error(
|
|
@@ -6122,7 +6248,8 @@ var init_ModelRegistry = __esm({
|
|
|
6122
6248
|
Logger.debug(`[ModelRegistry] Data removal complete for document ${docId}`);
|
|
6123
6249
|
}
|
|
6124
6250
|
// New method to initialize relationships
|
|
6125
|
-
async initializeRelationships() {
|
|
6251
|
+
async initializeRelationships(fallbackDbEngine) {
|
|
6252
|
+
if (!this.dbEngine && fallbackDbEngine) this.dbEngine = fallbackDbEngine;
|
|
6126
6253
|
if (!this.dbEngine) {
|
|
6127
6254
|
console.error(
|
|
6128
6255
|
"[ModelRegistry] DB engine not available for initializing relationships. Ensure initializeAll has been called."
|
|
@@ -6274,7 +6401,16 @@ var init_ModelRegistry = __esm({
|
|
|
6274
6401
|
return this.activeSessionModels || this.models;
|
|
6275
6402
|
}
|
|
6276
6403
|
validateSessionModels() {
|
|
6277
|
-
|
|
6404
|
+
this.validateModels(this.getActiveModels());
|
|
6405
|
+
}
|
|
6406
|
+
/**
|
|
6407
|
+
* Every relationship of `activeModels` names a model that is also in it.
|
|
6408
|
+
*
|
|
6409
|
+
* Taken as an argument rather than read off the registry so a runtime
|
|
6410
|
+
* adoption can be validated against what the instance WOULD hold before it
|
|
6411
|
+
* holds it (#3615).
|
|
6412
|
+
*/
|
|
6413
|
+
validateModels(activeModels) {
|
|
6278
6414
|
if (!activeModels || activeModels.size === 0) {
|
|
6279
6415
|
return;
|
|
6280
6416
|
}
|
|
@@ -6726,6 +6862,14 @@ var init_IncludeResolver = __esm({
|
|
|
6726
6862
|
function generateULID() {
|
|
6727
6863
|
return (0, import_ulid2.ulid)();
|
|
6728
6864
|
}
|
|
6865
|
+
function reindexFailure(modelConstructor, modelName, recordId, docId, error) {
|
|
6866
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6867
|
+
const failure = new Error(
|
|
6868
|
+
`[${modelConstructor.name}] Error re-indexing ${modelName}/${recordId} of document ${docId}: ${message}`
|
|
6869
|
+
);
|
|
6870
|
+
failure.cause = error;
|
|
6871
|
+
return failure;
|
|
6872
|
+
}
|
|
6729
6873
|
var Y7, import_ulid2, FORMAT2_ROW_PAGE, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
|
|
6730
6874
|
var init_BaseModel = __esm({
|
|
6731
6875
|
"src/models/BaseModel.ts"() {
|
|
@@ -7598,19 +7742,31 @@ var init_BaseModel = __esm({
|
|
|
7598
7742
|
if (fieldOptions?.default === void 0) return void 0;
|
|
7599
7743
|
return typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
|
|
7600
7744
|
}
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7745
|
+
/**
|
|
7746
|
+
* The members of a stringset as it is STORED, in stored order.
|
|
7747
|
+
*
|
|
7748
|
+
* Static and value-only so that the two readers of a stored stringset — the
|
|
7749
|
+
* instance's `getStringSetFromYjs`, which feeds the value a save builds its
|
|
7750
|
+
* unique key from, and `_readableUniqueOwner`, which rebuilds another
|
|
7751
|
+
* record's key from the map — cannot drift apart (#3252).
|
|
7752
|
+
*/
|
|
7753
|
+
static _stringSetMembersFromStored(stored) {
|
|
7754
|
+
if (stored instanceof Y7.Map) {
|
|
7755
|
+
return Array.from(stored.keys());
|
|
7605
7756
|
}
|
|
7606
|
-
if (Array.isArray(
|
|
7607
|
-
return
|
|
7757
|
+
if (Array.isArray(stored)) {
|
|
7758
|
+
return stored.filter((member) => typeof member === "string");
|
|
7608
7759
|
}
|
|
7609
|
-
if (
|
|
7610
|
-
return Object.keys(
|
|
7760
|
+
if (stored && typeof stored === "object") {
|
|
7761
|
+
return Object.keys(stored).filter((k) => Boolean(stored[k]));
|
|
7611
7762
|
}
|
|
7612
7763
|
return [];
|
|
7613
7764
|
}
|
|
7765
|
+
getStringSetFromYjs(fieldName) {
|
|
7766
|
+
return this.constructor._stringSetMembersFromStored(
|
|
7767
|
+
this.getFromYjs(fieldName)
|
|
7768
|
+
);
|
|
7769
|
+
}
|
|
7614
7770
|
getOrCreateStringSet(fieldName) {
|
|
7615
7771
|
if (!this._stringSetFields.has(fieldName)) {
|
|
7616
7772
|
const initialValues = this.getStringSetFromYjs(fieldName);
|
|
@@ -7639,6 +7795,30 @@ var init_BaseModel = __esm({
|
|
|
7639
7795
|
`[${this.name}] initialize(yDoc, db) is no longer supported. Use initializeForDocument(yDoc, db, docId, permission)`
|
|
7640
7796
|
);
|
|
7641
7797
|
}
|
|
7798
|
+
/**
|
|
7799
|
+
* Record one observer's asynchronous write of `docId` on this class's ORM
|
|
7800
|
+
* instance (#3023), so a caller that needs the document queryable can wait
|
|
7801
|
+
* for it. The observers themselves track through the context they captured
|
|
7802
|
+
* at install time; this static is for callers that only hold a model class.
|
|
7803
|
+
*/
|
|
7804
|
+
static trackDocumentIndexing(docId, work) {
|
|
7805
|
+
resolveOrmContext(this).trackDocumentIndexing(docId, work);
|
|
7806
|
+
}
|
|
7807
|
+
/**
|
|
7808
|
+
* Resolve once `docId`'s applied changes have reached this class's instance
|
|
7809
|
+
* of the query tables (#3023). See
|
|
7810
|
+
* {@link OrmContext.whenDocumentIndexed} — an `open()` that waited for the
|
|
7811
|
+
* initial sync waits for this too, otherwise it resolves on the sync signal
|
|
7812
|
+
* and the caller's first `query()` sees a partly-written table.
|
|
7813
|
+
*/
|
|
7814
|
+
static async whenDocumentIndexed(docId) {
|
|
7815
|
+
const drained = await resolveOrmContext(this).whenDocumentIndexed(docId);
|
|
7816
|
+
if (!drained) {
|
|
7817
|
+
Logger.warn(
|
|
7818
|
+
`[BaseModel] Document ${docId} is still indexing after 50 drain passes; not waiting any longer`
|
|
7819
|
+
);
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7642
7822
|
/**
|
|
7643
7823
|
* Initialize this model for a connected document.
|
|
7644
7824
|
*
|
|
@@ -7985,7 +8165,7 @@ var init_BaseModel = __esm({
|
|
|
7985
8165
|
return recordIdsToDiscard;
|
|
7986
8166
|
};
|
|
7987
8167
|
const observerContext = resolveOrmContext(this);
|
|
7988
|
-
const
|
|
8168
|
+
const indexDocumentYMapChange = async (event) => {
|
|
7989
8169
|
if (observerContext.destroyed) return;
|
|
7990
8170
|
Logger.verbose(
|
|
7991
8171
|
`[${this.name}] Document YMap change detected for ${modelName}/${docId}:`,
|
|
@@ -8154,6 +8334,12 @@ var init_BaseModel = __esm({
|
|
|
8154
8334
|
);
|
|
8155
8335
|
observerContext.notifyListeners(modelName);
|
|
8156
8336
|
};
|
|
8337
|
+
const documentYMapObserver = (event) => {
|
|
8338
|
+
observerContext.trackDocumentIndexing(
|
|
8339
|
+
docId,
|
|
8340
|
+
indexDocumentYMapChange(event)
|
|
8341
|
+
);
|
|
8342
|
+
};
|
|
8157
8343
|
documentYMap.observe(documentYMapObserver);
|
|
8158
8344
|
observerContext.registerObserverDisposer(
|
|
8159
8345
|
docId,
|
|
@@ -8179,6 +8365,116 @@ var init_BaseModel = __esm({
|
|
|
8179
8365
|
`[${this.name}] Model ${modelName} initialization complete for document ${docId}.`
|
|
8180
8366
|
);
|
|
8181
8367
|
}
|
|
8368
|
+
/**
|
|
8369
|
+
* Re-index ONE record of a document from its current Y.Map state (#3615).
|
|
8370
|
+
*
|
|
8371
|
+
* A legacy document's initial load walks the Y.Map before the permanent
|
|
8372
|
+
* observers are installed, so an update that lands in that window reaches
|
|
8373
|
+
* Yjs and misses the query tables. For a document connected long ago — which
|
|
8374
|
+
* is exactly what a model registered at runtime is initialized against —
|
|
8375
|
+
* that window is open on a live document. `registerModel` collects the
|
|
8376
|
+
* record ids touched while it runs and brings each of them here afterwards,
|
|
8377
|
+
* writing the same row the permanent per-record observer would have written.
|
|
8378
|
+
*
|
|
8379
|
+
* A record the window DELETED is deleted here for the same reason.
|
|
8380
|
+
*
|
|
8381
|
+
* A storage failure REJECTS rather than being logged and passed over, the
|
|
8382
|
+
* way the permanent observers' writes are: this write is part of what a
|
|
8383
|
+
* registration promises before it resolves, so swallowing it would resolve a
|
|
8384
|
+
* registration whose query tables are missing the very updates this call
|
|
8385
|
+
* exists to catch, and mark the pair complete so no retry would repair it.
|
|
8386
|
+
*/
|
|
8387
|
+
static async reindexDocumentRecord(docId, recordId) {
|
|
8388
|
+
const modelConstructor = this;
|
|
8389
|
+
const schema = modelConstructor.getSchema?.();
|
|
8390
|
+
const modelName = schema?.options?.name;
|
|
8391
|
+
if (!modelName) return;
|
|
8392
|
+
const ctx = resolveOrmContext(modelConstructor);
|
|
8393
|
+
if (ctx.destroyed) return;
|
|
8394
|
+
const documentYMap = modelConstructor.documentYMaps.get(
|
|
8395
|
+
`${docId}_${modelName}`
|
|
8396
|
+
);
|
|
8397
|
+
if (!documentYMap) return;
|
|
8398
|
+
const engine = ctx.dbEngine ? await modelConstructor.queryEngineFor([docId], modelName) : null;
|
|
8399
|
+
if (!engine) return;
|
|
8400
|
+
const recordData = documentYMap.get(recordId);
|
|
8401
|
+
if (recordData === void 0 || recordData === null) {
|
|
8402
|
+
try {
|
|
8403
|
+
await engine.delete(modelName, recordId);
|
|
8404
|
+
} catch (error) {
|
|
8405
|
+
throw reindexFailure(
|
|
8406
|
+
modelConstructor,
|
|
8407
|
+
modelName,
|
|
8408
|
+
recordId,
|
|
8409
|
+
docId,
|
|
8410
|
+
error
|
|
8411
|
+
);
|
|
8412
|
+
}
|
|
8413
|
+
ctx.notifyListeners(modelName);
|
|
8414
|
+
return;
|
|
8415
|
+
}
|
|
8416
|
+
const entries = recordData instanceof Y7.Map ? Array.from(recordData.entries()) : Object.entries(recordData);
|
|
8417
|
+
const rowData = {};
|
|
8418
|
+
for (const [fieldKey, value] of entries) {
|
|
8419
|
+
const fieldOptions = schema.fields?.get(fieldKey);
|
|
8420
|
+
if (!fieldOptions || fieldOptions.type === "stringset") continue;
|
|
8421
|
+
if (value === void 0) continue;
|
|
8422
|
+
rowData[fieldKey] = isYjsComposite(value) ? normalizeYjsValueForStorage(value) : value;
|
|
8423
|
+
}
|
|
8424
|
+
if (!rowData.id) return;
|
|
8425
|
+
const permissionHint = modelConstructor.connectedDocuments.get(docId)?.permissionHint ?? "read-write";
|
|
8426
|
+
try {
|
|
8427
|
+
await engine.insert(modelName, {
|
|
8428
|
+
...rowData,
|
|
8429
|
+
type: modelName,
|
|
8430
|
+
_meta_doc_id: docId,
|
|
8431
|
+
_meta_permission_hint: permissionHint
|
|
8432
|
+
});
|
|
8433
|
+
} catch (error) {
|
|
8434
|
+
throw reindexFailure(
|
|
8435
|
+
modelConstructor,
|
|
8436
|
+
modelName,
|
|
8437
|
+
recordId,
|
|
8438
|
+
docId,
|
|
8439
|
+
error
|
|
8440
|
+
);
|
|
8441
|
+
}
|
|
8442
|
+
ctx.notifyListeners(modelName);
|
|
8443
|
+
}
|
|
8444
|
+
/**
|
|
8445
|
+
* Withdraw ONE document's registration of this model (#3615).
|
|
8446
|
+
*
|
|
8447
|
+
* A `registerModel` that fails part-way through a document keeps the class
|
|
8448
|
+
* registered, because retrying is the recovery. This is what keeps that
|
|
8449
|
+
* half-finished pair from LOOKING finished in the meantime: without the
|
|
8450
|
+
* Y.Map mapping a save raises the same `YMap not found` an unregistered
|
|
8451
|
+
* model raises, instead of writing into a document whose observers were
|
|
8452
|
+
* never installed, and clearing the rows the failed load managed to write
|
|
8453
|
+
* keeps a query from answering out of a document nothing is watching. The
|
|
8454
|
+
* retry's `initializeForDocument` publishes both again.
|
|
8455
|
+
*
|
|
8456
|
+
* `connectedDocuments` is deliberately left alone: it is the CONTEXT's map,
|
|
8457
|
+
* shared by every model on the instance, so the document itself is still
|
|
8458
|
+
* connected for everything else.
|
|
8459
|
+
*/
|
|
8460
|
+
static async withdrawDocumentRegistration(docId) {
|
|
8461
|
+
const modelConstructor = this;
|
|
8462
|
+
const schema = modelConstructor.getSchema?.();
|
|
8463
|
+
const modelName = schema?.options?.name;
|
|
8464
|
+
if (!modelName) return;
|
|
8465
|
+
modelConstructor.documentYMaps.delete(`${docId}_${modelName}`);
|
|
8466
|
+
const ctx = resolveOrmContext(modelConstructor);
|
|
8467
|
+
if (ctx.destroyed || !ctx.dbEngine) return;
|
|
8468
|
+
try {
|
|
8469
|
+
const engine = await modelConstructor.queryEngineFor([docId], modelName);
|
|
8470
|
+
await engine.deleteByDocumentId(modelName, docId);
|
|
8471
|
+
} catch (error) {
|
|
8472
|
+
Logger.warn(
|
|
8473
|
+
`[${modelConstructor.name}] Could not clear ${modelName} rows of document ${docId} while withdrawing an incomplete registration:`,
|
|
8474
|
+
error
|
|
8475
|
+
);
|
|
8476
|
+
}
|
|
8477
|
+
}
|
|
8182
8478
|
// New method to clean up data for a disconnected document
|
|
8183
8479
|
static async cleanupDocumentData(docId) {
|
|
8184
8480
|
const modelConstructor = this;
|
|
@@ -8437,6 +8733,48 @@ var init_BaseModel = __esm({
|
|
|
8437
8733
|
constraintName
|
|
8438
8734
|
);
|
|
8439
8735
|
}
|
|
8736
|
+
/**
|
|
8737
|
+
* The record that READABLY owns `uniqueKey` under `constraint` in one
|
|
8738
|
+
* document's model map, or `null`.
|
|
8739
|
+
*
|
|
8740
|
+
* The `_uniqueIdx_` map is a cache over the records, not the record store,
|
|
8741
|
+
* and the two can disagree (#3252): a replica can hold an index entry for a
|
|
8742
|
+
* record whose structs have not landed yet, or an entry a field change or
|
|
8743
|
+
* delete left behind. `save()` used to take the entry at its word, so the
|
|
8744
|
+
* app saw a `query` that returned no row for a value and a save refused for
|
|
8745
|
+
* that same value — the index knew a row nothing else could read.
|
|
8746
|
+
*
|
|
8747
|
+
* An entry counts only when its owner is readable: the record map exists
|
|
8748
|
+
* under that id in `modelYMap`, and the key built from the record's stored
|
|
8749
|
+
* fields is `uniqueKey`. Anything else is a stale entry, and the caller
|
|
8750
|
+
* treats it as an index miss — the engine fallback decides, exactly as it
|
|
8751
|
+
* does for a missing entry — so the write that follows lands and rewrites
|
|
8752
|
+
* the entry to the record that really holds the value.
|
|
8753
|
+
*
|
|
8754
|
+
* "The key built from the record's stored fields" has to mean the key that
|
|
8755
|
+
* record's own save built, so the stored value is read the way the save
|
|
8756
|
+
* read it: a stringset is stored as a nested Y.Map of its members but keyed
|
|
8757
|
+
* as the array `getCurrentJSState` puts in `dataToSave` (finding 3252-01).
|
|
8758
|
+
* Comparing the raw Y.Map would call every readable owner of a unique
|
|
8759
|
+
* stringset stale and let a duplicate straight through the constraint.
|
|
8760
|
+
*/
|
|
8761
|
+
static _readableUniqueOwner(modelYMap, constraintMap, constraint, uniqueKey, modelName) {
|
|
8762
|
+
const ownerId = constraintMap.get(uniqueKey);
|
|
8763
|
+
if (!ownerId) return null;
|
|
8764
|
+
const recordYMap = modelYMap?.get(ownerId);
|
|
8765
|
+
if (!(recordYMap instanceof Y7.Map)) return null;
|
|
8766
|
+
const fields = this.getSchema?.()?.fields;
|
|
8767
|
+
const heldKey = this._buildKeyFromValues(
|
|
8768
|
+
constraint.fields,
|
|
8769
|
+
constraint.fields.map((field) => {
|
|
8770
|
+
const stored = recordYMap.get(field);
|
|
8771
|
+
return fields?.get(field)?.type === "stringset" ? this._stringSetMembersFromStored(stored) : stored;
|
|
8772
|
+
}),
|
|
8773
|
+
modelName,
|
|
8774
|
+
constraint.name
|
|
8775
|
+
);
|
|
8776
|
+
return heldKey === uniqueKey ? ownerId : null;
|
|
8777
|
+
}
|
|
8440
8778
|
/**
|
|
8441
8779
|
* The connected format-2 document that holds this instance's record, for an
|
|
8442
8780
|
* instance the constructor could not resolve (#3429, finding 3429-03).
|
|
@@ -8634,7 +8972,14 @@ var init_BaseModel = __esm({
|
|
|
8634
8972
|
const constraintMap = yDoc.getMap(
|
|
8635
8973
|
uniqueIndexMapName(modelName, constraint.name)
|
|
8636
8974
|
);
|
|
8637
|
-
|
|
8975
|
+
const readableOwner = modelConstructor._readableUniqueOwner(
|
|
8976
|
+
targetYMap,
|
|
8977
|
+
constraintMap,
|
|
8978
|
+
constraint,
|
|
8979
|
+
newUniqueKey,
|
|
8980
|
+
modelName
|
|
8981
|
+
);
|
|
8982
|
+
if (!readableOwner) {
|
|
8638
8983
|
const constraintKeyValues = constraint.fields.map(
|
|
8639
8984
|
(field) => dataToSave[field]
|
|
8640
8985
|
);
|
|
@@ -8652,7 +8997,13 @@ var init_BaseModel = __esm({
|
|
|
8652
8997
|
if (upsertOnConstraint && upsertOnKey) {
|
|
8653
8998
|
const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
|
|
8654
8999
|
const constraintMap = yDoc.getMap(constraintMapName);
|
|
8655
|
-
const existingId =
|
|
9000
|
+
const existingId = modelConstructor._readableUniqueOwner(
|
|
9001
|
+
targetYMap ?? void 0,
|
|
9002
|
+
constraintMap,
|
|
9003
|
+
upsertOnConstraint,
|
|
9004
|
+
upsertOnKey,
|
|
9005
|
+
modelName
|
|
9006
|
+
);
|
|
8656
9007
|
if (existingId) {
|
|
8657
9008
|
if (this._constructorProvidedId && this.id !== existingId) {
|
|
8658
9009
|
throw new Error(
|
|
@@ -8752,7 +9103,13 @@ var init_BaseModel = __esm({
|
|
|
8752
9103
|
50
|
|
8753
9104
|
)}'`
|
|
8754
9105
|
);
|
|
8755
|
-
const existingRecordIdWithNewKey =
|
|
9106
|
+
const existingRecordIdWithNewKey = modelConstructor._readableUniqueOwner(
|
|
9107
|
+
targetYMap ?? void 0,
|
|
9108
|
+
constraintMap,
|
|
9109
|
+
constraint,
|
|
9110
|
+
newUniqueKey,
|
|
9111
|
+
modelName
|
|
9112
|
+
);
|
|
8756
9113
|
Logger.verbose(
|
|
8757
9114
|
`[${modelName}] Save Check (Item '${this.id}'): Found Existing ID: '${existingRecordIdWithNewKey}' for Key '${newUniqueKey.substring(
|
|
8758
9115
|
0,
|
|
@@ -10890,7 +11247,7 @@ var init_BaseModel = __esm({
|
|
|
10890
11247
|
);
|
|
10891
11248
|
modelConstructor.attachStringSetObserversToRecord(recordYMap, schema, docId);
|
|
10892
11249
|
const observerContext = resolveOrmContext(modelConstructor);
|
|
10893
|
-
const
|
|
11250
|
+
const indexNestedYMapChange = async (event) => {
|
|
10894
11251
|
if (observerContext.destroyed) return;
|
|
10895
11252
|
Logger.verbose(
|
|
10896
11253
|
`[${modelName}] Nested YMap change detected for record ${recordId} in document ${docId}:`,
|
|
@@ -10966,6 +11323,12 @@ var init_BaseModel = __esm({
|
|
|
10966
11323
|
}
|
|
10967
11324
|
observerContext.notifyListeners(modelName);
|
|
10968
11325
|
};
|
|
11326
|
+
const recordObserver = (event) => {
|
|
11327
|
+
observerContext.trackDocumentIndexing(
|
|
11328
|
+
docId,
|
|
11329
|
+
indexNestedYMapChange(event)
|
|
11330
|
+
);
|
|
11331
|
+
};
|
|
10969
11332
|
recordYMap.observe(recordObserver);
|
|
10970
11333
|
observerContext.registerObserverDisposer(
|
|
10971
11334
|
docId,
|
|
@@ -11041,6 +11404,7 @@ __export(browser_exports, {
|
|
|
11041
11404
|
encodeOverlayMutation: () => encodeOverlayMutation,
|
|
11042
11405
|
estimateMaterializedBytes: () => estimateMaterializedBytes,
|
|
11043
11406
|
features: () => features,
|
|
11407
|
+
findReservedFieldNameInToml: () => findReservedFieldNameInToml,
|
|
11044
11408
|
foldFormat2Overlay: () => foldFormat2Overlay,
|
|
11045
11409
|
followEpoch: () => followEpoch,
|
|
11046
11410
|
format2ClientDDL: () => format2ClientDDL,
|
|
@@ -14664,6 +15028,31 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
14664
15028
|
* hydration scope; an uncapped one answers with the models this instance
|
|
14665
15029
|
* registered, which is every model whose rows could be here.
|
|
14666
15030
|
*/
|
|
15031
|
+
/**
|
|
15032
|
+
* Hydrate ONE model's derived query tables for a bound large document
|
|
15033
|
+
* (#3615).
|
|
15034
|
+
*
|
|
15035
|
+
* What a model registered on a running instance needs: the merged view
|
|
15036
|
+
* already holds its rows — the fold and the overlay observer are keyed by
|
|
15037
|
+
* the model name the overlay carries, not by the registry — but the derived
|
|
15038
|
+
* tables `query()` reads were never built, because the model was not in the
|
|
15039
|
+
* registry when the document was connected.
|
|
15040
|
+
*
|
|
15041
|
+
* Forced, because the point is a rebuild: whatever mark a previous session
|
|
15042
|
+
* left says nothing about tables this instance has never projected into.
|
|
15043
|
+
*/
|
|
15044
|
+
async hydrateModel(docId, modelName) {
|
|
15045
|
+
const store = this.opened.get(docId);
|
|
15046
|
+
if (!store) return;
|
|
15047
|
+
if (!store.isHydrated(modelName)) {
|
|
15048
|
+
Logger.warn(
|
|
15049
|
+
`[DocumentManager] Large document ${docId} does not hold model '${modelName}' on this device (its hydration scope excludes it), so the query tables for '${modelName}' were not hydrated. Reads of '${modelName}' on ${docId} answer as they do for any model this device declined.`
|
|
15050
|
+
);
|
|
15051
|
+
return;
|
|
15052
|
+
}
|
|
15053
|
+
await this.hydrateQueryTables(docId, store, { only: modelName });
|
|
15054
|
+
await this.whenProjectionSettled();
|
|
15055
|
+
}
|
|
14667
15056
|
localModels(docId) {
|
|
14668
15057
|
const store = this.ormContext.getFormat2StoreAsync(docId);
|
|
14669
15058
|
if (!store) return null;
|
|
@@ -15199,12 +15588,70 @@ var DocumentManagerImpl = class {
|
|
|
15199
15588
|
modelRegistry;
|
|
15200
15589
|
/** The ORM instance this manager belongs to (#3039). */
|
|
15201
15590
|
ormContext;
|
|
15591
|
+
/**
|
|
15592
|
+
* The registration gate (#3615).
|
|
15593
|
+
*
|
|
15594
|
+
* `connectDocument` finishes its model loop BEFORE it publishes the document
|
|
15595
|
+
* in `connectedDocuments`, so a registration that read the map in that
|
|
15596
|
+
* interval would miss the document while its own loop had already run —
|
|
15597
|
+
* leaving it connected with no table, no rows and no observer for the new
|
|
15598
|
+
* model. Registrations therefore run one at a time on this chain; a connect,
|
|
15599
|
+
* a rebind and a disconnect wait for it at entry, and a registration waits
|
|
15600
|
+
* for the connects that got in before it ({@link activeConnects}) before it
|
|
15601
|
+
* reads the map. Connects of different documents that no registration
|
|
15602
|
+
* overlaps still run concurrently, as they always have.
|
|
15603
|
+
*/
|
|
15604
|
+
registrationChain = Promise.resolve();
|
|
15605
|
+
/** Connects past the gate and not yet published — what a registration waits for. */
|
|
15606
|
+
activeConnects = /* @__PURE__ */ new Set();
|
|
15607
|
+
/**
|
|
15608
|
+
* The `(model, document)` pairs whose initialization for this instance
|
|
15609
|
+
* COMPLETED (#3615).
|
|
15610
|
+
*
|
|
15611
|
+
* Registry membership alone cannot say it: a document-level failure leaves
|
|
15612
|
+
* the class registered but that document unusable, and `initializeForDocument`
|
|
15613
|
+
* clears a document's rows and installs a fresh observer on every run, so a
|
|
15614
|
+
* blanket retry would wipe the documents that had succeeded and double their
|
|
15615
|
+
* observers. A retry resumes with the pairs that are not in here.
|
|
15616
|
+
*/
|
|
15617
|
+
runtimeInitialized = /* @__PURE__ */ new Map();
|
|
15618
|
+
/**
|
|
15619
|
+
* Pass the registration gate, then run `work` as an in-flight connect
|
|
15620
|
+
* (#3615).
|
|
15621
|
+
*
|
|
15622
|
+
* The re-read after the await is what closes the window: a registration
|
|
15623
|
+
* publishes its chain synchronously when it is called, so a caller that
|
|
15624
|
+
* waited on the chain it saw first has to check whether another one was
|
|
15625
|
+
* queued behind it. Joining {@link activeConnects} happens with no await in
|
|
15626
|
+
* between, so nothing can slip between the check and the join.
|
|
15627
|
+
*/
|
|
15628
|
+
async enterConnect(work) {
|
|
15629
|
+
for (; ; ) {
|
|
15630
|
+
const chain = this.registrationChain;
|
|
15631
|
+
await chain;
|
|
15632
|
+
if (this.registrationChain === chain) break;
|
|
15633
|
+
}
|
|
15634
|
+
let settle;
|
|
15635
|
+
const ticket = new Promise((resolve) => settle = resolve);
|
|
15636
|
+
this.activeConnects.add(ticket);
|
|
15637
|
+
try {
|
|
15638
|
+
return await work();
|
|
15639
|
+
} finally {
|
|
15640
|
+
this.activeConnects.delete(ticket);
|
|
15641
|
+
settle();
|
|
15642
|
+
}
|
|
15643
|
+
}
|
|
15202
15644
|
async connectDocument(docId, yDoc, permissionHint, options) {
|
|
15645
|
+
return this.enterConnect(
|
|
15646
|
+
() => this.connectDocumentInner(docId, yDoc, permissionHint, options)
|
|
15647
|
+
);
|
|
15648
|
+
}
|
|
15649
|
+
async connectDocumentInner(docId, yDoc, permissionHint, options) {
|
|
15203
15650
|
if (this.connectedDocuments.has(docId)) {
|
|
15204
15651
|
console.warn(
|
|
15205
15652
|
`[DocumentManager] Document ${docId} is already connected. Disconnecting old version.`
|
|
15206
15653
|
);
|
|
15207
|
-
await this.
|
|
15654
|
+
await this.disconnectDocumentInner(docId);
|
|
15208
15655
|
}
|
|
15209
15656
|
const documentFormat = options?.documentFormat ?? 1;
|
|
15210
15657
|
const connectedDoc = {
|
|
@@ -15231,6 +15678,9 @@ var DocumentManagerImpl = class {
|
|
|
15231
15678
|
await this.format2.connect(docId, yDoc, options?.epoch);
|
|
15232
15679
|
}
|
|
15233
15680
|
this.connectedDocuments.set(docId, connectedDoc);
|
|
15681
|
+
for (const modelName of this.modelRegistry.getActiveModels().keys()) {
|
|
15682
|
+
this.markRuntimeInitialized(modelName, docId);
|
|
15683
|
+
}
|
|
15234
15684
|
this.notifyConnectionChange({
|
|
15235
15685
|
type: "connect",
|
|
15236
15686
|
docId,
|
|
@@ -15249,6 +15699,11 @@ var DocumentManagerImpl = class {
|
|
|
15249
15699
|
* models read and write through is exchanged.
|
|
15250
15700
|
*/
|
|
15251
15701
|
async rebindEpochDocument(docId, yDoc, options = {}) {
|
|
15702
|
+
return this.enterConnect(
|
|
15703
|
+
() => this.rebindEpochDocumentInner(docId, yDoc, options)
|
|
15704
|
+
);
|
|
15705
|
+
}
|
|
15706
|
+
async rebindEpochDocumentInner(docId, yDoc, options = {}) {
|
|
15252
15707
|
const connected = this.connectedDocuments.get(docId);
|
|
15253
15708
|
if (!connected) {
|
|
15254
15709
|
throw new Error(
|
|
@@ -15363,6 +15818,9 @@ var DocumentManagerImpl = class {
|
|
|
15363
15818
|
return await this.format2.recordPresence(docId, records);
|
|
15364
15819
|
}
|
|
15365
15820
|
async disconnectDocument(docId) {
|
|
15821
|
+
return this.enterConnect(() => this.disconnectDocumentInner(docId));
|
|
15822
|
+
}
|
|
15823
|
+
async disconnectDocumentInner(docId) {
|
|
15366
15824
|
const connectedDoc = this.connectedDocuments.get(docId);
|
|
15367
15825
|
if (!connectedDoc) {
|
|
15368
15826
|
Logger.warn(`[DocumentManager] Document ${docId} is not connected`);
|
|
@@ -15379,6 +15837,9 @@ var DocumentManagerImpl = class {
|
|
|
15379
15837
|
wipeModels ? { wipeIfLastModels: wipeModels } : {}
|
|
15380
15838
|
);
|
|
15381
15839
|
this.connectedDocuments.delete(docId);
|
|
15840
|
+
for (const docIds of this.runtimeInitialized.values()) {
|
|
15841
|
+
docIds.delete(docId);
|
|
15842
|
+
}
|
|
15382
15843
|
this.ormContext.disposeObserversForDocument(docId);
|
|
15383
15844
|
this.ormContext.clearMappingsForDocId(docId);
|
|
15384
15845
|
this.notifyConnectionChange({
|
|
@@ -15389,10 +15850,150 @@ var DocumentManagerImpl = class {
|
|
|
15389
15850
|
`[DocumentManager] Document ${docId} disconnected successfully`
|
|
15390
15851
|
);
|
|
15391
15852
|
}
|
|
15853
|
+
markRuntimeInitialized(modelName, docId) {
|
|
15854
|
+
let docIds = this.runtimeInitialized.get(modelName);
|
|
15855
|
+
if (!docIds) {
|
|
15856
|
+
docIds = /* @__PURE__ */ new Set();
|
|
15857
|
+
this.runtimeInitialized.set(modelName, docIds);
|
|
15858
|
+
}
|
|
15859
|
+
docIds.add(docId);
|
|
15860
|
+
}
|
|
15861
|
+
/**
|
|
15862
|
+
* Add a model to this running instance (#3615). See
|
|
15863
|
+
* {@link InitJsBaoResult.registerModel}.
|
|
15864
|
+
*/
|
|
15865
|
+
async registerModel(modelClass) {
|
|
15866
|
+
const previous = this.registrationChain;
|
|
15867
|
+
let release;
|
|
15868
|
+
this.registrationChain = new Promise((resolve) => release = resolve);
|
|
15869
|
+
try {
|
|
15870
|
+
await previous;
|
|
15871
|
+
while (this.activeConnects.size > 0) {
|
|
15872
|
+
await Promise.all(Array.from(this.activeConnects));
|
|
15873
|
+
}
|
|
15874
|
+
await this.runRegistration(modelClass);
|
|
15875
|
+
} finally {
|
|
15876
|
+
release();
|
|
15877
|
+
}
|
|
15878
|
+
}
|
|
15879
|
+
async runRegistration(modelClass) {
|
|
15880
|
+
if (this.ormContext.destroyed) {
|
|
15881
|
+
throw new Error(
|
|
15882
|
+
`[initJsBao] Cannot register model on destroyed js-bao instance #${this.ormContext.instanceId}. Create a new instance with initJsBao.`
|
|
15883
|
+
);
|
|
15884
|
+
}
|
|
15885
|
+
const modelName = this.modelRegistry.assertAdoptable(
|
|
15886
|
+
modelClass,
|
|
15887
|
+
this.ormContext.instanceId
|
|
15888
|
+
);
|
|
15889
|
+
const boundTo = modelClass?._ormContext;
|
|
15890
|
+
if (boundTo && boundTo !== this.ormContext && !boundTo.destroyed) {
|
|
15891
|
+
throw new Error(
|
|
15892
|
+
`[initJsBao] Model '${modelName}' is bound to live js-bao instance #${boundTo.instanceId}. Give instance #${this.ormContext.instanceId} its own model class (a fresh createModelClass / attachAndRegisterModel result).`
|
|
15893
|
+
);
|
|
15894
|
+
}
|
|
15895
|
+
const { added } = this.modelRegistry.adoptModel(modelClass, {
|
|
15896
|
+
instanceId: this.ormContext.instanceId
|
|
15897
|
+
});
|
|
15898
|
+
bindModelClass(this.ormContext, modelName, modelClass, "throw");
|
|
15899
|
+
let didDocumentWork = false;
|
|
15900
|
+
for (const connected of Array.from(this.connectedDocuments.values())) {
|
|
15901
|
+
if (this.runtimeInitialized.get(modelName)?.has(connected.docId)) {
|
|
15902
|
+
continue;
|
|
15903
|
+
}
|
|
15904
|
+
didDocumentWork = true;
|
|
15905
|
+
try {
|
|
15906
|
+
await this.initializeModelForConnectedDocument(
|
|
15907
|
+
modelClass,
|
|
15908
|
+
modelName,
|
|
15909
|
+
connected
|
|
15910
|
+
);
|
|
15911
|
+
} catch (error) {
|
|
15912
|
+
await modelClass.withdrawDocumentRegistration(connected.docId);
|
|
15913
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15914
|
+
const wrapped = new Error(
|
|
15915
|
+
`[initJsBao] registerModel('${modelName}') failed on document ${connected.docId}; registration is incomplete and can be retried: ` + message
|
|
15916
|
+
);
|
|
15917
|
+
wrapped.cause = error;
|
|
15918
|
+
throw wrapped;
|
|
15919
|
+
}
|
|
15920
|
+
this.markRuntimeInitialized(modelName, connected.docId);
|
|
15921
|
+
}
|
|
15922
|
+
if (added || didDocumentWork) {
|
|
15923
|
+
await this.modelRegistry.initializeRelationships(this.dbEngine);
|
|
15924
|
+
}
|
|
15925
|
+
}
|
|
15926
|
+
/**
|
|
15927
|
+
* Initialize one model for one already-connected document (#3615).
|
|
15928
|
+
*
|
|
15929
|
+
* The legacy half carries the window `initializeForDocument` leaves open: it
|
|
15930
|
+
* loads the Y.Map as records in an asynchronous transaction and installs its
|
|
15931
|
+
* observers afterwards, which on a document the app connected long ago is a
|
|
15932
|
+
* live document still receiving peer updates. A temporary deep observer
|
|
15933
|
+
* collects the record ids touched while that runs; once the instance's
|
|
15934
|
+
* indexing for the document has drained, each of them is written from its
|
|
15935
|
+
* CURRENT state, so the call resolves with the query tables holding every
|
|
15936
|
+
* update that arrived before it did.
|
|
15937
|
+
*/
|
|
15938
|
+
async initializeModelForConnectedDocument(modelClass, modelName, connected) {
|
|
15939
|
+
const { docId, yDoc, permissionHint } = connected;
|
|
15940
|
+
const documentFormat = connected.documentFormat ?? 1;
|
|
15941
|
+
if (documentFormat === 2) {
|
|
15942
|
+
await modelClass.initializeForDocument(
|
|
15943
|
+
yDoc,
|
|
15944
|
+
this.dbEngine,
|
|
15945
|
+
docId,
|
|
15946
|
+
permissionHint,
|
|
15947
|
+
2
|
|
15948
|
+
);
|
|
15949
|
+
await this.format2.hydrateModel(docId, modelName);
|
|
15950
|
+
return;
|
|
15951
|
+
}
|
|
15952
|
+
const touched = /* @__PURE__ */ new Set();
|
|
15953
|
+
const documentYMap = yDoc.getMap(modelName);
|
|
15954
|
+
const collect2 = (events) => {
|
|
15955
|
+
for (const event of events) {
|
|
15956
|
+
if (event.target === documentYMap) {
|
|
15957
|
+
for (const key of event.changes.keys.keys()) touched.add(key);
|
|
15958
|
+
} else if (event.path.length > 0) {
|
|
15959
|
+
touched.add(String(event.path[0]));
|
|
15960
|
+
}
|
|
15961
|
+
}
|
|
15962
|
+
};
|
|
15963
|
+
documentYMap.observeDeep(collect2);
|
|
15964
|
+
try {
|
|
15965
|
+
await modelClass.initializeForDocument(
|
|
15966
|
+
yDoc,
|
|
15967
|
+
this.dbEngine,
|
|
15968
|
+
docId,
|
|
15969
|
+
permissionHint,
|
|
15970
|
+
1
|
|
15971
|
+
);
|
|
15972
|
+
await this.whenDocumentIndexed(docId);
|
|
15973
|
+
for (const recordId of touched) {
|
|
15974
|
+
await modelClass.reindexDocumentRecord(docId, recordId);
|
|
15975
|
+
}
|
|
15976
|
+
} finally {
|
|
15977
|
+
documentYMap.unobserveDeep(collect2);
|
|
15978
|
+
}
|
|
15979
|
+
}
|
|
15392
15980
|
/** Resolves once every enqueued query-projection refresh has been applied. */
|
|
15393
15981
|
whenFormat2ProjectionSettled() {
|
|
15394
15982
|
return this.format2.whenProjectionSettled();
|
|
15395
15983
|
}
|
|
15984
|
+
/**
|
|
15985
|
+
* Resolves once THIS instance's query tables have caught up with `docId`
|
|
15986
|
+
* (#3023). Scoped to the instance's own context, so a second client holding
|
|
15987
|
+
* the same document id never waits on this one's writes.
|
|
15988
|
+
*/
|
|
15989
|
+
async whenDocumentIndexed(docId) {
|
|
15990
|
+
const drained = await this.ormContext.whenDocumentIndexed(docId);
|
|
15991
|
+
if (!drained) {
|
|
15992
|
+
Logger.warn(
|
|
15993
|
+
`[DocumentManager] Document ${docId} is still indexing after 50 drain passes; not waiting any longer`
|
|
15994
|
+
);
|
|
15995
|
+
}
|
|
15996
|
+
}
|
|
15396
15997
|
getConnectedDocuments() {
|
|
15397
15998
|
return new Map(this.connectedDocuments);
|
|
15398
15999
|
}
|
|
@@ -15420,22 +16021,33 @@ var DocumentManagerImpl = class {
|
|
|
15420
16021
|
};
|
|
15421
16022
|
function bindSessionModels(ctx, registry) {
|
|
15422
16023
|
for (const [modelName, modelClass] of registry.getActiveModels()) {
|
|
15423
|
-
|
|
15424
|
-
|
|
15425
|
-
|
|
15426
|
-
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
|
|
15430
|
-
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
16024
|
+
bindModelClass(ctx, modelName, modelClass, "warn");
|
|
16025
|
+
}
|
|
16026
|
+
}
|
|
16027
|
+
function bindModelClass(ctx, modelName, modelClass, onLiveConflict) {
|
|
16028
|
+
const current = modelClass?._ormContext;
|
|
16029
|
+
if (current === ctx) {
|
|
16030
|
+
ctx.boundModels.add(modelClass);
|
|
16031
|
+
return;
|
|
16032
|
+
}
|
|
16033
|
+
if (!current || current.destroyed) {
|
|
16034
|
+
Object.defineProperty(modelClass, "_ormContext", {
|
|
16035
|
+
value: ctx,
|
|
16036
|
+
writable: true,
|
|
16037
|
+
configurable: true,
|
|
16038
|
+
enumerable: false
|
|
16039
|
+
});
|
|
16040
|
+
ctx.boundModels.add(modelClass);
|
|
16041
|
+
return;
|
|
16042
|
+
}
|
|
16043
|
+
if (onLiveConflict === "throw") {
|
|
16044
|
+
throw new Error(
|
|
16045
|
+
`[initJsBao] Model '${modelName}' is bound to live js-bao instance #${current.instanceId}. Give instance #${ctx.instanceId} its own model class (a fresh createModelClass / attachAndRegisterModel result).`
|
|
15437
16046
|
);
|
|
15438
16047
|
}
|
|
16048
|
+
console.warn(
|
|
16049
|
+
`[initJsBao] Model '${modelName}' is already bound to a live js-bao instance #${current.instanceId} and stays with it; instance #${ctx.instanceId}'s queries, writes and subscriptions for '${modelName}' will route to instance #${current.instanceId}. Give each client its own model classes (the schemaToml path already does) if you need them isolated.`
|
|
16050
|
+
);
|
|
15439
16051
|
}
|
|
15440
16052
|
async function initJsBaoCore(options, getEngine) {
|
|
15441
16053
|
const { databaseConfig, models: modelClasses } = options;
|
|
@@ -15464,6 +16076,7 @@ async function initJsBaoCore(options, getEngine) {
|
|
|
15464
16076
|
destroy: () => ormContext.destroy(),
|
|
15465
16077
|
connectDocument: docManager.connectDocument.bind(docManager),
|
|
15466
16078
|
disconnectDocument: docManager.disconnectDocument.bind(docManager),
|
|
16079
|
+
registerModel: docManager.registerModel.bind(docManager),
|
|
15467
16080
|
format2SyncMarks: (docId) => format2SyncMarks(docId, ormContext),
|
|
15468
16081
|
format2StoredEpoch: (docId) => format2StoredEpoch(dbEngine, docId, ormContext),
|
|
15469
16082
|
// The pre-bind epoch read opens the same pool the bind will, so it needs
|
|
@@ -15496,6 +16109,7 @@ async function initJsBaoCore(options, getEngine) {
|
|
|
15496
16109
|
// queue updates after it). The transport awaits it before an update
|
|
15497
16110
|
// from the room counts as applied.
|
|
15498
16111
|
whenFormat2ProjectionSettled: docManager.whenFormat2ProjectionSettled.bind(docManager),
|
|
16112
|
+
whenDocumentIndexed: docManager.whenDocumentIndexed.bind(docManager),
|
|
15499
16113
|
format2LoadSnapshot: docManager.loadSnapshot.bind(docManager),
|
|
15500
16114
|
format2LocalModels: (docId) => docManager.localModels(docId),
|
|
15501
16115
|
format2RecordPresence: (docId, records) => docManager.recordPresence(docId, records),
|
|
@@ -15899,6 +16513,26 @@ function schemaToToml(schema) {
|
|
|
15899
16513
|
|
|
15900
16514
|
// src/models/tomlLoader.ts
|
|
15901
16515
|
var import_smol_toml = require("smol-toml");
|
|
16516
|
+
|
|
16517
|
+
// src/utils/reservedFields.ts
|
|
16518
|
+
function checkReservedFieldName(fieldName) {
|
|
16519
|
+
if (fieldName === "id") return null;
|
|
16520
|
+
if (fieldName === "type") {
|
|
16521
|
+
return `Field 'type' is reserved (maps to internal _type column in queries)`;
|
|
16522
|
+
}
|
|
16523
|
+
if (fieldName.startsWith("_")) {
|
|
16524
|
+
return `Field '${fieldName}' is reserved (fields starting with '_' are internal)`;
|
|
16525
|
+
}
|
|
16526
|
+
return null;
|
|
16527
|
+
}
|
|
16528
|
+
var ReservedFieldNameError = class extends Error {
|
|
16529
|
+
constructor(message) {
|
|
16530
|
+
super(message);
|
|
16531
|
+
this.name = "ReservedFieldNameError";
|
|
16532
|
+
}
|
|
16533
|
+
};
|
|
16534
|
+
|
|
16535
|
+
// src/models/tomlLoader.ts
|
|
15902
16536
|
var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
15903
16537
|
"string",
|
|
15904
16538
|
"number",
|
|
@@ -16045,6 +16679,7 @@ function parseRelationship(raw, context, strict) {
|
|
|
16045
16679
|
}
|
|
16046
16680
|
function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
16047
16681
|
const strict = options.strict !== false;
|
|
16682
|
+
const rejectReserved = options.rejectReservedFieldNames === true;
|
|
16048
16683
|
const parsed = (0, import_smol_toml.parse)(tomlString);
|
|
16049
16684
|
const models = parsed.models;
|
|
16050
16685
|
if (!models || typeof models !== "object") {
|
|
@@ -16061,11 +16696,14 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
16061
16696
|
const fields = {};
|
|
16062
16697
|
if (modelDef.fields) {
|
|
16063
16698
|
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
16064
|
-
fields
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16699
|
+
const context = `[models.${modelName}.fields.${fieldName}]`;
|
|
16700
|
+
if (rejectReserved) {
|
|
16701
|
+
const reserved = checkReservedFieldName(fieldName);
|
|
16702
|
+
if (reserved) {
|
|
16703
|
+
throw new ReservedFieldNameError(`${context}: ${reserved}`);
|
|
16704
|
+
}
|
|
16705
|
+
}
|
|
16706
|
+
fields[fieldName] = parseFieldOptions(fieldDef, context, strict);
|
|
16069
16707
|
}
|
|
16070
16708
|
}
|
|
16071
16709
|
let relationships;
|
|
@@ -16112,6 +16750,18 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
16112
16750
|
}
|
|
16113
16751
|
return schemas;
|
|
16114
16752
|
}
|
|
16753
|
+
function findReservedFieldNameInToml(tomlString) {
|
|
16754
|
+
try {
|
|
16755
|
+
loadSchemaFromTomlString(tomlString, {
|
|
16756
|
+
strict: false,
|
|
16757
|
+
rejectReservedFieldNames: true
|
|
16758
|
+
});
|
|
16759
|
+
return null;
|
|
16760
|
+
} catch (error) {
|
|
16761
|
+
if (error instanceof ReservedFieldNameError) return error.message;
|
|
16762
|
+
return null;
|
|
16763
|
+
}
|
|
16764
|
+
}
|
|
16115
16765
|
|
|
16116
16766
|
// src/browser.ts
|
|
16117
16767
|
init_metaSync();
|