@abloatai/ablo 0.35.0 → 0.36.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/CHANGELOG.md +713 -629
- package/README.md +56 -519
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +24 -4
- package/dist/BaseSyncedStore.js +53 -37
- package/dist/Database.d.ts +8 -20
- package/dist/Database.js +61 -59
- package/dist/InstanceCache.d.ts +6 -2
- package/dist/InstanceCache.js +18 -16
- package/dist/Model.d.ts +17 -7
- package/dist/Model.js +17 -7
- package/dist/ModelRegistry.d.ts +4 -0
- package/dist/ModelRegistry.js +17 -15
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/SyncClient.d.ts +5 -15
- package/dist/SyncClient.js +60 -57
- package/dist/client/Ablo.js +30 -19
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +8 -2
- package/dist/client/createModelProxy.d.ts +21 -1
- package/dist/client/createModelProxy.js +123 -57
- package/dist/client/humans.d.ts +30 -9
- package/dist/client/humans.js +45 -19
- package/dist/client/reactiveEngine.d.ts +16 -11
- package/dist/client/reactiveEngine.js +113 -335
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +23 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/docs/catalog.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/query/client.d.ts +3 -0
- package/dist/query/client.js +6 -5
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +5 -2
- package/dist/react/context.d.ts +2 -2
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAblo.d.ts +9 -2
- package/dist/react/useAblo.js +25 -7
- package/dist/schema/coordination.js +5 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/select.js +3 -0
- package/dist/schema/serialize.js +3 -0
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +13 -12
- package/dist/{core → stores}/StoreManager.d.ts +5 -3
- package/dist/{core → stores}/StoreManager.js +24 -22
- package/dist/stores/SyncActionStore.d.ts +3 -1
- package/dist/stores/SyncActionStore.js +9 -7
- package/dist/sync/BootstrapFetcher.d.ts +4 -0
- package/dist/sync/BootstrapFetcher.js +28 -26
- package/dist/sync/OnDemandLoader.d.ts +3 -0
- package/dist/sync/OnDemandLoader.js +1 -0
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +19 -2
- package/dist/sync/participants.js +3 -1
- package/dist/sync/schemas.d.ts +2 -1
- package/dist/sync/schemas.js +3 -3
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/auth/capability.d.ts +35 -0
- package/dist/transaction/auth/capability.js +25 -0
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/index.d.ts +4 -4
- package/dist/transaction/coordination/index.js +4 -3
- package/dist/transaction/coordination/locator.d.ts +23 -2
- package/dist/transaction/coordination/locator.js +22 -2
- package/dist/transaction/coordination/schema.d.ts +125 -62
- package/dist/transaction/coordination/schema.js +228 -64
- package/dist/transaction/coordination/targetConflict.js +32 -28
- package/dist/transaction/errorCodes.d.ts +2 -2
- package/dist/transaction/errorCodes.js +13 -9
- package/dist/transaction/plugin.d.ts +95 -2
- package/dist/transaction/plugin.js +21 -2
- package/dist/transaction/resources/httpResources.d.ts +57 -2
- package/dist/transaction/resources/modelOperations.d.ts +148 -40
- package/dist/transaction/resources/where.d.ts +16 -0
- package/dist/transaction/resources/where.js +45 -0
- package/dist/transaction/schema/field.d.ts +12 -18
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/openapi.d.ts +16 -15
- package/dist/transaction/schema/openapi.js +186 -25
- package/dist/transaction/schema/relation.d.ts +7 -2
- package/dist/transaction/schema/schema.d.ts +27 -0
- package/dist/transaction/schema/schema.js +20 -0
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
- package/dist/transaction/transport/httpClient.d.ts +9 -1
- package/dist/transaction/transport/httpClient.js +1 -0
- package/dist/transaction/transport/httpTransport.js +156 -44
- package/dist/transaction/transport/wsTransport.d.ts +2 -4
- package/dist/transaction/transport/wsTransport.js +16 -10
- package/dist/transaction/types/streams.d.ts +10 -0
- package/dist/transaction/utils/duration.d.ts +25 -0
- package/dist/transaction/utils/duration.js +32 -0
- package/dist/transaction/wire/accountResponses.d.ts +69 -0
- package/dist/transaction/wire/accountResponses.js +36 -1
- package/dist/transaction/wire/auth.d.ts +9 -2
- package/dist/transaction/wire/auth.js +7 -1
- package/dist/transaction/wire/claims.d.ts +164 -97
- package/dist/transaction/wire/claims.js +126 -28
- package/dist/transaction/wire/commit.d.ts +1 -1
- package/dist/transaction/wire/feedEvent.d.ts +27 -0
- package/dist/transaction/wire/feedEvent.js +27 -1
- package/dist/transaction/wire/frames.d.ts +2 -2
- package/dist/transaction/wire/inboundFrames.d.ts +12 -2
- package/dist/transaction/wire/index.d.ts +10 -6
- package/dist/transaction/wire/index.js +12 -3
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
- package/dist/transactions/mutations/MutationQueue.js +55 -45
- package/dist/transactions/mutations/commitPayload.d.ts +3 -2
- package/dist/transactions/mutations/commitPayload.js +5 -5
- package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
- package/dist/transactions/mutations/deltaConfirmation.js +10 -8
- package/dist/transactions/mutations/replayValidation.d.ts +2 -1
- package/dist/transactions/mutations/replayValidation.js +3 -2
- package/dist/{core → views}/QueryView.d.ts +1 -1
- package/dist/{core → views}/QueryView.js +1 -1
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/docs/agents.md +1 -1
- package/docs/api-keys.md +6 -6
- package/docs/api.md +5 -43
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +4 -4
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +235 -83
- package/docs/data-sources.md +4 -4
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +29 -5
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +22 -23
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +14 -3
- package/docs/mcp.md +7 -7
- package/docs/migration.md +34 -15
- package/docs/projects.md +1 -1
- package/docs/react.md +19 -8
- package/docs/sessions.md +1 -1
- package/docs/webhooks.md +9 -9
- package/llms.txt +4 -4
- package/package.json +13 -20
- package/dist/cli.cjs +0 -288600
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/docs/interaction-model.md +0 -99
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{source → transaction}/footprint.d.ts +0 -0
- /package/dist/{source → transaction}/footprint.js +0 -0
- /package/dist/{core → views}/ViewRegistry.js +0 -0
package/dist/Database.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* server. {@link BaseSyncedStore} drives it, and {@link InstanceCache} holds the
|
|
6
6
|
* in-memory mirror of what this class persists.
|
|
7
7
|
*/
|
|
8
|
-
import { DatabaseManager } from './
|
|
9
|
-
import { StoreManager } from './
|
|
8
|
+
import { DatabaseManager } from './stores/DatabaseManager.js';
|
|
9
|
+
import { StoreManager } from './stores/StoreManager.js';
|
|
10
10
|
import { ModelRegistry } from './ModelRegistry.js';
|
|
11
11
|
import { LoadStrategy } from './transaction/types/index.js';
|
|
12
|
-
import {
|
|
12
|
+
import { globalRuntime } from './context.js';
|
|
13
13
|
import { AbloConnectionError, AbloValidationError } from './transaction/errors.js';
|
|
14
14
|
import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
|
|
15
15
|
import { logPositionSchema } from './transaction/logPosition.js';
|
|
@@ -110,7 +110,7 @@ export class Database {
|
|
|
110
110
|
/** Essential fields that must be preserved during partial UPDATE merges.
|
|
111
111
|
* Sourced from SyncEngineConfig.essentialFields — consumers define their own. */
|
|
112
112
|
get essentialFields() {
|
|
113
|
-
return
|
|
113
|
+
return this.runtime.config.essentialFields;
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* When true, all IndexedDB operations are replaced with in-memory Maps.
|
|
@@ -121,13 +121,15 @@ export class Database {
|
|
|
121
121
|
* new Database(registry, bootstrap, { inMemory: true })
|
|
122
122
|
*/
|
|
123
123
|
inMemory;
|
|
124
|
+
runtime;
|
|
124
125
|
/** In-memory stores used when inMemory=true. Keyed by model name. */
|
|
125
126
|
inMemoryStores = new Map();
|
|
126
127
|
/** In-memory workspace metadata when inMemory=true. */
|
|
127
128
|
inMemoryMetadata = null;
|
|
128
129
|
constructor(modelRegistry, bootstrapHelper, options) {
|
|
129
|
-
this.
|
|
130
|
-
this.
|
|
130
|
+
this.runtime = options?.runtime ?? globalRuntime;
|
|
131
|
+
this.databaseManager = new DatabaseManager(this.runtime);
|
|
132
|
+
this.storeManager = new StoreManager(modelRegistry, this.runtime);
|
|
131
133
|
this.modelRegistry = modelRegistry;
|
|
132
134
|
this.bootstrapHelper = bootstrapHelper;
|
|
133
135
|
this.inMemory = options?.inMemory ?? false;
|
|
@@ -149,7 +151,7 @@ export class Database {
|
|
|
149
151
|
? this.inMemoryStores.get(modelName)
|
|
150
152
|
: this.storeManager.getStore(modelName);
|
|
151
153
|
if (!store && context) {
|
|
152
|
-
|
|
154
|
+
this.runtime.observability.breadcrumb(`Store not found for model: ${modelName}`, 'sync.database', 'warning', { context });
|
|
153
155
|
}
|
|
154
156
|
return store;
|
|
155
157
|
}
|
|
@@ -170,7 +172,7 @@ export class Database {
|
|
|
170
172
|
const requiredFields = this.essentialFields[modelName] ?? [];
|
|
171
173
|
const preserved = requiredFields.filter((field) => existing[field] !== undefined && delta[field] === undefined);
|
|
172
174
|
if (preserved.length > 0) {
|
|
173
|
-
|
|
175
|
+
this.runtime.logger.debug('[Database] UPDATE merged - preserved fields', {
|
|
174
176
|
modelName,
|
|
175
177
|
modelId: modelId.slice(0, 12),
|
|
176
178
|
deltaFields: Object.keys(delta),
|
|
@@ -188,7 +190,7 @@ export class Database {
|
|
|
188
190
|
// Creates InMemoryObjectStore instances for all registered models.
|
|
189
191
|
// Bootstrap via HTTP still works; only local persistence is skipped.
|
|
190
192
|
if (this.inMemory) {
|
|
191
|
-
|
|
193
|
+
this.runtime.logger.debug('Opening in-memory database (headless mode)');
|
|
192
194
|
const allModels = this.modelRegistry.getRegisteredModelNames();
|
|
193
195
|
for (const modelName of allModels) {
|
|
194
196
|
const storeName = `store_${modelName.toLowerCase()}`;
|
|
@@ -196,11 +198,11 @@ export class Database {
|
|
|
196
198
|
}
|
|
197
199
|
// Create a __transactions store for the offline queue
|
|
198
200
|
this.inMemoryStores.set('__transactions', new InMemoryObjectStore('__transactions', '__transactions'));
|
|
199
|
-
|
|
201
|
+
this.runtime.logger.info(`In-memory database opened: ${this.inMemoryStores.size} stores`);
|
|
200
202
|
return;
|
|
201
203
|
}
|
|
202
204
|
// ── Browser mode: IndexedDB (existing behavior, unchanged) ───
|
|
203
|
-
|
|
205
|
+
this.runtime.logger.debug('Opening IndexedDB database');
|
|
204
206
|
// Initialize meta database
|
|
205
207
|
await this.databaseManager.initializeMetaDatabase();
|
|
206
208
|
// Calculate database info
|
|
@@ -214,7 +216,7 @@ export class Database {
|
|
|
214
216
|
// Initialize stores
|
|
215
217
|
await this.storeManager.initializeStores(this.workspaceDb);
|
|
216
218
|
const readiness = await this.storeManager.checkReadinessOfStores();
|
|
217
|
-
|
|
219
|
+
this.runtime.logger.info(`Database opened: ${this.currentDbInfo.name} (${readiness.readyStores.length}/${readiness.totalStores} stores ready)`);
|
|
218
220
|
}
|
|
219
221
|
/**
|
|
220
222
|
* Shrink a record before persisting it. Drops `undefined` fields, empty
|
|
@@ -277,7 +279,7 @@ export class Database {
|
|
|
277
279
|
*/
|
|
278
280
|
markRequiresFullBootstrap() {
|
|
279
281
|
this._forceFullBootstrap = true;
|
|
280
|
-
|
|
282
|
+
this.runtime.logger.info('[Database] Marked for forced full bootstrap (sync group change)');
|
|
281
283
|
}
|
|
282
284
|
/**
|
|
283
285
|
* Smart bootstrap requirements based on data freshness
|
|
@@ -309,7 +311,7 @@ export class Database {
|
|
|
309
311
|
this._forceFullBootstrap = false;
|
|
310
312
|
const instantModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.instant);
|
|
311
313
|
const lazyModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.lazy);
|
|
312
|
-
|
|
314
|
+
this.runtime.logger.info('[Database.requiredBootstrap] Forced FULL bootstrap (sync group change)');
|
|
313
315
|
return {
|
|
314
316
|
type: 'full',
|
|
315
317
|
modelsToLoad: [...instantModels, ...lazyModels],
|
|
@@ -348,7 +350,7 @@ export class Database {
|
|
|
348
350
|
// we've confirmed the data it refers to actually exists in the stores.
|
|
349
351
|
const lastSyncId = dataExists ? metadataLastSyncId : 0;
|
|
350
352
|
// Log the resolved database state for diagnostics.
|
|
351
|
-
|
|
353
|
+
this.runtime.logger.debug('[Database.requiredBootstrap] State check', {
|
|
352
354
|
readinessReady: readiness.ready,
|
|
353
355
|
hasMetadata: !!metadata,
|
|
354
356
|
metadataLastSyncId,
|
|
@@ -365,13 +367,13 @@ export class Database {
|
|
|
365
367
|
if (offline && hasLocalData) {
|
|
366
368
|
// Offline with data - use local bootstrap (only option when offline)
|
|
367
369
|
type = 'local';
|
|
368
|
-
|
|
370
|
+
this.runtime.logger.info('Offline detected with local data - using local bootstrap');
|
|
369
371
|
}
|
|
370
372
|
else {
|
|
371
373
|
// The server is the source of truth: always use a full bootstrap
|
|
372
374
|
// when online.
|
|
373
375
|
type = 'full';
|
|
374
|
-
|
|
376
|
+
this.runtime.logger.info('Full bootstrap - server is source of truth', {
|
|
375
377
|
reason: offline ? 'offline_no_data' : 'server_authoritative',
|
|
376
378
|
hasLocalData,
|
|
377
379
|
lastSyncId,
|
|
@@ -396,12 +398,12 @@ export class Database {
|
|
|
396
398
|
* shrinkage check. Caller supplies the complete list, not just
|
|
397
399
|
* team-derived groups. */
|
|
398
400
|
syncGroups, onProgress) {
|
|
399
|
-
|
|
401
|
+
this.runtime.logger.debug('Starting bootstrap fetch', {
|
|
400
402
|
type: requirements.type,
|
|
401
403
|
lastSyncId: requirements.lastSyncId,
|
|
402
404
|
modelsToLoad: requirements.modelsToLoad,
|
|
403
405
|
});
|
|
404
|
-
|
|
406
|
+
this.runtime.logger.info('Database: Starting bootstrap from Go server', {
|
|
405
407
|
type: requirements.type,
|
|
406
408
|
syncGroups,
|
|
407
409
|
modelsToLoad: requirements.modelsToLoad,
|
|
@@ -410,12 +412,12 @@ export class Database {
|
|
|
410
412
|
// Fetch before any destructive operation, so a failed network
|
|
411
413
|
// request can't leave the local store empty.
|
|
412
414
|
const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
413
|
-
|
|
415
|
+
this.runtime.logger.info('Fetching bootstrap data from server (before clearing local data)', {
|
|
414
416
|
type: requirements.type,
|
|
415
417
|
lastSyncId: requirements.lastSyncId,
|
|
416
418
|
});
|
|
417
419
|
const bootstrapData = await this.bootstrapHelper.fetchBootstrap(requirements.lastSyncId);
|
|
418
|
-
|
|
420
|
+
this.runtime.logger.debug('Received bootstrap response', {
|
|
419
421
|
type: bootstrapData.type,
|
|
420
422
|
lastSyncId: bootstrapData.lastSyncId,
|
|
421
423
|
hasModels: !!bootstrapData.models,
|
|
@@ -431,7 +433,7 @@ export class Database {
|
|
|
431
433
|
// Handle partial bootstrap (delta batch)
|
|
432
434
|
if (bootstrapData.type === 'partial') {
|
|
433
435
|
const deltas = bootstrapData.deltas ?? [];
|
|
434
|
-
|
|
436
|
+
this.runtime.logger.info('Processing partial bootstrap with delta batch', {
|
|
435
437
|
deltaCount: deltas.length,
|
|
436
438
|
fromSyncId: requirements.lastSyncId,
|
|
437
439
|
toSyncId: bootstrapData.lastSyncId,
|
|
@@ -471,7 +473,7 @@ export class Database {
|
|
|
471
473
|
updatedAt: new Date(),
|
|
472
474
|
});
|
|
473
475
|
const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - startTime;
|
|
474
|
-
|
|
476
|
+
this.runtime.logger.info(`Partial bootstrap complete in ${elapsed.toFixed(2)}ms`, {
|
|
475
477
|
deltasApplied,
|
|
476
478
|
lastSyncId: bootstrapData.lastSyncId,
|
|
477
479
|
});
|
|
@@ -488,21 +490,21 @@ export class Database {
|
|
|
488
490
|
for (const [modelName, modelData] of Object.entries(bootstrapData.models)) {
|
|
489
491
|
// Handle null, undefined, or non-array data
|
|
490
492
|
if (!modelData) {
|
|
491
|
-
|
|
493
|
+
this.runtime.observability.breadcrumb(`No data received for ${modelName}`, 'sync.bootstrap', 'warning');
|
|
492
494
|
continue;
|
|
493
495
|
}
|
|
494
496
|
if (!Array.isArray(modelData)) {
|
|
495
|
-
|
|
497
|
+
this.runtime.observability.breadcrumb(`Skipping non-array data for ${modelName}`, 'sync.bootstrap', 'warning');
|
|
496
498
|
continue;
|
|
497
499
|
}
|
|
498
500
|
// Skip empty arrays silently (expected for some models)
|
|
499
501
|
if (modelData.length === 0) {
|
|
500
|
-
|
|
502
|
+
this.runtime.logger.debug(`No ${modelName} items to store (empty array)`);
|
|
501
503
|
continue;
|
|
502
504
|
}
|
|
503
505
|
const store = this.getStore(modelName, 'bootstrap');
|
|
504
506
|
if (!store) {
|
|
505
|
-
|
|
507
|
+
this.runtime.logger.debug(`[Bootstrap] NO IDB STORE for ${modelName} — ${modelData.length} items DROPPED`);
|
|
506
508
|
continue;
|
|
507
509
|
}
|
|
508
510
|
let writeErrors = 0;
|
|
@@ -520,7 +522,7 @@ export class Database {
|
|
|
520
522
|
}
|
|
521
523
|
catch (error) {
|
|
522
524
|
writeErrors++;
|
|
523
|
-
|
|
525
|
+
this.runtime.observability.breadcrumb(`Failed to store ${modelName} item`, 'sync.database', 'error', {
|
|
524
526
|
error: error instanceof Error ? error.message : String(error),
|
|
525
527
|
});
|
|
526
528
|
}
|
|
@@ -530,7 +532,7 @@ export class Database {
|
|
|
530
532
|
// against. Counted and surfaced here so a partial does not read as a
|
|
531
533
|
// clean bootstrap.
|
|
532
534
|
if (writeErrors > 0) {
|
|
533
|
-
|
|
535
|
+
this.runtime.observability.breadcrumb(`Stored ${modelName} with ${writeErrors} of ${modelData.length} items dropped`, 'sync.database', 'warning');
|
|
534
536
|
}
|
|
535
537
|
// Mark model as persisted after successful write
|
|
536
538
|
try {
|
|
@@ -548,8 +550,8 @@ export class Database {
|
|
|
548
550
|
updatedAt: new Date(),
|
|
549
551
|
});
|
|
550
552
|
const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - startTime;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
+
this.runtime.logger.info(`Bootstrap complete: ${modelsLoaded} items loaded, ${modelsStored} stored to IndexedDB in ${elapsed.toFixed(2)}ms`);
|
|
554
|
+
this.runtime.analytics?.capture('bootstrap_success', {
|
|
553
555
|
responseTime: elapsed,
|
|
554
556
|
modelsLoaded,
|
|
555
557
|
});
|
|
@@ -557,12 +559,12 @@ export class Database {
|
|
|
557
559
|
}
|
|
558
560
|
catch (error) {
|
|
559
561
|
// Comprehensive error logging for bootstrap failures
|
|
560
|
-
|
|
562
|
+
this.runtime.observability.captureBootstrapFailure(error, {
|
|
561
563
|
type: requirements.type,
|
|
562
564
|
navigatorOnline: typeof navigator !== 'undefined' ? navigator.onLine : undefined,
|
|
563
565
|
});
|
|
564
566
|
// Track bootstrap failure telemetry
|
|
565
|
-
|
|
567
|
+
this.runtime.analytics?.capture('bootstrap_failed', {
|
|
566
568
|
bootstrapType: requirements.type,
|
|
567
569
|
lastSyncId: requirements.lastSyncId,
|
|
568
570
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
@@ -617,7 +619,7 @@ export class Database {
|
|
|
617
619
|
// newly created, it was newly visible. We fall through to the 'I' case
|
|
618
620
|
// after a debug trace so the two can be disambiguated in logs.
|
|
619
621
|
case 'C':
|
|
620
|
-
|
|
622
|
+
this.runtime.observability.breadcrumb('Applying covering delta (gained permission)', 'sync.database', 'info', { modelName, modelId: modelId.slice(0, 12) });
|
|
621
623
|
// falls through
|
|
622
624
|
case 'I': {
|
|
623
625
|
// Skip when the delta payload was empty/null. IDB rejects
|
|
@@ -635,7 +637,7 @@ export class Database {
|
|
|
635
637
|
}
|
|
636
638
|
}
|
|
637
639
|
catch (err) {
|
|
638
|
-
|
|
640
|
+
this.runtime.observability.breadcrumb(`IndexedDB put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
|
|
639
641
|
error: err instanceof Error ? err.message : String(err),
|
|
640
642
|
});
|
|
641
643
|
throw err; // Re-throw to see the actual error
|
|
@@ -650,7 +652,7 @@ export class Database {
|
|
|
650
652
|
// building a record from partial update data would corrupt it
|
|
651
653
|
// (missing reportId, and so on).
|
|
652
654
|
if (!existing) {
|
|
653
|
-
|
|
655
|
+
this.runtime.observability.breadcrumb('Skipping UPDATE delta - no existing record to merge with', 'sync.database', 'warning', {
|
|
654
656
|
modelName,
|
|
655
657
|
modelId: modelId.slice(0, 12),
|
|
656
658
|
});
|
|
@@ -671,7 +673,7 @@ export class Database {
|
|
|
671
673
|
}
|
|
672
674
|
}
|
|
673
675
|
catch (err) {
|
|
674
|
-
|
|
676
|
+
this.runtime.observability.breadcrumb(`IndexedDB put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
|
|
675
677
|
error: err instanceof Error ? err.message : String(err),
|
|
676
678
|
});
|
|
677
679
|
throw err;
|
|
@@ -688,7 +690,7 @@ export class Database {
|
|
|
688
690
|
}
|
|
689
691
|
}
|
|
690
692
|
catch (err) {
|
|
691
|
-
|
|
693
|
+
this.runtime.observability.breadcrumb(`IndexedDB delete failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
|
|
692
694
|
error: err instanceof Error ? err.message : String(err),
|
|
693
695
|
});
|
|
694
696
|
// Surface failure so caller does not mutate InstanceCache inconsistently
|
|
@@ -706,7 +708,7 @@ export class Database {
|
|
|
706
708
|
}
|
|
707
709
|
}
|
|
708
710
|
catch (err) {
|
|
709
|
-
|
|
711
|
+
this.runtime.observability.breadcrumb(`IndexedDB archive put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
|
|
710
712
|
error: err instanceof Error ? err.message : String(err),
|
|
711
713
|
});
|
|
712
714
|
throw err;
|
|
@@ -722,7 +724,7 @@ export class Database {
|
|
|
722
724
|
// queue), we return a no-op verify rather than crashing the engine.
|
|
723
725
|
case 'G':
|
|
724
726
|
case 'S':
|
|
725
|
-
|
|
727
|
+
this.runtime.observability.breadcrumb(`Group membership delta (${actionType}) reached processDelta — should be handled upstream`, 'sync.database', 'warning', { modelName, modelId: modelId.slice(0, 12), actionType });
|
|
726
728
|
return { action: 'verify', modelName, modelId, data: null };
|
|
727
729
|
default: {
|
|
728
730
|
// The switch above is exhaustive over the declared action types, so
|
|
@@ -808,7 +810,7 @@ export class Database {
|
|
|
808
810
|
}
|
|
809
811
|
}
|
|
810
812
|
if (deleteSyncIds.size > 0) {
|
|
811
|
-
|
|
813
|
+
this.runtime.logger.debug('[Database.processDeltaBatch] Built DELETE index for conflict resolution', {
|
|
812
814
|
deleteCount: deleteSyncIds.size,
|
|
813
815
|
totalDeltas: deltas.length,
|
|
814
816
|
});
|
|
@@ -850,7 +852,7 @@ export class Database {
|
|
|
850
852
|
const deltaSyncId = delta.syncId ?? 0;
|
|
851
853
|
if (deleteSyncId >= deltaSyncId) {
|
|
852
854
|
// DELETE has equal or higher syncId - skip this UPDATE/INSERT
|
|
853
|
-
|
|
855
|
+
this.runtime.logger.debug('[Database.processDeltaBatch] Skipping stale delta (DELETE wins)', {
|
|
854
856
|
modelName: delta.modelName,
|
|
855
857
|
modelId: delta.modelId.slice(0, 12),
|
|
856
858
|
actionType: delta.actionType,
|
|
@@ -877,7 +879,7 @@ export class Database {
|
|
|
877
879
|
deltasByStore.get(delta.modelName).push({ idx, delta });
|
|
878
880
|
});
|
|
879
881
|
if (skippedDueToConflict > 0) {
|
|
880
|
-
|
|
882
|
+
this.runtime.logger.info('[Database.processDeltaBatch] Conflict resolution summary', {
|
|
881
883
|
skippedDueToConflict,
|
|
882
884
|
totalDeltas: deltas.length,
|
|
883
885
|
deleteCount: deleteSyncIds.size,
|
|
@@ -908,7 +910,7 @@ export class Database {
|
|
|
908
910
|
}
|
|
909
911
|
}
|
|
910
912
|
catch {
|
|
911
|
-
|
|
913
|
+
this.runtime.observability.breadcrumb(`Batch read failed for ${modelName}, falling back to individual reads`, 'sync.database', 'warning');
|
|
912
914
|
// Fallback: mark all as missing for self-healing
|
|
913
915
|
for (const id of updateIds) {
|
|
914
916
|
missingIds.add(id);
|
|
@@ -920,7 +922,7 @@ export class Database {
|
|
|
920
922
|
// so its delta is skipped).
|
|
921
923
|
const failedToFetch = new Set();
|
|
922
924
|
if (missingIds.size > 0) {
|
|
923
|
-
|
|
925
|
+
this.runtime.logger.info(`[Database.processDeltaBatch] Found ${missingIds.size} missing records for ${modelName}, fetching from server...`);
|
|
924
926
|
// Fetch sequentially to avoid overwhelming server
|
|
925
927
|
for (const id of missingIds) {
|
|
926
928
|
try {
|
|
@@ -928,24 +930,24 @@ export class Database {
|
|
|
928
930
|
if (fetchedRecord) {
|
|
929
931
|
const compacted = this.compactRecord(modelName, fetchedRecord);
|
|
930
932
|
existingRecords.set(id, compacted);
|
|
931
|
-
|
|
933
|
+
this.runtime.logger.debug(`[Database.processDeltaBatch] Successfully fetched missing record: ${modelName}:${id}`);
|
|
932
934
|
}
|
|
933
935
|
else {
|
|
934
936
|
// fetchEntity returns null for 404 — entity was deleted, skip the delta
|
|
935
937
|
failedToFetch.add(id);
|
|
936
|
-
|
|
938
|
+
this.runtime.logger.debug(`[Database.processDeltaBatch] Entity not found (deleted): ${modelName}:${id}`);
|
|
937
939
|
}
|
|
938
940
|
}
|
|
939
941
|
catch (error) {
|
|
940
942
|
// Unexpected error (5xx, network failure) — mark for skipping and report
|
|
941
943
|
failedToFetch.add(id);
|
|
942
|
-
|
|
944
|
+
this.runtime.observability.breadcrumb(`Failed to fetch missing record ${modelName}:${id}`, 'sync.database', 'warning', {
|
|
943
945
|
error: error instanceof Error ? error.message : String(error),
|
|
944
946
|
});
|
|
945
947
|
}
|
|
946
948
|
}
|
|
947
949
|
if (failedToFetch.size > 0) {
|
|
948
|
-
|
|
950
|
+
this.runtime.logger.info(`[Database.processDeltaBatch] Skipping ${failedToFetch.size} stale UPDATE deltas for deleted entities`, {
|
|
949
951
|
modelName,
|
|
950
952
|
failedCount: failedToFetch.size,
|
|
951
953
|
totalMissing: missingIds.size,
|
|
@@ -997,7 +999,7 @@ export class Database {
|
|
|
997
999
|
// store nor fetchable from the server (a 404), it was deleted,
|
|
998
1000
|
// so skip it rather than create an incomplete record.
|
|
999
1001
|
if (!existing && failedToFetch.has(modelId)) {
|
|
1000
|
-
|
|
1002
|
+
this.runtime.logger.debug('[Database.processDeltaBatch] Skipping UPDATE for deleted entity', {
|
|
1001
1003
|
modelName,
|
|
1002
1004
|
modelId: modelId.slice(0, 12),
|
|
1003
1005
|
});
|
|
@@ -1008,7 +1010,7 @@ export class Database {
|
|
|
1008
1010
|
// building a record from partial update data would corrupt it
|
|
1009
1011
|
// (missing reportId, and so on).
|
|
1010
1012
|
if (!existing) {
|
|
1011
|
-
|
|
1013
|
+
this.runtime.observability.breadcrumb('Batch: Skipping UPDATE delta - no existing record', 'sync.database', 'warning', {
|
|
1012
1014
|
modelName,
|
|
1013
1015
|
modelId: modelId.slice(0, 12),
|
|
1014
1016
|
});
|
|
@@ -1084,7 +1086,7 @@ export class Database {
|
|
|
1084
1086
|
// `DataError`, `AbortError`) so we can find what's wrong with
|
|
1085
1087
|
// the `compacted` payload shape or store schema.
|
|
1086
1088
|
const idbErr = err instanceof Error ? err : new Error(String(err));
|
|
1087
|
-
|
|
1089
|
+
this.runtime.logger.debug('[Database.processDeltaBatch] store tx FAILED', {
|
|
1088
1090
|
modelName,
|
|
1089
1091
|
storeDeltasCount: storeDeltas.length,
|
|
1090
1092
|
errorName: idbErr.name,
|
|
@@ -1097,7 +1099,7 @@ export class Database {
|
|
|
1097
1099
|
: typeof delta.data,
|
|
1098
1100
|
})),
|
|
1099
1101
|
});
|
|
1100
|
-
|
|
1102
|
+
this.runtime.observability.captureMutationFailure({
|
|
1101
1103
|
context: 'batch-indexeddb-operation',
|
|
1102
1104
|
modelName,
|
|
1103
1105
|
error: idbErr,
|
|
@@ -1123,7 +1125,7 @@ export class Database {
|
|
|
1123
1125
|
await this.updateWorkspaceMetadata({ lastSyncId: highestPersistedSyncId });
|
|
1124
1126
|
}
|
|
1125
1127
|
catch (err) {
|
|
1126
|
-
|
|
1128
|
+
this.runtime.observability.breadcrumb('Failed to update metadata after batch', 'sync.database', 'error', {
|
|
1127
1129
|
error: err instanceof Error ? err.message : String(err),
|
|
1128
1130
|
});
|
|
1129
1131
|
}
|
|
@@ -1133,7 +1135,7 @@ export class Database {
|
|
|
1133
1135
|
// persisted" signal loud when it actually happens. If this fires
|
|
1134
1136
|
// repeatedly on the same sync IDs, a specific row is un-writable
|
|
1135
1137
|
// (validation? compact issue?) and needs fixing at that layer.
|
|
1136
|
-
|
|
1138
|
+
this.runtime.logger.debug('[Database.processDeltaBatch] cursor withheld due to failed store tx', {
|
|
1137
1139
|
seen: highestSyncId,
|
|
1138
1140
|
persisted: highestPersistedSyncId,
|
|
1139
1141
|
gap: highestSyncId - highestPersistedSyncId,
|
|
@@ -1153,7 +1155,7 @@ export class Database {
|
|
|
1153
1155
|
async putRecord(modelName, id, data) {
|
|
1154
1156
|
const store = this.getStore(modelName, 'putRecord');
|
|
1155
1157
|
if (!store) {
|
|
1156
|
-
|
|
1158
|
+
this.runtime.observability.breadcrumb(`Store not found for putRecord: ${modelName}`, 'sync.database', 'warning');
|
|
1157
1159
|
return;
|
|
1158
1160
|
}
|
|
1159
1161
|
const compacted = this.compactRecord(modelName, data);
|
|
@@ -1196,7 +1198,7 @@ export class Database {
|
|
|
1196
1198
|
// Graceful degradation: skip if database is closing or not open
|
|
1197
1199
|
// This prevents "Database not opened" errors during React Strict Mode cleanup
|
|
1198
1200
|
if (!this.workspaceDb || this.isClosing) {
|
|
1199
|
-
|
|
1201
|
+
this.runtime.observability.breadcrumb('updateWorkspaceMetadata: Database not open or closing', 'sync.database', 'warning', {
|
|
1200
1202
|
hasDb: !!this.workspaceDb,
|
|
1201
1203
|
isClosing: this.isClosing,
|
|
1202
1204
|
});
|
|
@@ -1488,13 +1490,13 @@ export class Database {
|
|
|
1488
1490
|
}
|
|
1489
1491
|
await this.databaseManager.close();
|
|
1490
1492
|
this.currentDbInfo = null;
|
|
1491
|
-
|
|
1493
|
+
this.runtime.logger.debug('Database closed');
|
|
1492
1494
|
}
|
|
1493
1495
|
async clear(options = {}) {
|
|
1494
1496
|
await this.storeManager.clearAllStores();
|
|
1495
1497
|
if (options.includeWriteJournal) {
|
|
1496
1498
|
await this.transactionStore.clear();
|
|
1497
1499
|
}
|
|
1498
|
-
|
|
1500
|
+
this.runtime.logger.info('All stores cleared');
|
|
1499
1501
|
}
|
|
1500
1502
|
}
|
package/dist/InstanceCache.d.ts
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Model } from './Model.js';
|
|
10
10
|
import { ModelRegistry } from './ModelRegistry.js';
|
|
11
|
+
import type { RuntimeContext } from './RuntimeContext.js';
|
|
11
12
|
import { ModelScope } from './transaction/types/index.js';
|
|
12
|
-
import { ViewRegistry } from './
|
|
13
|
-
import { QueryView, type QueryViewOptions } from './
|
|
13
|
+
import { ViewRegistry } from './views/ViewRegistry.js';
|
|
14
|
+
import { QueryView, type QueryViewOptions } from './views/QueryView.js';
|
|
14
15
|
/** Constructor type for Model subclasses - uses abstract to handle variance */
|
|
15
16
|
type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
|
|
16
17
|
export { ModelScope };
|
|
@@ -19,6 +20,8 @@ interface PoolConfig {
|
|
|
19
20
|
maxAge?: number;
|
|
20
21
|
gcInterval?: number;
|
|
21
22
|
useWeakRefs?: boolean;
|
|
23
|
+
/** The owning client's runtime. Defaults to the module-global bridge. */
|
|
24
|
+
runtime?: RuntimeContext;
|
|
22
25
|
}
|
|
23
26
|
interface DeltaInfo {
|
|
24
27
|
action?: string;
|
|
@@ -38,6 +41,7 @@ export declare class InstanceCache {
|
|
|
38
41
|
private foreignKeyConfig;
|
|
39
42
|
private metrics;
|
|
40
43
|
private config;
|
|
44
|
+
private readonly runtime;
|
|
41
45
|
private gcTimer?;
|
|
42
46
|
readonly registry: ModelRegistry;
|
|
43
47
|
readonly viewRegistry: ViewRegistry;
|
package/dist/InstanceCache.js
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
import { makeObservable, observable, action, computed, runInAction } from 'mobx';
|
|
10
10
|
import { Model } from './Model.js';
|
|
11
11
|
import { ModelRegistry } from './ModelRegistry.js';
|
|
12
|
-
import {
|
|
12
|
+
import { globalRuntime } from './context.js';
|
|
13
13
|
import { AbloValidationError } from './transaction/errors.js';
|
|
14
14
|
import { ModelScope } from './transaction/types/index.js';
|
|
15
|
-
import { ViewRegistry } from './
|
|
16
|
-
import { QueryView } from './
|
|
15
|
+
import { ViewRegistry } from './views/ViewRegistry.js';
|
|
16
|
+
import { QueryView } from './views/QueryView.js';
|
|
17
17
|
// Re-exported so `import { ModelScope } from './InstanceCache.js'` resolves
|
|
18
18
|
export { ModelScope };
|
|
19
19
|
/**
|
|
@@ -51,6 +51,7 @@ export class InstanceCache {
|
|
|
51
51
|
};
|
|
52
52
|
// Configuration
|
|
53
53
|
config;
|
|
54
|
+
runtime;
|
|
54
55
|
gcTimer;
|
|
55
56
|
// ModelRegistry instance — single source of truth for model metadata
|
|
56
57
|
registry;
|
|
@@ -106,6 +107,7 @@ export class InstanceCache {
|
|
|
106
107
|
gcInterval: config.gcInterval ?? 60000, // 1 minute
|
|
107
108
|
useWeakRefs: config.useWeakRefs ?? true,
|
|
108
109
|
};
|
|
110
|
+
this.runtime = config.runtime ?? globalRuntime;
|
|
109
111
|
// Store the model registry reference
|
|
110
112
|
if (!modelRegistry) {
|
|
111
113
|
throw new AbloValidationError('InstanceCache requires ModelRegistry for production-safe model name lookup', { code: 'pool_registry_missing' });
|
|
@@ -264,7 +266,7 @@ export class InstanceCache {
|
|
|
264
266
|
(history.lastAction === 'U' || history.lastAction === 'D')) {
|
|
265
267
|
// Internal delta-ordering anomaly that reconciles on the next
|
|
266
268
|
// catch-up — forensic, not consumer-actionable → debug.
|
|
267
|
-
|
|
269
|
+
this.runtime.logger.debug(`InstanceCache.add() SUSPICIOUS: INSERT after ${history.lastAction}`, { modelType, id, syncId: deltaInfo.syncId });
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
// Update delta history
|
|
@@ -539,7 +541,7 @@ export class InstanceCache {
|
|
|
539
541
|
// This is expected during migration from hand-coded → dynamic models.
|
|
540
542
|
}
|
|
541
543
|
catch (e) {
|
|
542
|
-
|
|
544
|
+
this.runtime.observability.breadcrumb(`Failed to create fallback instance for ${modelClass.name}`, 'sync.database', 'error', {
|
|
543
545
|
error: e instanceof Error ? e.message : String(e),
|
|
544
546
|
});
|
|
545
547
|
return [];
|
|
@@ -647,8 +649,8 @@ export class InstanceCache {
|
|
|
647
649
|
if (modelName === 'Unknown') {
|
|
648
650
|
// Malformed row with no type marker — dropped, but nothing the consumer
|
|
649
651
|
// can act on (the actionable schema-drift case is handled below) → debug.
|
|
650
|
-
|
|
651
|
-
|
|
652
|
+
this.runtime.logger.debug('InstanceCache.createFromData: No model identifier found', { data });
|
|
653
|
+
this.runtime.modelDebugLogger?.logError('Unknown', 'CREATE', 'No model identifier found', data);
|
|
652
654
|
return null;
|
|
653
655
|
}
|
|
654
656
|
if (opts?.strict) {
|
|
@@ -661,9 +663,9 @@ export class InstanceCache {
|
|
|
661
663
|
// Genuinely actionable and NOT self-healing: a model the server is sending
|
|
662
664
|
// isn't in your schema, so these rows are silently skipped. Keep at warn,
|
|
663
665
|
// consumer register (their model name + the `ablo status` fix); forensics ride debug.
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
666
|
+
this.runtime.logger.warn(`Received data for "${modelName}", which isn't in your schema — these rows will be skipped. Run \`ablo status\` to compare your local schema with the server.`);
|
|
667
|
+
this.runtime.logger.debug(`InstanceCache.createFromData: No constructor found for model "${modelName}"`, { data });
|
|
668
|
+
this.runtime.modelDebugLogger?.logError(modelName, 'CREATE', `No constructor found for model "${modelName}"`, data);
|
|
667
669
|
return null;
|
|
668
670
|
}
|
|
669
671
|
// If the model already exists, update it in place instead of creating a
|
|
@@ -680,7 +682,7 @@ export class InstanceCache {
|
|
|
680
682
|
// Don't return existing, create new model (will use composite key for storage)
|
|
681
683
|
}
|
|
682
684
|
// Log model creation attempt
|
|
683
|
-
|
|
685
|
+
this.runtime.modelDebugLogger?.logCreation(modelName, data, Constructor);
|
|
684
686
|
try {
|
|
685
687
|
// Pass data directly to constructor for Prisma-first models
|
|
686
688
|
const model = new Constructor(data);
|
|
@@ -690,14 +692,14 @@ export class InstanceCache {
|
|
|
690
692
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
691
693
|
// Internal construction failure — captured via observability below and
|
|
692
694
|
// re-fetched on resync; the stack is forensic → debug.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
+
this.runtime.logger.debug(`[InstanceCache.createFromData] FAILED ${modelName}`, { errorMessage, stack: error instanceof Error ? error.stack : undefined });
|
|
696
|
+
this.runtime.observability.captureMutationFailure({
|
|
695
697
|
context: 'createFromData',
|
|
696
698
|
modelName,
|
|
697
699
|
modelId: data.id,
|
|
698
700
|
error: errorMessage,
|
|
699
701
|
});
|
|
700
|
-
|
|
702
|
+
this.runtime.modelDebugLogger?.logError(modelName, 'CREATE', errorMessage, {
|
|
701
703
|
data,
|
|
702
704
|
constructor: Constructor.name,
|
|
703
705
|
});
|
|
@@ -904,7 +906,7 @@ export class InstanceCache {
|
|
|
904
906
|
}
|
|
905
907
|
}
|
|
906
908
|
if (skippedObserved > 0) {
|
|
907
|
-
|
|
909
|
+
this.runtime.logger.debug(`[InstanceCache GC] Skipped ${skippedObserved} models with active React observers`);
|
|
908
910
|
}
|
|
909
911
|
// Also clean up old tracking data
|
|
910
912
|
this.clearDeltaHistory();
|
|
@@ -1046,7 +1048,7 @@ export class InstanceCache {
|
|
|
1046
1048
|
// has dangling refs (legacy orphan deltas, pending CREATE
|
|
1047
1049
|
// transactions, etc.). Noisy at warn level, useful during
|
|
1048
1050
|
// investigation.
|
|
1049
|
-
|
|
1051
|
+
this.runtime.logger.debug('[InstanceCache.getByForeignKey] ROWS DROPPED', {
|
|
1050
1052
|
modelName,
|
|
1051
1053
|
fieldName,
|
|
1052
1054
|
fieldValue,
|