@abloatai/ablo 0.3.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 +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Sync Engine - Lazy Reference Collection
|
|
3
|
+
*
|
|
4
|
+
* Efficient implementation of one-to-many relationships that loads
|
|
5
|
+
* data on-demand with intelligent caching and batching.
|
|
6
|
+
*/
|
|
7
|
+
import type { Model } from './Model.js';
|
|
8
|
+
import { Database } from './Database.js';
|
|
9
|
+
import { ObjectPool } from './ObjectPool.js';
|
|
10
|
+
/**
|
|
11
|
+
* Options for LazyReferenceCollection behavior
|
|
12
|
+
*/
|
|
13
|
+
export interface LazyCollectionOptions {
|
|
14
|
+
/** Skip network hydration if local data exists */
|
|
15
|
+
canSkipNetworkHydration?: () => boolean;
|
|
16
|
+
/** Custom filter for loaded items */
|
|
17
|
+
filter?: (item: any) => boolean;
|
|
18
|
+
/** Custom sort function */
|
|
19
|
+
sort?: (a: any, b: any) => number;
|
|
20
|
+
/** Maximum items to load */
|
|
21
|
+
limit?: number;
|
|
22
|
+
/** Enable automatic refresh on parent changes */
|
|
23
|
+
autoRefresh?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* LazyReferenceCollection - Lazy-loaded one-to-many relationships
|
|
27
|
+
*
|
|
28
|
+
* Key features:
|
|
29
|
+
* - Loads from IndexedDB first, then network if needed
|
|
30
|
+
* - Automatic batching to prevent N+1 queries
|
|
31
|
+
* - Observable for React integration
|
|
32
|
+
* - Memory efficient with intelligent caching
|
|
33
|
+
* - Support for filtering and sorting
|
|
34
|
+
*/
|
|
35
|
+
export declare class LazyReferenceCollection<T extends Model> {
|
|
36
|
+
private modelName;
|
|
37
|
+
private parent;
|
|
38
|
+
private foreignKey;
|
|
39
|
+
private customQuery?;
|
|
40
|
+
private options;
|
|
41
|
+
/** Static dependencies - shared across all instances */
|
|
42
|
+
private static _database;
|
|
43
|
+
private static _objectPool;
|
|
44
|
+
/**
|
|
45
|
+
* Set global dependencies for all LazyReferenceCollection instances
|
|
46
|
+
* Called once during SyncedStore initialization
|
|
47
|
+
*/
|
|
48
|
+
static setDependencies(database: Database, objectPool: ObjectPool): void;
|
|
49
|
+
/**
|
|
50
|
+
* Clear dependencies (e.g., on logout/store disposal)
|
|
51
|
+
*/
|
|
52
|
+
static clearDependencies(): void;
|
|
53
|
+
/** Loaded items (null = not loaded, [] = loaded but empty) */
|
|
54
|
+
items: T[] | null;
|
|
55
|
+
/** Loading state */
|
|
56
|
+
isLoading: boolean;
|
|
57
|
+
/** Error state */
|
|
58
|
+
loadError: Error | null;
|
|
59
|
+
/**
|
|
60
|
+
* MobX observation tracking - prevents GC while React is observing this collection
|
|
61
|
+
* Following MobX best practice: https://mobx.js.org/lazy-observables.html
|
|
62
|
+
*/
|
|
63
|
+
_isBeingObserved: boolean;
|
|
64
|
+
/** Promise for ongoing hydration */
|
|
65
|
+
private hydrationPromise;
|
|
66
|
+
/** Disposer for observation lifecycle hooks */
|
|
67
|
+
private observationDisposer;
|
|
68
|
+
/** Get database from static dependencies */
|
|
69
|
+
private get database();
|
|
70
|
+
/** Get objectPool from static dependencies */
|
|
71
|
+
private get objectPool();
|
|
72
|
+
constructor(modelName: string, parent: Model, foreignKey: string, customQuery?: any | undefined, options?: LazyCollectionOptions);
|
|
73
|
+
/**
|
|
74
|
+
* Set up MobX observation lifecycle hooks
|
|
75
|
+
* When React components observe this collection, we prevent GC of the parent model
|
|
76
|
+
*/
|
|
77
|
+
private _setupObservationTracking;
|
|
78
|
+
/**
|
|
79
|
+
* Check if this collection is currently being observed by React/MobX
|
|
80
|
+
*/
|
|
81
|
+
get isBeingObserved(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Get the collection value (triggers hydration if needed).
|
|
84
|
+
*
|
|
85
|
+
* Filters out items whose id is no longer in the ObjectPool. The
|
|
86
|
+
* local `items` array isn't auto-synced with `pool.remove()` — a
|
|
87
|
+
* deleted entity would linger here until hydrate() re-runs on
|
|
88
|
+
* reload. Reading `pool.has(item.id)` inside this computed getter
|
|
89
|
+
* makes MobX track both `this.items` AND the pool's entries map,
|
|
90
|
+
* so any pool.remove invalidates the computed and re-renders the
|
|
91
|
+
* consumer with the deleted item gone.
|
|
92
|
+
*
|
|
93
|
+
* Without this, deleting a slide layer would pool.remove() cleanly
|
|
94
|
+
* but the canvas — which reads `slide.layers.value` — would keep
|
|
95
|
+
* showing the deleted layer until a full reload rebuilt the
|
|
96
|
+
* collection.
|
|
97
|
+
*/
|
|
98
|
+
get value(): T[];
|
|
99
|
+
/**
|
|
100
|
+
* Check if collection has been loaded
|
|
101
|
+
*/
|
|
102
|
+
get loaded(): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Check if collection is empty (only meaningful after loading)
|
|
105
|
+
*/
|
|
106
|
+
get empty(): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Check if currently loading
|
|
109
|
+
*/
|
|
110
|
+
get loading(): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Get load error if any
|
|
113
|
+
*/
|
|
114
|
+
get error(): Error | null;
|
|
115
|
+
/**
|
|
116
|
+
* Get collection size
|
|
117
|
+
*/
|
|
118
|
+
get size(): number;
|
|
119
|
+
/**
|
|
120
|
+
* Hydrate the collection from local storage and/or network
|
|
121
|
+
*/
|
|
122
|
+
hydrate(): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Internal hydration implementation
|
|
125
|
+
*/
|
|
126
|
+
private _performHydration;
|
|
127
|
+
/**
|
|
128
|
+
* Load items from IndexedDB
|
|
129
|
+
*/
|
|
130
|
+
private _loadFromLocal;
|
|
131
|
+
/**
|
|
132
|
+
* Refresh the collection (reload from network)
|
|
133
|
+
*/
|
|
134
|
+
refresh(): Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* Add an item to the collection
|
|
137
|
+
*/
|
|
138
|
+
add(item: T): void;
|
|
139
|
+
/**
|
|
140
|
+
* Remove an item from the collection
|
|
141
|
+
*/
|
|
142
|
+
remove(itemOrId: T | string): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Find an item in the collection
|
|
145
|
+
*/
|
|
146
|
+
find(predicate: (item: T) => boolean): T | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* Filter items in the collection
|
|
149
|
+
*/
|
|
150
|
+
filter(predicate: (item: T) => boolean): T[];
|
|
151
|
+
/**
|
|
152
|
+
* Check if collection contains an item
|
|
153
|
+
*/
|
|
154
|
+
contains(itemOrId: T | string): boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Convert to array (triggers hydration)
|
|
157
|
+
*/
|
|
158
|
+
toArray(): T[];
|
|
159
|
+
/**
|
|
160
|
+
* Set items directly (internal use)
|
|
161
|
+
*/
|
|
162
|
+
private _setItems;
|
|
163
|
+
/**
|
|
164
|
+
* Set loading state (internal use)
|
|
165
|
+
*/
|
|
166
|
+
private _setLoading;
|
|
167
|
+
/**
|
|
168
|
+
* Set error state (internal use)
|
|
169
|
+
*/
|
|
170
|
+
private _setError;
|
|
171
|
+
/**
|
|
172
|
+
* Clear the collection
|
|
173
|
+
*/
|
|
174
|
+
clear(): void;
|
|
175
|
+
/**
|
|
176
|
+
* Dispose of the collection (cleanup)
|
|
177
|
+
* Following MobX best practice: always clean up observation hooks
|
|
178
|
+
* See: https://github.com/mobxjs/mobx/issues/2047
|
|
179
|
+
*/
|
|
180
|
+
dispose(): void;
|
|
181
|
+
}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Sync Engine - Lazy Reference Collection
|
|
3
|
+
*
|
|
4
|
+
* Efficient implementation of one-to-many relationships that loads
|
|
5
|
+
* data on-demand with intelligent caching and batching.
|
|
6
|
+
*/
|
|
7
|
+
import { makeObservable, observable, action, computed, onBecomeObserved, onBecomeUnobserved, } from 'mobx';
|
|
8
|
+
import { getActiveRegistry } from './ModelRegistry.js';
|
|
9
|
+
import { AbloValidationError } from './errors.js';
|
|
10
|
+
/**
|
|
11
|
+
* LazyReferenceCollection - Lazy-loaded one-to-many relationships
|
|
12
|
+
*
|
|
13
|
+
* Key features:
|
|
14
|
+
* - Loads from IndexedDB first, then network if needed
|
|
15
|
+
* - Automatic batching to prevent N+1 queries
|
|
16
|
+
* - Observable for React integration
|
|
17
|
+
* - Memory efficient with intelligent caching
|
|
18
|
+
* - Support for filtering and sorting
|
|
19
|
+
*/
|
|
20
|
+
export class LazyReferenceCollection {
|
|
21
|
+
modelName;
|
|
22
|
+
parent;
|
|
23
|
+
foreignKey;
|
|
24
|
+
customQuery;
|
|
25
|
+
options;
|
|
26
|
+
/** Static dependencies - shared across all instances */
|
|
27
|
+
static _database = null;
|
|
28
|
+
static _objectPool = null;
|
|
29
|
+
/**
|
|
30
|
+
* Set global dependencies for all LazyReferenceCollection instances
|
|
31
|
+
* Called once during SyncedStore initialization
|
|
32
|
+
*/
|
|
33
|
+
static setDependencies(database, objectPool) {
|
|
34
|
+
LazyReferenceCollection._database = database;
|
|
35
|
+
LazyReferenceCollection._objectPool = objectPool;
|
|
36
|
+
getContext().logger.debug('LazyReferenceCollection dependencies set');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Clear dependencies (e.g., on logout/store disposal)
|
|
40
|
+
*/
|
|
41
|
+
static clearDependencies() {
|
|
42
|
+
LazyReferenceCollection._database = null;
|
|
43
|
+
LazyReferenceCollection._objectPool = null;
|
|
44
|
+
}
|
|
45
|
+
/** Loaded items (null = not loaded, [] = loaded but empty) */
|
|
46
|
+
items = null;
|
|
47
|
+
/** Loading state */
|
|
48
|
+
isLoading = false;
|
|
49
|
+
/** Error state */
|
|
50
|
+
loadError = null;
|
|
51
|
+
/**
|
|
52
|
+
* MobX observation tracking - prevents GC while React is observing this collection
|
|
53
|
+
* Following MobX best practice: https://mobx.js.org/lazy-observables.html
|
|
54
|
+
*/
|
|
55
|
+
_isBeingObserved = false;
|
|
56
|
+
/** Promise for ongoing hydration */
|
|
57
|
+
hydrationPromise = null;
|
|
58
|
+
/** Disposer for observation lifecycle hooks */
|
|
59
|
+
observationDisposer = null;
|
|
60
|
+
/** Get database from static dependencies */
|
|
61
|
+
get database() {
|
|
62
|
+
return LazyReferenceCollection._database;
|
|
63
|
+
}
|
|
64
|
+
/** Get objectPool from static dependencies */
|
|
65
|
+
get objectPool() {
|
|
66
|
+
return LazyReferenceCollection._objectPool;
|
|
67
|
+
}
|
|
68
|
+
constructor(modelName, parent, foreignKey, customQuery, options = {}) {
|
|
69
|
+
this.modelName = modelName;
|
|
70
|
+
this.parent = parent;
|
|
71
|
+
this.foreignKey = foreignKey;
|
|
72
|
+
this.customQuery = customQuery;
|
|
73
|
+
this.options = options;
|
|
74
|
+
makeObservable(this, {
|
|
75
|
+
items: observable,
|
|
76
|
+
isLoading: observable,
|
|
77
|
+
loadError: observable,
|
|
78
|
+
_isBeingObserved: observable,
|
|
79
|
+
hydrate: action,
|
|
80
|
+
refresh: action,
|
|
81
|
+
value: computed,
|
|
82
|
+
loaded: computed,
|
|
83
|
+
empty: computed,
|
|
84
|
+
loading: computed,
|
|
85
|
+
error: computed,
|
|
86
|
+
isBeingObserved: computed,
|
|
87
|
+
});
|
|
88
|
+
// Set up MobX observation lifecycle hooks
|
|
89
|
+
// This follows the official MobX pattern for lazy observables
|
|
90
|
+
// See: https://mobx.js.org/lazy-observables.html
|
|
91
|
+
this._setupObservationTracking();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Set up MobX observation lifecycle hooks
|
|
95
|
+
* When React components observe this collection, we prevent GC of the parent model
|
|
96
|
+
*/
|
|
97
|
+
_setupObservationTracking() {
|
|
98
|
+
// Track when 'items' becomes observed (React component is rendering)
|
|
99
|
+
const disposeOnObserved = onBecomeObserved(this, 'items', () => {
|
|
100
|
+
this._isBeingObserved = true;
|
|
101
|
+
// Touch parent model to prevent GC while we're being observed
|
|
102
|
+
if (this.objectPool && this.parent?.id) {
|
|
103
|
+
this.objectPool.touch(this.parent.id);
|
|
104
|
+
}
|
|
105
|
+
// Register this collection with parent for observation tracking
|
|
106
|
+
if (this.parent) {
|
|
107
|
+
this.parent._registerObservedCollection(this);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// Track when 'items' stops being observed (component unmounted)
|
|
111
|
+
const disposeOnUnobserved = onBecomeUnobserved(this, 'items', () => {
|
|
112
|
+
this._isBeingObserved = false;
|
|
113
|
+
// Unregister from parent
|
|
114
|
+
if (this.parent) {
|
|
115
|
+
this.parent._unregisterObservedCollection(this);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// Store combined disposer for cleanup
|
|
119
|
+
this.observationDisposer = () => {
|
|
120
|
+
disposeOnObserved();
|
|
121
|
+
disposeOnUnobserved();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check if this collection is currently being observed by React/MobX
|
|
126
|
+
*/
|
|
127
|
+
get isBeingObserved() {
|
|
128
|
+
return this._isBeingObserved;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get the collection value (triggers hydration if needed).
|
|
132
|
+
*
|
|
133
|
+
* Filters out items whose id is no longer in the ObjectPool. The
|
|
134
|
+
* local `items` array isn't auto-synced with `pool.remove()` — a
|
|
135
|
+
* deleted entity would linger here until hydrate() re-runs on
|
|
136
|
+
* reload. Reading `pool.has(item.id)` inside this computed getter
|
|
137
|
+
* makes MobX track both `this.items` AND the pool's entries map,
|
|
138
|
+
* so any pool.remove invalidates the computed and re-renders the
|
|
139
|
+
* consumer with the deleted item gone.
|
|
140
|
+
*
|
|
141
|
+
* Without this, deleting a slide layer would pool.remove() cleanly
|
|
142
|
+
* but the canvas — which reads `slide.layers.value` — would keep
|
|
143
|
+
* showing the deleted layer until a full reload rebuilt the
|
|
144
|
+
* collection.
|
|
145
|
+
*/
|
|
146
|
+
get value() {
|
|
147
|
+
// Touch parent model to prevent GC during active collection usage
|
|
148
|
+
if (this.objectPool && this.parent?.id) {
|
|
149
|
+
this.objectPool.touch(this.parent.id);
|
|
150
|
+
}
|
|
151
|
+
if (this.items === null && !this.isLoading) {
|
|
152
|
+
// Auto-hydrate on first access
|
|
153
|
+
this.hydrate().catch((error) => {
|
|
154
|
+
getContext().observability.breadcrumb('Auto-hydration failed', 'sync.database', 'warning', {
|
|
155
|
+
error: error instanceof Error ? error.message : String(error),
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
return []; // Return empty array while loading
|
|
159
|
+
}
|
|
160
|
+
const raw = this.items || [];
|
|
161
|
+
const pool = this.objectPool;
|
|
162
|
+
if (!pool || raw.length === 0)
|
|
163
|
+
return raw;
|
|
164
|
+
// Filter items still present in the pool. `pool.has(id)` reads the
|
|
165
|
+
// observable `entries` map — MobX tracks the dependency, so a
|
|
166
|
+
// subsequent `pool.remove(id)` re-runs this computed.
|
|
167
|
+
return raw.filter((item) => pool.has(item.id));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if collection has been loaded
|
|
171
|
+
*/
|
|
172
|
+
get loaded() {
|
|
173
|
+
return this.items !== null;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check if collection is empty (only meaningful after loading)
|
|
177
|
+
*/
|
|
178
|
+
get empty() {
|
|
179
|
+
return this.loaded && this.items.length === 0;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Check if currently loading
|
|
183
|
+
*/
|
|
184
|
+
get loading() {
|
|
185
|
+
return this.isLoading;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get load error if any
|
|
189
|
+
*/
|
|
190
|
+
get error() {
|
|
191
|
+
return this.loadError;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get collection size
|
|
195
|
+
*/
|
|
196
|
+
get size() {
|
|
197
|
+
// Touch parent model when accessing collection size
|
|
198
|
+
if (this.objectPool && this.parent?.id) {
|
|
199
|
+
this.objectPool.touch(this.parent.id);
|
|
200
|
+
}
|
|
201
|
+
return this.items?.length || 0;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Hydrate the collection from local storage and/or network
|
|
205
|
+
*/
|
|
206
|
+
async hydrate() {
|
|
207
|
+
// Return existing hydration promise if already in progress
|
|
208
|
+
if (this.hydrationPromise) {
|
|
209
|
+
return this.hydrationPromise;
|
|
210
|
+
}
|
|
211
|
+
// Skip if already loaded
|
|
212
|
+
if (this.items !== null) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.hydrationPromise = this._performHydration();
|
|
216
|
+
try {
|
|
217
|
+
await this.hydrationPromise;
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
this.hydrationPromise = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Internal hydration implementation
|
|
225
|
+
*/
|
|
226
|
+
async _performHydration() {
|
|
227
|
+
this._setLoading(true);
|
|
228
|
+
this._setError(null);
|
|
229
|
+
try {
|
|
230
|
+
// Step 1: Try loading from IndexedDB
|
|
231
|
+
const localData = await this._loadFromLocal();
|
|
232
|
+
if (localData.length > 0) {
|
|
233
|
+
this._setItems(localData);
|
|
234
|
+
// Check if we can skip network hydration
|
|
235
|
+
if (this.options.canSkipNetworkHydration?.()) {
|
|
236
|
+
this._setLoading(false);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
this._setError(error);
|
|
243
|
+
getContext().observability.breadcrumb('Failed to hydrate collection', 'sync.database', 'warning', {
|
|
244
|
+
parent: this.parent.getModelName(),
|
|
245
|
+
parentId: this.parent.id,
|
|
246
|
+
foreignKey: this.foreignKey,
|
|
247
|
+
error: error instanceof Error ? error.message : String(error),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
this._setLoading(false);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Load items from IndexedDB
|
|
256
|
+
*/
|
|
257
|
+
async _loadFromLocal() {
|
|
258
|
+
try {
|
|
259
|
+
if (!this.database) {
|
|
260
|
+
throw new AbloValidationError(`Database dependency not provided to LazyReferenceCollection for ${this.modelName}`, { code: 'lazy_ref_db_missing' });
|
|
261
|
+
}
|
|
262
|
+
if (!this.objectPool) {
|
|
263
|
+
throw new AbloValidationError(`ObjectPool dependency not provided to LazyReferenceCollection for ${this.modelName}`, { code: 'lazy_ref_pool_missing' });
|
|
264
|
+
}
|
|
265
|
+
const store = this.database.getStore(this.modelName);
|
|
266
|
+
const rawData = store ? await store.getAllFromIndex(this.foreignKey, this.parent.id) : [];
|
|
267
|
+
// Get model class from registry
|
|
268
|
+
const ModelClass = getActiveRegistry().getModelByName(this.modelName);
|
|
269
|
+
if (!ModelClass) {
|
|
270
|
+
getContext().observability.breadcrumb(`Model '${this.modelName}' not found in registry`, 'sync.database', 'error');
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
// Convert raw data to model instances
|
|
274
|
+
const models = [];
|
|
275
|
+
for (const data of rawData) {
|
|
276
|
+
const id = data.id;
|
|
277
|
+
// Skip malformed rows. Records from IDB are typed as
|
|
278
|
+
// `Record<string, unknown>` (the centralized
|
|
279
|
+
// `ObjectStoreContract` shape) so the `id` field is
|
|
280
|
+
// narrow-checked here rather than assumed.
|
|
281
|
+
if (typeof id !== 'string')
|
|
282
|
+
continue;
|
|
283
|
+
// Check if already in ObjectPool
|
|
284
|
+
let model = this.objectPool.get(id);
|
|
285
|
+
if (!model) {
|
|
286
|
+
// Create new model instance
|
|
287
|
+
model = new ModelClass();
|
|
288
|
+
model.updateFromData(data);
|
|
289
|
+
this.objectPool.add(model);
|
|
290
|
+
}
|
|
291
|
+
if (model) {
|
|
292
|
+
models.push(model);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Apply filtering if specified
|
|
296
|
+
let filteredModels = models;
|
|
297
|
+
if (this.options.filter) {
|
|
298
|
+
filteredModels = models.filter(this.options.filter);
|
|
299
|
+
}
|
|
300
|
+
// Apply sorting if specified
|
|
301
|
+
if (this.options.sort) {
|
|
302
|
+
filteredModels.sort(this.options.sort);
|
|
303
|
+
}
|
|
304
|
+
// Apply limit if specified
|
|
305
|
+
if (this.options.limit) {
|
|
306
|
+
filteredModels = filteredModels.slice(0, this.options.limit);
|
|
307
|
+
}
|
|
308
|
+
getContext().logger.debug('Loaded local items for collection', {
|
|
309
|
+
count: filteredModels.length,
|
|
310
|
+
parent: this.parent.getModelName(),
|
|
311
|
+
id: this.parent.id,
|
|
312
|
+
fk: this.foreignKey,
|
|
313
|
+
});
|
|
314
|
+
return filteredModels;
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
getContext().observability.breadcrumb('Failed to load from local', 'sync.database', 'warning', {
|
|
318
|
+
error: error instanceof Error ? error.message : String(error),
|
|
319
|
+
});
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Refresh the collection (reload from network)
|
|
325
|
+
*/
|
|
326
|
+
async refresh() {
|
|
327
|
+
this.items = null;
|
|
328
|
+
this.hydrationPromise = null;
|
|
329
|
+
await this.hydrate();
|
|
330
|
+
getContext().logger.debug('Refreshed collection', {
|
|
331
|
+
parent: this.parent.getModelName(),
|
|
332
|
+
id: this.parent.id,
|
|
333
|
+
fk: this.foreignKey,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Add an item to the collection
|
|
338
|
+
*/
|
|
339
|
+
add(item) {
|
|
340
|
+
if (this.items === null) {
|
|
341
|
+
this.items = [];
|
|
342
|
+
}
|
|
343
|
+
// Check if item already exists
|
|
344
|
+
const existingIndex = this.items.findIndex((existing) => existing.id === item.id);
|
|
345
|
+
if (existingIndex >= 0) {
|
|
346
|
+
// Replace existing item
|
|
347
|
+
this.items[existingIndex] = item;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
// Add new item
|
|
351
|
+
this.items.push(item);
|
|
352
|
+
// Apply sorting if specified
|
|
353
|
+
if (this.options.sort) {
|
|
354
|
+
this.items.sort(this.options.sort);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
getContext().logger.debug('Added item to collection', { model: item.getModelName(), id: item.id });
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Remove an item from the collection
|
|
361
|
+
*/
|
|
362
|
+
remove(itemOrId) {
|
|
363
|
+
if (this.items === null)
|
|
364
|
+
return false;
|
|
365
|
+
const id = typeof itemOrId === 'string' ? itemOrId : itemOrId.id;
|
|
366
|
+
const index = this.items.findIndex((item) => item.id === id);
|
|
367
|
+
if (index >= 0) {
|
|
368
|
+
this.items.splice(index, 1);
|
|
369
|
+
getContext().logger.debug('Removed item from collection', { id });
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Find an item in the collection
|
|
376
|
+
*/
|
|
377
|
+
find(predicate) {
|
|
378
|
+
// Touch parent model when searching collection
|
|
379
|
+
if (this.objectPool && this.parent?.id) {
|
|
380
|
+
this.objectPool.touch(this.parent.id);
|
|
381
|
+
}
|
|
382
|
+
if (this.items === null)
|
|
383
|
+
return undefined;
|
|
384
|
+
return this.items.find(predicate);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Filter items in the collection
|
|
388
|
+
*/
|
|
389
|
+
filter(predicate) {
|
|
390
|
+
// Touch parent model when filtering collection
|
|
391
|
+
if (this.objectPool && this.parent?.id) {
|
|
392
|
+
this.objectPool.touch(this.parent.id);
|
|
393
|
+
}
|
|
394
|
+
if (this.items === null)
|
|
395
|
+
return [];
|
|
396
|
+
return this.items.filter(predicate);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Check if collection contains an item
|
|
400
|
+
*/
|
|
401
|
+
contains(itemOrId) {
|
|
402
|
+
if (this.items === null)
|
|
403
|
+
return false;
|
|
404
|
+
const id = typeof itemOrId === 'string' ? itemOrId : itemOrId.id;
|
|
405
|
+
return this.items.some((item) => item.id === id);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Convert to array (triggers hydration)
|
|
409
|
+
*/
|
|
410
|
+
toArray() {
|
|
411
|
+
return this.value;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Set items directly (internal use)
|
|
415
|
+
*/
|
|
416
|
+
_setItems(items) {
|
|
417
|
+
this.items = items;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Set loading state (internal use)
|
|
421
|
+
*/
|
|
422
|
+
_setLoading(loading) {
|
|
423
|
+
this.isLoading = loading;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Set error state (internal use)
|
|
427
|
+
*/
|
|
428
|
+
_setError(error) {
|
|
429
|
+
this.loadError = error;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Clear the collection
|
|
433
|
+
*/
|
|
434
|
+
clear() {
|
|
435
|
+
this.items = [];
|
|
436
|
+
this.loadError = null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Dispose of the collection (cleanup)
|
|
440
|
+
* Following MobX best practice: always clean up observation hooks
|
|
441
|
+
* See: https://github.com/mobxjs/mobx/issues/2047
|
|
442
|
+
*/
|
|
443
|
+
dispose() {
|
|
444
|
+
// Clean up MobX observation hooks first
|
|
445
|
+
if (this.observationDisposer) {
|
|
446
|
+
this.observationDisposer();
|
|
447
|
+
this.observationDisposer = null;
|
|
448
|
+
}
|
|
449
|
+
// Unregister from parent if still registered
|
|
450
|
+
if (this._isBeingObserved && this.parent) {
|
|
451
|
+
this.parent._unregisterObservedCollection(this);
|
|
452
|
+
}
|
|
453
|
+
this._isBeingObserved = false;
|
|
454
|
+
this.items = null;
|
|
455
|
+
this.hydrationPromise = null;
|
|
456
|
+
this.loadError = null;
|
|
457
|
+
this.isLoading = false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
import { getContext } from './context.js';
|