@abloatai/ablo 0.25.0 → 0.26.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.
Files changed (244) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +3 -3
  4. package/dist/BaseSyncedStore.d.ts +72 -195
  5. package/dist/BaseSyncedStore.js +240 -625
  6. package/dist/Database.d.ts +8 -9
  7. package/dist/Database.js +9 -14
  8. package/dist/LazyReferenceCollection.js +5 -0
  9. package/dist/Model.js +6 -8
  10. package/dist/ModelRegistry.d.ts +25 -6
  11. package/dist/ModelRegistry.js +12 -6
  12. package/dist/NetworkMonitor.js +1 -1
  13. package/dist/ObjectPool.js +9 -3
  14. package/dist/SyncClient.d.ts +15 -5
  15. package/dist/SyncClient.js +152 -66
  16. package/dist/SyncEngineContext.d.ts +1 -3
  17. package/dist/SyncEngineContext.js +1 -2
  18. package/dist/agent/Agent.d.ts +21 -8
  19. package/dist/agent/Agent.js +26 -13
  20. package/dist/ai-sdk/coordination-context.js +9 -3
  21. package/dist/ai-sdk/wrap.d.ts +1 -1
  22. package/dist/ai-sdk/wrap.js +1 -1
  23. package/dist/auth/credentialPolicy.d.ts +10 -2
  24. package/dist/auth/credentialSource.d.ts +1 -13
  25. package/dist/auth/credentialSource.js +6 -13
  26. package/dist/auth/index.js +3 -3
  27. package/dist/cli.cjs +328 -262
  28. package/dist/client/Ablo.d.ts +35 -764
  29. package/dist/client/Ablo.js +64 -688
  30. package/dist/client/ApiClient.d.ts +14 -1
  31. package/dist/client/ApiClient.js +63 -9
  32. package/dist/client/auth.d.ts +8 -17
  33. package/dist/client/auth.js +42 -6
  34. package/dist/client/consoleLogger.d.ts +36 -0
  35. package/dist/client/consoleLogger.js +45 -0
  36. package/dist/client/createInternalComponents.js +1 -1
  37. package/dist/client/createModelProxy.js +10 -6
  38. package/dist/client/credentialEndpoint.d.ts +63 -0
  39. package/dist/client/credentialEndpoint.js +87 -0
  40. package/dist/client/hostedEndpoints.d.ts +24 -0
  41. package/dist/client/hostedEndpoints.js +24 -0
  42. package/dist/client/httpClient.d.ts +1 -1
  43. package/dist/client/identity.js +2 -1
  44. package/dist/client/modelRegistration.d.ts +14 -0
  45. package/dist/client/modelRegistration.js +321 -0
  46. package/dist/client/options.d.ts +396 -0
  47. package/dist/client/options.js +10 -0
  48. package/dist/client/resourceTypes.d.ts +344 -0
  49. package/dist/client/resourceTypes.js +13 -0
  50. package/dist/client/schemaConfig.d.ts +56 -0
  51. package/dist/client/schemaConfig.js +188 -0
  52. package/dist/client/sessionMint.d.ts +1 -1
  53. package/dist/client/validateAbloOptions.js +1 -1
  54. package/dist/client/wsMutationExecutor.d.ts +33 -0
  55. package/dist/client/wsMutationExecutor.js +77 -0
  56. package/dist/context.js +0 -3
  57. package/dist/coordination/index.d.ts +8 -1
  58. package/dist/coordination/index.js +24 -1
  59. package/dist/coordination/trace.d.ts +0 -1
  60. package/dist/coordination/trace.js +0 -1
  61. package/dist/core/DatabaseManager.d.ts +0 -1
  62. package/dist/core/DatabaseManager.js +26 -24
  63. package/dist/core/QueryProcessor.js +7 -8
  64. package/dist/core/QueryView.d.ts +14 -2
  65. package/dist/core/QueryView.js +4 -5
  66. package/dist/core/StoreManager.d.ts +2 -2
  67. package/dist/core/StoreManager.js +5 -1
  68. package/dist/core/index.d.ts +1 -1
  69. package/dist/core/openIDBWithTimeout.js +15 -11
  70. package/dist/core/query-utils.js +1 -1
  71. package/dist/core/storeContract.d.ts +143 -0
  72. package/dist/core/storeContract.js +16 -0
  73. package/dist/errorCodes.d.ts +11 -2
  74. package/dist/errorCodes.js +155 -137
  75. package/dist/errors.d.ts +18 -7
  76. package/dist/errors.js +14 -1
  77. package/dist/index.js +5 -0
  78. package/dist/interfaces/index.d.ts +6 -20
  79. package/dist/keys/index.js +18 -9
  80. package/dist/mutators/UndoManager.js +6 -1
  81. package/dist/mutators/defineMutators.d.ts +1 -3
  82. package/dist/policy/types.d.ts +1 -1
  83. package/dist/query/client.d.ts +11 -0
  84. package/dist/query/client.js +81 -52
  85. package/dist/react/AbloProvider.d.ts +6 -10
  86. package/dist/react/AbloProvider.js +22 -15
  87. package/dist/react/context.d.ts +2 -118
  88. package/dist/react/useAblo.js +1 -1
  89. package/dist/react/useErrorListener.js +1 -1
  90. package/dist/react/useMutationFailureListener.js +1 -1
  91. package/dist/react/useMutators.js +7 -1
  92. package/dist/react/useReactive.js +0 -1
  93. package/dist/react/useUndoScope.js +2 -2
  94. package/dist/schema/ddlLock.d.ts +39 -0
  95. package/dist/schema/ddlLock.js +51 -0
  96. package/dist/schema/field.js +1 -1
  97. package/dist/schema/index.d.ts +4 -3
  98. package/dist/schema/index.js +15 -5
  99. package/dist/schema/model.d.ts +3 -3
  100. package/dist/schema/model.js +2 -2
  101. package/dist/schema/openapi.js +5 -1
  102. package/dist/schema/queries.d.ts +1 -1
  103. package/dist/schema/queries.js +1 -0
  104. package/dist/schema/residency.d.ts +35 -0
  105. package/dist/schema/residency.js +27 -0
  106. package/dist/schema/roles.d.ts +25 -0
  107. package/dist/schema/roles.js +37 -0
  108. package/dist/schema/schema.d.ts +3 -3
  109. package/dist/schema/schema.js +11 -7
  110. package/dist/schema/serialize.d.ts +8 -8
  111. package/dist/schema/sync-delta-row.d.ts +2 -2
  112. package/dist/schema/sync-delta-row.js +2 -2
  113. package/dist/server/storage-mode.d.ts +8 -1
  114. package/dist/server/storage-mode.js +14 -4
  115. package/dist/source/adapter.d.ts +1 -1
  116. package/dist/source/adapters/drizzle.js +4 -3
  117. package/dist/source/adapters/kysely.js +4 -3
  118. package/dist/source/adapters/memory.js +1 -1
  119. package/dist/source/adapters/prisma.js +5 -4
  120. package/dist/source/conformance.js +10 -8
  121. package/dist/source/connector.js +4 -2
  122. package/dist/source/factory.d.ts +106 -0
  123. package/dist/source/factory.js +273 -0
  124. package/dist/source/index.d.ts +11 -461
  125. package/dist/source/index.js +14 -416
  126. package/dist/source/next.d.ts +1 -1
  127. package/dist/source/next.js +1 -1
  128. package/dist/source/pushQueue.d.ts +1 -1
  129. package/dist/source/pushQueue.js +6 -3
  130. package/dist/source/signing.d.ts +59 -0
  131. package/dist/source/signing.js +142 -0
  132. package/dist/source/types.d.ts +334 -0
  133. package/dist/source/types.js +44 -0
  134. package/dist/stores/ObjectStore.d.ts +1 -1
  135. package/dist/stores/ObjectStore.js +23 -23
  136. package/dist/stores/SyncActionStore.d.ts +1 -1
  137. package/dist/stores/SyncActionStore.js +70 -35
  138. package/dist/surface.d.ts +1 -1
  139. package/dist/surface.js +1 -0
  140. package/dist/sync/AreaOfInterestManager.d.ts +2 -2
  141. package/dist/sync/AreaOfInterestManager.js +1 -1
  142. package/dist/sync/BootstrapHelper.d.ts +1 -3
  143. package/dist/sync/BootstrapHelper.js +29 -8
  144. package/dist/sync/ConnectionManager.d.ts +8 -0
  145. package/dist/sync/ConnectionManager.js +19 -4
  146. package/dist/sync/HydrationCoordinator.d.ts +12 -0
  147. package/dist/sync/HydrationCoordinator.js +25 -5
  148. package/dist/sync/NetworkProbe.d.ts +3 -2
  149. package/dist/sync/NetworkProbe.js +7 -6
  150. package/dist/sync/SyncWebSocket.d.ts +71 -120
  151. package/dist/sync/SyncWebSocket.js +324 -630
  152. package/dist/sync/awaitClaimGrant.js +27 -19
  153. package/dist/sync/bootstrapApply.d.ts +60 -0
  154. package/dist/sync/bootstrapApply.js +65 -0
  155. package/dist/sync/commitFrames.d.ts +43 -0
  156. package/dist/sync/commitFrames.js +94 -0
  157. package/dist/sync/createClaimStream.js +3 -2
  158. package/dist/sync/createSnapshot.d.ts +1 -3
  159. package/dist/sync/credentialLifecycle.d.ts +175 -0
  160. package/dist/sync/credentialLifecycle.js +329 -0
  161. package/dist/sync/deltaPipeline.d.ts +111 -0
  162. package/dist/sync/deltaPipeline.js +262 -0
  163. package/dist/sync/groupChange.d.ts +111 -0
  164. package/dist/sync/groupChange.js +246 -0
  165. package/dist/sync/heartbeat.d.ts +62 -0
  166. package/dist/sync/heartbeat.js +91 -0
  167. package/dist/sync/participants.d.ts +8 -8
  168. package/dist/sync/syncCursor.d.ts +44 -0
  169. package/dist/sync/syncCursor.js +59 -0
  170. package/dist/sync/syncPlan.d.ts +62 -0
  171. package/dist/sync/syncPlan.js +51 -0
  172. package/dist/sync/wsFrameHandlers.d.ts +117 -0
  173. package/dist/sync/wsFrameHandlers.js +378 -0
  174. package/dist/testing/fixtures/bootstrap.d.ts +9 -11
  175. package/dist/testing/fixtures/deltas.d.ts +1 -1
  176. package/dist/testing/fixtures/models.js +1 -0
  177. package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
  178. package/dist/testing/helpers/sync-engine-harness.js +2 -1
  179. package/dist/testing/index.d.ts +1 -1
  180. package/dist/testing/index.js +1 -1
  181. package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
  182. package/dist/testing/mocks/MockSyncContext.js +1 -32
  183. package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
  184. package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
  185. package/dist/transactions/TransactionQueue.d.ts +20 -56
  186. package/dist/transactions/TransactionQueue.js +101 -487
  187. package/dist/transactions/TransactionStore.d.ts +18 -0
  188. package/dist/transactions/TransactionStore.js +51 -0
  189. package/dist/transactions/coalesceRules.d.ts +34 -0
  190. package/dist/transactions/coalesceRules.js +117 -0
  191. package/dist/transactions/commitPayload.d.ts +134 -0
  192. package/dist/transactions/commitPayload.js +152 -0
  193. package/dist/transactions/deltaConfirmation.d.ts +60 -0
  194. package/dist/transactions/deltaConfirmation.js +223 -0
  195. package/dist/transactions/optimistic.d.ts +24 -0
  196. package/dist/transactions/optimistic.js +45 -0
  197. package/dist/transactions/persistedReplay.d.ts +93 -0
  198. package/dist/transactions/persistedReplay.js +105 -0
  199. package/dist/types/index.d.ts +3 -3
  200. package/dist/types/index.js +0 -5
  201. package/dist/types/modelData.d.ts +12 -0
  202. package/dist/types/modelData.js +11 -0
  203. package/dist/types/participant.d.ts +21 -0
  204. package/dist/types/participant.js +12 -0
  205. package/dist/types/streams.d.ts +8 -14
  206. package/dist/utils/mobx-setup.d.ts +1 -1
  207. package/dist/utils/mobx-setup.js +2 -2
  208. package/dist/wire/errorEnvelope.d.ts +11 -2
  209. package/dist/wire/errorEnvelope.js +14 -2
  210. package/dist/wire/frames.d.ts +90 -0
  211. package/dist/wire/frames.js +55 -1
  212. package/dist/wire/index.d.ts +4 -1
  213. package/dist/wire/index.js +20 -1
  214. package/dist/wire/protocol.d.ts +45 -0
  215. package/dist/wire/protocol.js +45 -0
  216. package/dist/wire/protocolVersion.d.ts +56 -0
  217. package/dist/wire/protocolVersion.js +63 -0
  218. package/docs/api-keys.md +4 -3
  219. package/docs/examples/existing-python-backend.md +3 -3
  220. package/docs/identity.md +4 -4
  221. package/docs/integration-guide.md +1 -1
  222. package/docs/react.md +1 -1
  223. package/docs/sessions.md +5 -7
  224. package/package.json +15 -13
  225. package/dist/client/index.d.ts +0 -36
  226. package/dist/client/index.js +0 -33
  227. package/dist/config/index.d.ts +0 -10
  228. package/dist/config/index.js +0 -12
  229. package/dist/interfaces/headless.d.ts +0 -95
  230. package/dist/interfaces/headless.js +0 -41
  231. package/dist/query/index.d.ts +0 -6
  232. package/dist/query/index.js +0 -5
  233. package/dist/realtime/index.d.ts +0 -10
  234. package/dist/realtime/index.js +0 -9
  235. package/dist/schema/plane.d.ts +0 -23
  236. package/dist/schema/plane.js +0 -19
  237. package/dist/server/next.d.ts +0 -51
  238. package/dist/server/next.js +0 -47
  239. package/dist/sync/OfflineFlush.d.ts +0 -9
  240. package/dist/sync/OfflineFlush.js +0 -22
  241. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  242. package/dist/sync/OfflineTransactionStore.js +0 -263
  243. package/dist/transactions/index.d.ts +0 -16
  244. package/dist/transactions/index.js +0 -7
@@ -77,12 +77,12 @@ export interface BootstrapResult {
77
77
  * was disconnected — without this, DELETE deltas persist to IDB but
78
78
  * ghost entities linger in the pool until a full reload.
79
79
  */
80
- deltaResults?: Array<{
80
+ deltaResults?: {
81
81
  action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
82
82
  modelName: string;
83
83
  modelId: string;
84
84
  data?: ModelData | null;
85
- }>;
85
+ }[];
86
86
  }
87
87
  export declare class Database {
88
88
  private databaseManager;
@@ -236,7 +236,7 @@ export declare class Database {
236
236
  * - Skips stale updates for entities that will be/were deleted
237
237
  * - Prevents 404 errors from fetching already-deleted entities
238
238
  */
239
- processDeltaBatch(deltas: Array<{
239
+ processDeltaBatch(deltas: {
240
240
  syncId?: number;
241
241
  /**
242
242
  * Includes 'G' and 'S' defensively — they're routed upstream and
@@ -257,14 +257,14 @@ export declare class Database {
257
257
  * have a client transaction.
258
258
  */
259
259
  transactionId?: string;
260
- }>): Promise<{
261
- results: Array<{
260
+ }[]): Promise<{
261
+ results: {
262
262
  action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
263
263
  modelName: string;
264
264
  modelId: string;
265
265
  data?: ModelData | null;
266
266
  transactionId?: string;
267
- }>;
267
+ }[];
268
268
  /**
269
269
  * Highest syncId whose IDB store transaction actually committed in this
270
270
  * batch. The runtime delta cursor (WS `lastSyncId`, server-side
@@ -292,7 +292,6 @@ export declare class Database {
292
292
  /** Read workspace metadata from IDB (returns null if db not open). */
293
293
  getWorkspaceMetadata(): Promise<WorkspaceMetadata | null>;
294
294
  getLastSyncId(): Promise<number>;
295
- getVersionVector(): Promise<Record<string, number> | null>;
296
295
  updateWorkspaceMetadata(metadata: Partial<WorkspaceMetadata>): Promise<void>;
297
296
  /** Transaction persistence for offline/retry support.
298
297
  * Returns either the IDB-backed ObjectStore or its in-memory twin
@@ -332,13 +331,13 @@ export declare class Database {
332
331
  notReady: number;
333
332
  };
334
333
  totalRecords: number;
335
- storeDetails: Array<{
334
+ storeDetails: {
336
335
  modelName: string;
337
336
  storeName: string;
338
337
  strategy: LoadStrategy;
339
338
  ready: boolean;
340
339
  count: number;
341
- }>;
340
+ }[];
342
341
  };
343
342
  metadata: WorkspaceMetadata | null;
344
343
  }>;
package/dist/Database.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { DatabaseManager } from './core/DatabaseManager.js';
6
6
  import { StoreManager } from './core/StoreManager.js';
7
+ import { ModelRegistry } from './ModelRegistry.js';
7
8
  import { LoadStrategy } from './types/index.js';
8
9
  import { getContext } from './context.js';
9
10
  import { AbloConnectionError, AbloValidationError } from './errors.js';
@@ -105,7 +106,7 @@ export class Database {
105
106
  logPreservedFields(modelName, modelId, existing, delta) {
106
107
  if (modelName === 'Activity')
107
108
  return;
108
- const requiredFields = this.essentialFields[modelName] || [];
109
+ const requiredFields = this.essentialFields[modelName] ?? [];
109
110
  const preserved = requiredFields.filter((field) => existing[field] !== undefined && delta[field] === undefined);
110
111
  if (preserved.length > 0) {
111
112
  getContext().logger.debug('[Database] UPDATE merged - preserved fields', {
@@ -324,7 +325,7 @@ export class Database {
324
325
  type,
325
326
  modelsToLoad,
326
327
  lastSyncId,
327
- syncGroups: metadata?.syncGroups || [],
328
+ syncGroups: metadata?.syncGroups ?? [],
328
329
  };
329
330
  }
330
331
  /**
@@ -360,7 +361,7 @@ export class Database {
360
361
  lastSyncId: bootstrapData.lastSyncId,
361
362
  hasModels: !!bootstrapData.models,
362
363
  hasDeltas: !!bootstrapData.deltas,
363
- deltaCount: bootstrapData.deltaCount || 0,
364
+ deltaCount: bootstrapData.deltaCount ?? 0,
364
365
  });
365
366
  // ✅ Only clear AFTER successful fetch (transactional safety)
366
367
  // IMPORTANT: Clear if the SERVER says it's a full snapshot, regardless of what we asked.
@@ -369,7 +370,7 @@ export class Database {
369
370
  }
370
371
  // Handle partial bootstrap (delta batch)
371
372
  if (bootstrapData.type === 'partial') {
372
- const deltas = bootstrapData.deltas || [];
373
+ const deltas = bootstrapData.deltas ?? [];
373
374
  getContext().logger.info('Processing partial bootstrap with delta batch', {
374
375
  deltaCount: deltas.length,
375
376
  fromSyncId: requirements.lastSyncId,
@@ -783,7 +784,7 @@ export class Database {
783
784
  const deleteSyncId = deleteSyncIds.get(key);
784
785
  if (deleteSyncId !== undefined) {
785
786
  // DELETE exists for this entity
786
- const deltaSyncId = delta.syncId || 0;
787
+ const deltaSyncId = delta.syncId ?? 0;
787
788
  if (deleteSyncId >= deltaSyncId) {
788
789
  // DELETE has equal or higher syncId - skip this UPDATE/INSERT
789
790
  getContext().logger.debug('[Database.processDeltaBatch] Skipping stale delta (DELETE wins)', {
@@ -989,8 +990,8 @@ export class Database {
989
990
  }
990
991
  // Wait for transaction to complete
991
992
  await new Promise((resolve, reject) => {
992
- tx.oncomplete = () => resolve();
993
- tx.onerror = () => reject(tx.error);
993
+ tx.oncomplete = () => { resolve(); };
994
+ tx.onerror = () => { reject(tx.error); };
994
995
  });
995
996
  // Only commit staged results to the global results if the transaction succeeded.
996
997
  // Also advance `highestPersistedSyncId` ONLY for deltas in this successful tx
@@ -1122,13 +1123,7 @@ export class Database {
1122
1123
  return 0;
1123
1124
  }
1124
1125
  const metadata = await this.databaseManager.getWorkspaceMetadata(this.workspaceDb);
1125
- return metadata?.lastSyncId || 0;
1126
- }
1127
- async getVersionVector() {
1128
- if (!this.workspaceDb)
1129
- return null;
1130
- const metadata = await this.databaseManager.getWorkspaceMetadata(this.workspaceDb);
1131
- return (metadata?.versions || null);
1126
+ return metadata?.lastSyncId ?? 0;
1132
1127
  }
1133
1128
  async updateWorkspaceMetadata(metadata) {
1134
1129
  // In-memory mode: store in local variable
@@ -5,6 +5,8 @@
5
5
  * data on-demand with intelligent caching and batching.
6
6
  */
7
7
  import { makeObservable, observable, action, computed, onBecomeObserved, onBecomeUnobserved, } from 'mobx';
8
+ import { Database } from './Database.js';
9
+ import { ObjectPool } from './ObjectPool.js';
8
10
  import { getActiveRegistry } from './ModelRegistry.js';
9
11
  import { AbloValidationError } from './errors.js';
10
12
  /**
@@ -289,6 +291,9 @@ export class LazyReferenceCollection {
289
291
  this.objectPool.add(model);
290
292
  }
291
293
  if (model) {
294
+ // The by-name registry lookup types instances as the base
295
+ // `Model`; this collection's `modelName` IS `T`'s registered
296
+ // name (fixed at construction), so the instance is a `T`.
292
297
  models.push(model);
293
298
  }
294
299
  }
package/dist/Model.js CHANGED
@@ -70,7 +70,7 @@ export class Model {
70
70
  _observedCollections = new Set();
71
71
  constructor(data = {}) {
72
72
  // Always generate permanent UUID on client
73
- this.id = data.id || Model.generateId();
73
+ this.id = data.id ?? Model.generateId();
74
74
  this.clientId = this.id; // No more temp IDs!
75
75
  // Ensure dates are Date objects, not strings
76
76
  this.createdAt = data.createdAt
@@ -91,7 +91,7 @@ export class Model {
91
91
  : data.createdAt
92
92
  ? new Date(this.createdAt)
93
93
  : new Date();
94
- this.syncStatus = data.syncStatus || 'pending';
94
+ this.syncStatus = data.syncStatus ?? 'pending';
95
95
  }
96
96
  /**
97
97
  * Generate unique ID
@@ -340,9 +340,7 @@ export class Model {
340
340
  * Add validation rule
341
341
  */
342
342
  addValidationRule(propName, rule) {
343
- if (!this.validationRules[propName]) {
344
- this.validationRules[propName] = [];
345
- }
343
+ this.validationRules[propName] ??= [];
346
344
  this.validationRules[propName].push(rule);
347
345
  }
348
346
  /**
@@ -575,7 +573,7 @@ export class Model {
575
573
  syncStatus: this.syncStatus,
576
574
  };
577
575
  if (this.archivedAt !== undefined) {
578
- result.archivedAt = this.archivedAt?.toISOString() || null;
576
+ result.archivedAt = this.archivedAt?.toISOString() ?? null;
579
577
  }
580
578
  if (properties) {
581
579
  const self = this;
@@ -616,7 +614,7 @@ export class Model {
616
614
  const className = this.constructor.name;
617
615
  // Use consumer-provided fallback map from config (replaces hardcoded Prisma name map)
618
616
  const fallbackMap = getContext().config.classNameFallbackMap;
619
- return fallbackMap[className] || className.replace(/Model$/, '');
617
+ return fallbackMap[className] ?? className.replace(/Model$/, '');
620
618
  }
621
619
  /**
622
620
  * Read a field value by name. Runtime-safe dynamic field access —
@@ -809,7 +807,7 @@ export class Model {
809
807
  */
810
808
  static fromJSON(data) {
811
809
  // Support both __class and __typename, and handle both old and new naming
812
- const modelIdentifier = data.__typename || data.__class || data.modelName;
810
+ const modelIdentifier = data.__typename ?? data.__class ?? data.modelName;
813
811
  if (!modelIdentifier) {
814
812
  throw new AbloValidationError('Model identifier (__typename, __class, or modelName) not found in data', { code: 'model_identifier_missing' });
815
813
  }
@@ -9,7 +9,26 @@
9
9
  * - Comprehensive error reporting
10
10
  * - Best practices from Linear Sync Engine
11
11
  */
12
- import { ModelMetadata, PropertyMetadata, ReferenceMetadata, LoadStrategy } from './types/index.js';
12
+ import { type ModelMetadata, type PropertyMetadata, type ReferenceMetadata, LoadStrategy } from './types/index.js';
13
+ import type { Model } from './Model.js';
14
+ import type { ConcreteModelConstructor } from './BaseSyncedStore.js';
15
+ /**
16
+ * What callers may hand to {@link ModelRegistry.registerModel}: any concrete
17
+ * `Model` subclass constructor. `never[]` params make every subclass
18
+ * constructor assignable (construct-signature params are contravariant).
19
+ */
20
+ export type ModelClassInput = new (...args: never[]) => Model;
21
+ /**
22
+ * What the registry hands BACK: a registered model class — concretely
23
+ * constructible with an optional data row (`ConcreteModelConstructor`, the
24
+ * SDK's existing Model-vs-row construction seam) and carrying `Model`'s
25
+ * statics (`fromJSON`, …). `Omit<typeof Model, never>` keeps the statics
26
+ * while stripping the ABSTRACT construct signature (mapped types drop
27
+ * construct signatures), so `new registry.getModelByName(n)!(...)` is legal.
28
+ * The one cast from {@link ModelClassInput} lives at the validated
29
+ * registration boundary below.
30
+ */
31
+ export type RegisteredModelClass = Omit<typeof Model, never> & ConcreteModelConstructor<Model>;
13
32
  /**
14
33
  * Extended ReferenceMetadata with additional Linear-style options
15
34
  */
@@ -73,7 +92,7 @@ export declare class ModelRegistry {
73
92
  /**
74
93
  * Register a model with validation
75
94
  */
76
- registerModel(name: string, constructor: any, metadata?: ModelMetadata): void;
95
+ registerModel(name: string, constructor: ModelClassInput, metadata?: ModelMetadata): void;
77
96
  /**
78
97
  * Register property with validation
79
98
  */
@@ -95,7 +114,7 @@ export declare class ModelRegistry {
95
114
  /** Get all models with specific load strategy. */
96
115
  getModelsByLoadStrategy(strategy: LoadStrategy): string[];
97
116
  /** Get model name from constructor (production-safe). */
98
- getModelNameFromConstructor(constructor: any): string | undefined;
117
+ getModelNameFromConstructor(constructor: unknown): string | undefined;
99
118
  /** Get properties for a model. */
100
119
  getPropertiesForModel(modelName: string): Map<string, PropertyMetadata>;
101
120
  /**
@@ -103,7 +122,7 @@ export declare class ModelRegistry {
103
122
  */
104
123
  getRegisteredModelNames(): string[];
105
124
  /** Get model constructor by name */
106
- getModelByName(name: string): any;
125
+ getModelByName(name: string): RegisteredModelClass | undefined;
107
126
  /** Check if model is registered */
108
127
  hasModel(name: string): boolean;
109
128
  /** Get model metadata by name */
@@ -117,10 +136,10 @@ export declare class ModelRegistry {
117
136
  /** Get back-references for a child model */
118
137
  getBackReferences(childModelName: string): BackReferenceMetadata[];
119
138
  /** Get child models for a parent */
120
- getChildModels(parentModelName: string): Array<{
139
+ getChildModels(parentModelName: string): {
121
140
  childModel: string;
122
141
  foreignKey: string;
123
- }>;
142
+ }[];
124
143
  /**
125
144
  * Calculate schema hash using crypto
126
145
  */
@@ -43,7 +43,9 @@ export class ModelRegistry {
43
43
  properties = new Map();
44
44
  references = new Map();
45
45
  pendingReferences = new Map();
46
- // 🔧 PROPER FIX: Static mapping from constructor to model name
46
+ // 🔧 PROPER FIX: Static mapping from constructor to model name.
47
+ // Keyed `unknown` on purpose: lookups arrive as `this.constructor`
48
+ // (Function) and as typed model classes — both accepted without casts.
47
49
  constructorToModelName = new Map();
48
50
  // LINEAR PATTERN: BackReferences for cascade-aware transaction handling.
49
51
  // Maps childModelName → BackReferenceMetadata[] (which parent models
@@ -65,13 +67,14 @@ export class ModelRegistry {
65
67
  if (typeof constructor !== 'function') {
66
68
  throw new AbloValidationError(`Model ${name} constructor must be a function`, { code: 'registry_invalid_constructor' });
67
69
  }
68
- if (!constructor.prototype) {
70
+ const prototype = constructor.prototype;
71
+ if (!prototype || typeof prototype !== 'object') {
69
72
  throw new AbloValidationError(`Model ${name} constructor must have a prototype`, { code: 'registry_invalid_constructor' });
70
73
  }
71
74
  // Check for required methods
72
75
  const required = ['updateFromData', 'toJSON', 'getModelName'];
73
76
  for (const method of required) {
74
- if (typeof constructor.prototype[method] !== 'function') {
77
+ if (typeof prototype[method] !== 'function') {
75
78
  getContext().logger.debug('Model missing required method', name, { method });
76
79
  }
77
80
  }
@@ -159,8 +162,12 @@ export class ModelRegistry {
159
162
  return;
160
163
  }
161
164
  getContext().logger.debug('Registering model', name);
162
- // Register
163
- this.models.set(name, constructor);
165
+ // Register. The one cast in this file: input is any Model-subclass
166
+ // constructor (validated above); registered classes are concrete
167
+ // subclasses constructible with `data?` and carrying Model's statics —
168
+ // the shape `RegisteredModelClass` names for every read site.
169
+ const modelClass = constructor;
170
+ this.models.set(name, modelClass);
164
171
  this.modelMetadata.set(name, metadata);
165
172
  // 🔧 PROPER FIX: Create reverse mapping from constructor to model name
166
173
  this.constructorToModelName.set(constructor, name);
@@ -291,7 +298,6 @@ export class ModelRegistry {
291
298
  return Array.from(this.models.keys());
292
299
  }
293
300
  /** Get model constructor by name */
294
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
295
301
  getModelByName(name) {
296
302
  return this.models.get(name);
297
303
  }
@@ -18,7 +18,7 @@ export class NetworkMonitor extends EventEmitter {
18
18
  super();
19
19
  this.setupListeners();
20
20
  }
21
- handleOnline = async () => {
21
+ handleOnline = () => {
22
22
  const wasOffline = !this.isOnline;
23
23
  this.isOnline = true;
24
24
  this.lastOnlineCheck = new Date();
@@ -4,7 +4,9 @@
4
4
  * Pure memory management without database or registry dependencies.
5
5
  * Uses static ModelRegistry for model class lookup only.
6
6
  */
7
- import { makeObservable, observable, action, computed, runInAction } from 'mobx';
7
+ import { makeObservable, observable, action, computed, runInAction, set } from 'mobx';
8
+ import { Model } from './Model.js';
9
+ import { ModelRegistry } from './ModelRegistry.js';
8
10
  import { getContext } from './context.js';
9
11
  import { AbloValidationError } from './errors.js';
10
12
  import { ModelScope } from './types/index.js';
@@ -203,7 +205,7 @@ export class ObjectPool {
203
205
  }
204
206
  // Check if model already exists to prevent duplicates
205
207
  const existingEntry = this.entries.get(id);
206
- if (existingEntry && existingEntry.model && !existingEntry.model.disposed) {
208
+ if (existingEntry?.model && !existingEntry.model.disposed) {
207
209
  // Model already exists and is valid, update its scope if needed
208
210
  if (existingEntry.scope !== scope) {
209
211
  runInAction(() => {
@@ -320,7 +322,7 @@ export class ObjectPool {
320
322
  }
321
323
  // Skip if model already exists and is valid
322
324
  const existingEntry = this.entries.get(id);
323
- if (existingEntry && existingEntry.model && !existingEntry.model.disposed) {
325
+ if (existingEntry?.model && !existingEntry.model.disposed) {
324
326
  if (existingEntry.scope !== scope) {
325
327
  this.entries.set(id, { ...existingEntry, scope });
326
328
  this.accessTimes.set(id, now);
@@ -881,6 +883,10 @@ export class ObjectPool {
881
883
  if (this.gcTimer)
882
884
  return;
883
885
  this.gcTimer = setInterval(() => this.gc(), this.config.gcInterval);
886
+ // Don't hold a headless Node process open just for pool GC — without
887
+ // this, an agent that never calls disconnect() can never exit. No-op in
888
+ // browsers (where setInterval returns a number without `unref`).
889
+ this.gcTimer.unref?.();
884
890
  }
885
891
  stopGC() {
886
892
  if (this.gcTimer) {
@@ -117,6 +117,10 @@ export declare class SyncClient extends EventEmitter {
117
117
  * will deliver the missing deltas and naturally confirm the transaction.
118
118
  */
119
119
  private setupAwaitingTransactionPersistence;
120
+ /** Persist an unconfirmed transaction to IDB (never rejects — failures are captured). */
121
+ private persistAwaitingTransaction;
122
+ /** Drop the persisted awaiting-row once confirmed (never rejects). */
123
+ private removeAwaitingTransaction;
120
124
  /**
121
125
  * Initialize sync client with authentication
122
126
  */
@@ -254,7 +258,12 @@ export declare class SyncClient extends EventEmitter {
254
258
  */
255
259
  private persistMutationQueue;
256
260
  /**
257
- * Restore mutation queue from IndexedDB
261
+ * Restore mutation queue from IndexedDB.
262
+ *
263
+ * The persisted record was written by a PREVIOUS session (possibly an older
264
+ * SDK build), so each entry is validated at this replay boundary (T1.8):
265
+ * corrupt entries are dropped + logged at debug, and a failure never
266
+ * vanishes into an empty catch — offline write survival must be observable.
258
267
  */
259
268
  private restoreMutationQueue;
260
269
  /**
@@ -278,10 +287,11 @@ export declare class SyncClient extends EventEmitter {
278
287
  * CRITICAL: Always respects certain server states (deletes, deactivations)
279
288
  * even when there are local changes, to maintain data consistency.
280
289
  */
281
- resolveConflicts(localModel: Model, serverData: any): Model;
290
+ resolveConflicts(localModel: Model, serverData: Record<string, unknown>): Model;
282
291
  /**
283
292
  * Extract critical state fields from server data
284
- * These are states that must always be respected, even with local changes
293
+ * These are states that must always be respected, even with local changes.
294
+ * The conflict brain reads exactly these known fields — nothing else.
285
295
  */
286
296
  private extractCriticalState;
287
297
  /**
@@ -482,7 +492,7 @@ export declare class SyncClient extends EventEmitter {
482
492
  * consumers — `Ablo` itself is the public surface.
483
493
  */
484
494
  getTransactionQueue(): TransactionQueue;
485
- applyDeltaBatchToPool(dbResults: Array<{
495
+ applyDeltaBatchToPool(dbResults: {
486
496
  action: string;
487
497
  modelName: string;
488
498
  modelId: string;
@@ -495,7 +505,7 @@ export declare class SyncClient extends EventEmitter {
495
505
  * schema-derived deltas, etc.) don't have a client transaction.
496
506
  */
497
507
  transactionId?: string;
498
- }>, enrichRelations: (modelName: string, data: Record<string, unknown>) => Record<string, unknown>): void;
508
+ }[], enrichRelations: (modelName: string, data: Record<string, unknown>) => Record<string, unknown>): void;
499
509
  /**
500
510
  * Apply bootstrap data to the ObjectPool with ghost removal.
501
511
  * Owns: model creation, batch upsert, ghost detection + removal.