@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,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelRegistry - Type-safe model metadata management
|
|
3
|
+
*
|
|
4
|
+
* Key improvements:
|
|
5
|
+
* - Instance-based for better testing
|
|
6
|
+
* - Validation at registration
|
|
7
|
+
* - Lazy reference resolution
|
|
8
|
+
* - Crypto-based schema hashing
|
|
9
|
+
* - Comprehensive error reporting
|
|
10
|
+
* - Best practices from Linear Sync Engine
|
|
11
|
+
*/
|
|
12
|
+
// Removed Node.js crypto import for browser compatibility
|
|
13
|
+
import { PropertyType, LoadStrategy, } from './types/index.js';
|
|
14
|
+
import { getContext } from './context.js';
|
|
15
|
+
import { AbloValidationError } from './errors.js';
|
|
16
|
+
/**
|
|
17
|
+
* Module-level active registry. Set by createSyncEngine so that Model instances
|
|
18
|
+
* (which don't receive DI) can look up metadata without static maps.
|
|
19
|
+
*/
|
|
20
|
+
let _activeRegistry = null;
|
|
21
|
+
/** Set the active ModelRegistry instance (called by createSyncEngine) */
|
|
22
|
+
export function setActiveRegistry(registry) {
|
|
23
|
+
_activeRegistry = registry;
|
|
24
|
+
}
|
|
25
|
+
/** Get the active ModelRegistry. Throws if none set. */
|
|
26
|
+
export function getActiveRegistry() {
|
|
27
|
+
if (!_activeRegistry) {
|
|
28
|
+
throw new AbloValidationError('No active ModelRegistry — call createSyncEngine() first', { code: 'registry_not_initialized' });
|
|
29
|
+
}
|
|
30
|
+
return _activeRegistry;
|
|
31
|
+
}
|
|
32
|
+
/** Whether an active ModelRegistry has been set. */
|
|
33
|
+
export function hasActiveRegistry() {
|
|
34
|
+
return _activeRegistry !== null;
|
|
35
|
+
}
|
|
36
|
+
/** Clear the active ModelRegistry (tests only). */
|
|
37
|
+
export function clearActiveRegistry() {
|
|
38
|
+
_activeRegistry = null;
|
|
39
|
+
}
|
|
40
|
+
export class ModelRegistry {
|
|
41
|
+
models = new Map();
|
|
42
|
+
modelMetadata = new Map();
|
|
43
|
+
properties = new Map();
|
|
44
|
+
references = new Map();
|
|
45
|
+
pendingReferences = new Map();
|
|
46
|
+
// 🔧 PROPER FIX: Static mapping from constructor to model name
|
|
47
|
+
constructorToModelName = new Map();
|
|
48
|
+
// LINEAR PATTERN: BackReferences for cascade-aware transaction handling.
|
|
49
|
+
// Maps childModelName → BackReferenceMetadata[] (which parent models
|
|
50
|
+
// own this child). The inverse direction (parentModelName → children)
|
|
51
|
+
// is derived on demand by `getChildModels`, populated only here.
|
|
52
|
+
backReferences = new Map();
|
|
53
|
+
schemaHash;
|
|
54
|
+
config;
|
|
55
|
+
registeredModels = new Set();
|
|
56
|
+
batchMode = false;
|
|
57
|
+
pendingHashUpdate = false;
|
|
58
|
+
constructor(config = {}) {
|
|
59
|
+
this.config = {
|
|
60
|
+
validateOnRegister: config.validateOnRegister ?? true,
|
|
61
|
+
allowLateReferences: config.allowLateReferences ?? true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
validateModelConstructor(name, constructor) {
|
|
65
|
+
if (typeof constructor !== 'function') {
|
|
66
|
+
throw new AbloValidationError(`Model ${name} constructor must be a function`, { code: 'registry_invalid_constructor' });
|
|
67
|
+
}
|
|
68
|
+
if (!constructor.prototype) {
|
|
69
|
+
throw new AbloValidationError(`Model ${name} constructor must have a prototype`, { code: 'registry_invalid_constructor' });
|
|
70
|
+
}
|
|
71
|
+
// Check for required methods
|
|
72
|
+
const required = ['updateFromData', 'toJSON', 'getModelName'];
|
|
73
|
+
for (const method of required) {
|
|
74
|
+
if (typeof constructor.prototype[method] !== 'function') {
|
|
75
|
+
getContext().logger.debug('Model missing required method', name, { method });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
arePropertiesCompatible(existing, incoming) {
|
|
80
|
+
// For reference-generated ID properties, be more lenient
|
|
81
|
+
// Only check core compatibility, not all metadata fields
|
|
82
|
+
return (existing.type === incoming.type &&
|
|
83
|
+
// For indexed, treat undefined as false for comparison
|
|
84
|
+
(existing.indexed ?? false) === (incoming.indexed ?? false) &&
|
|
85
|
+
// For optional, treat undefined as false for comparison
|
|
86
|
+
(existing.optional ?? false) === (incoming.optional ?? false));
|
|
87
|
+
}
|
|
88
|
+
addPendingReference(modelName, propertyName, metadata) {
|
|
89
|
+
// Get target model name
|
|
90
|
+
let targetName;
|
|
91
|
+
try {
|
|
92
|
+
targetName = metadata.referencedModel()?.name || 'Unknown';
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
targetName = 'Unknown';
|
|
96
|
+
}
|
|
97
|
+
let pending = this.pendingReferences.get(targetName);
|
|
98
|
+
if (!pending) {
|
|
99
|
+
pending = [];
|
|
100
|
+
this.pendingReferences.set(targetName, pending);
|
|
101
|
+
}
|
|
102
|
+
pending.push({ modelName, propertyName, metadata });
|
|
103
|
+
getContext().logger.debug('Reference deferred', `${modelName}.${propertyName}`, { targetModel: targetName });
|
|
104
|
+
}
|
|
105
|
+
resolvePendingReferences(targetModelName) {
|
|
106
|
+
const pending = this.pendingReferences.get(targetModelName);
|
|
107
|
+
if (!pending)
|
|
108
|
+
return;
|
|
109
|
+
for (const ref of pending) {
|
|
110
|
+
try {
|
|
111
|
+
this.completeReferenceRegistration(ref.modelName, ref.propertyName, ref.metadata);
|
|
112
|
+
getContext().logger.debug('Reference resolved', `${ref.modelName}.${ref.propertyName}`, {
|
|
113
|
+
targetModel: targetModelName,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
getContext().observability.breadcrumb(`Failed to resolve reference ${ref.modelName}.${ref.propertyName}`, 'sync.database', 'error', {
|
|
118
|
+
error: error instanceof Error ? error.message : String(error),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.pendingReferences.delete(targetModelName);
|
|
123
|
+
}
|
|
124
|
+
completeReferenceRegistration(modelName, propertyName, metadata) {
|
|
125
|
+
// Store reference
|
|
126
|
+
let refs = this.references.get(modelName);
|
|
127
|
+
if (!refs) {
|
|
128
|
+
refs = new Map();
|
|
129
|
+
this.references.set(modelName, refs);
|
|
130
|
+
}
|
|
131
|
+
refs.set(propertyName, metadata);
|
|
132
|
+
// Register ID property (skip organizationId as it's handled by models themselves)
|
|
133
|
+
const idPropName = propertyName.endsWith('Id') ? propertyName : `${propertyName}Id`;
|
|
134
|
+
if (idPropName !== 'organizationId') {
|
|
135
|
+
this.registerProperty(modelName, idPropName, {
|
|
136
|
+
type: PropertyType.reference,
|
|
137
|
+
indexed: metadata.indexed || false,
|
|
138
|
+
optional: metadata.nullable || false,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// Register model property
|
|
142
|
+
this.registerProperty(modelName, propertyName, {
|
|
143
|
+
type: PropertyType.referenceModel,
|
|
144
|
+
optional: metadata.nullable || false,
|
|
145
|
+
});
|
|
146
|
+
this.schemaHash = undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Register a model with validation
|
|
150
|
+
*/
|
|
151
|
+
registerModel(name, constructor, metadata = { loadStrategy: LoadStrategy.instant }) {
|
|
152
|
+
// Validate
|
|
153
|
+
if (this.config.validateOnRegister) {
|
|
154
|
+
this.validateModelConstructor(name, constructor);
|
|
155
|
+
}
|
|
156
|
+
// Check for duplicate
|
|
157
|
+
if (this.models.has(name)) {
|
|
158
|
+
getContext().logger.debug('Model already registered, skipping', name);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
getContext().logger.debug('Registering model', name);
|
|
162
|
+
// Register
|
|
163
|
+
this.models.set(name, constructor);
|
|
164
|
+
this.modelMetadata.set(name, metadata);
|
|
165
|
+
// 🔧 PROPER FIX: Create reverse mapping from constructor to model name
|
|
166
|
+
this.constructorToModelName.set(constructor, name);
|
|
167
|
+
// Initialize property maps
|
|
168
|
+
if (!this.properties.has(name)) {
|
|
169
|
+
this.properties.set(name, new Map());
|
|
170
|
+
}
|
|
171
|
+
if (!this.references.has(name)) {
|
|
172
|
+
this.references.set(name, new Map());
|
|
173
|
+
}
|
|
174
|
+
// Mark as registered
|
|
175
|
+
this.registeredModels.add(name);
|
|
176
|
+
// Resolve pending references to this model
|
|
177
|
+
this.resolvePendingReferences(name);
|
|
178
|
+
// Invalidate schema hash
|
|
179
|
+
this.schemaHash = undefined;
|
|
180
|
+
getContext().logger.debug('Model registered', name, metadata);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Register property with validation
|
|
184
|
+
*/
|
|
185
|
+
registerProperty(modelName, propertyName, metadata) {
|
|
186
|
+
// Validate model exists
|
|
187
|
+
if (!this.models.has(modelName) && this.config.validateOnRegister) {
|
|
188
|
+
throw new AbloValidationError(`Cannot register property for unknown model: ${modelName}`, { code: 'registry_unknown_model' });
|
|
189
|
+
}
|
|
190
|
+
// Get or create property map
|
|
191
|
+
let props = this.properties.get(modelName);
|
|
192
|
+
if (!props) {
|
|
193
|
+
props = new Map();
|
|
194
|
+
this.properties.set(modelName, props);
|
|
195
|
+
}
|
|
196
|
+
// Check for conflicts
|
|
197
|
+
const existing = props.get(propertyName);
|
|
198
|
+
if (existing) {
|
|
199
|
+
if (this.arePropertiesCompatible(existing, metadata)) {
|
|
200
|
+
// Properties are compatible, skip re-registration
|
|
201
|
+
getContext().logger.debug('Property already registered (compatible)', `${modelName}.${propertyName}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
throw new AbloValidationError(`Property ${modelName}.${propertyName} already registered with incompatible metadata`, { code: 'registry_property_conflict' });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
props.set(propertyName, metadata);
|
|
209
|
+
this.schemaHash = undefined;
|
|
210
|
+
getContext().logger.debug('Property registered', `${modelName}.${propertyName}`, metadata);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Register reference with lazy resolution
|
|
214
|
+
*/
|
|
215
|
+
registerReference(modelName, propertyName, metadata) {
|
|
216
|
+
// Try to resolve target model
|
|
217
|
+
let targetModelName;
|
|
218
|
+
try {
|
|
219
|
+
const targetModel = metadata.referencedModel();
|
|
220
|
+
targetModelName = targetModel?.name;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Defer resolution
|
|
224
|
+
if (this.config.allowLateReferences) {
|
|
225
|
+
this.addPendingReference(modelName, propertyName, metadata);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
throw new AbloValidationError(`Cannot resolve reference ${modelName}.${propertyName}`, { code: 'registry_reference_unresolved' });
|
|
229
|
+
}
|
|
230
|
+
// Validate target exists or defer
|
|
231
|
+
if (!this.models.has(targetModelName)) {
|
|
232
|
+
if (this.config.allowLateReferences) {
|
|
233
|
+
this.addPendingReference(modelName, propertyName, metadata);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
throw new AbloValidationError(`Reference ${modelName}.${propertyName} points to unknown model ${targetModelName}`, { code: 'registry_reference_unknown_target' });
|
|
237
|
+
}
|
|
238
|
+
// Complete registration
|
|
239
|
+
this.completeReferenceRegistration(modelName, propertyName, metadata);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* LINEAR PATTERN: Register a back-reference for cascade-aware transaction handling
|
|
243
|
+
*
|
|
244
|
+
* When a parent model is deleted, the TransactionQueue will cancel pending
|
|
245
|
+
* transactions for all child models that have a backReference to that parent.
|
|
246
|
+
*
|
|
247
|
+
* @param childModelName - The model that has a FK to the parent (e.g., 'Slide')
|
|
248
|
+
* @param metadata - BackReference configuration
|
|
249
|
+
*/
|
|
250
|
+
registerBackReference(childModelName, metadata) {
|
|
251
|
+
// Add to instance map
|
|
252
|
+
let refs = this.backReferences.get(childModelName);
|
|
253
|
+
if (!refs) {
|
|
254
|
+
refs = [];
|
|
255
|
+
this.backReferences.set(childModelName, refs);
|
|
256
|
+
}
|
|
257
|
+
// Avoid duplicates
|
|
258
|
+
const exists = refs.some((r) => r.parentModel === metadata.parentModel && r.foreignKey === metadata.foreignKey);
|
|
259
|
+
if (!exists) {
|
|
260
|
+
refs.push(metadata);
|
|
261
|
+
}
|
|
262
|
+
// Reverse lookup (parent → children) is derived on demand by
|
|
263
|
+
// `getChildModels`, which scans this map.
|
|
264
|
+
getContext().logger.debug('BackReference registered', `${childModelName} -> ${metadata.parentModel}`, {
|
|
265
|
+
foreignKey: metadata.foreignKey,
|
|
266
|
+
cascadeDelete: metadata.cascadeDelete,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/** Get all models with specific load strategy. */
|
|
270
|
+
getModelsByLoadStrategy(strategy) {
|
|
271
|
+
const models = [];
|
|
272
|
+
for (const [modelName, metadata] of this.modelMetadata) {
|
|
273
|
+
if (metadata.loadStrategy === strategy) {
|
|
274
|
+
models.push(modelName);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return models;
|
|
278
|
+
}
|
|
279
|
+
/** Get model name from constructor (production-safe). */
|
|
280
|
+
getModelNameFromConstructor(constructor) {
|
|
281
|
+
return this.constructorToModelName.get(constructor);
|
|
282
|
+
}
|
|
283
|
+
/** Get properties for a model. */
|
|
284
|
+
getPropertiesForModel(modelName) {
|
|
285
|
+
return this.properties.get(modelName) || new Map();
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get all registered model names for this instance
|
|
289
|
+
*/
|
|
290
|
+
getRegisteredModelNames() {
|
|
291
|
+
return Array.from(this.models.keys());
|
|
292
|
+
}
|
|
293
|
+
/** Get model constructor by name */
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
295
|
+
getModelByName(name) {
|
|
296
|
+
return this.models.get(name);
|
|
297
|
+
}
|
|
298
|
+
/** Check if model is registered */
|
|
299
|
+
hasModel(name) {
|
|
300
|
+
return this.models.has(name);
|
|
301
|
+
}
|
|
302
|
+
/** Get model metadata by name */
|
|
303
|
+
getMetadata(name) {
|
|
304
|
+
return this.modelMetadata.get(name);
|
|
305
|
+
}
|
|
306
|
+
/** Get properties for a model */
|
|
307
|
+
getProperties(name) {
|
|
308
|
+
return this.properties.get(name) || new Map();
|
|
309
|
+
}
|
|
310
|
+
/** Get references for a model */
|
|
311
|
+
getReferences(name) {
|
|
312
|
+
return this.references.get(name) || new Map();
|
|
313
|
+
}
|
|
314
|
+
/** Get indexed properties for a model */
|
|
315
|
+
getIndexedProperties(modelName) {
|
|
316
|
+
const properties = this.getProperties(modelName);
|
|
317
|
+
const indexed = [];
|
|
318
|
+
for (const [propName, metadata] of properties) {
|
|
319
|
+
if (metadata.indexed)
|
|
320
|
+
indexed.push(propName);
|
|
321
|
+
}
|
|
322
|
+
return indexed;
|
|
323
|
+
}
|
|
324
|
+
/** Get back-references for a child model */
|
|
325
|
+
getBackReferences(childModelName) {
|
|
326
|
+
return this.backReferences.get(childModelName) || [];
|
|
327
|
+
}
|
|
328
|
+
/** Get child models for a parent */
|
|
329
|
+
getChildModels(parentModelName) {
|
|
330
|
+
// Derive from backReferences
|
|
331
|
+
const children = [];
|
|
332
|
+
for (const [childModel, refs] of this.backReferences) {
|
|
333
|
+
for (const ref of refs) {
|
|
334
|
+
if (ref.parentModel === parentModelName) {
|
|
335
|
+
children.push({ childModel, foreignKey: ref.foreignKey });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return children;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Calculate schema hash using crypto
|
|
343
|
+
*/
|
|
344
|
+
getSchemaHash() {
|
|
345
|
+
if (this.schemaHash)
|
|
346
|
+
return this.schemaHash;
|
|
347
|
+
const schema = {};
|
|
348
|
+
// Build schema object
|
|
349
|
+
for (const [modelName, props] of this.properties) {
|
|
350
|
+
schema[modelName] = {};
|
|
351
|
+
for (const [propName, meta] of props) {
|
|
352
|
+
schema[modelName][propName] = {
|
|
353
|
+
type: meta.type,
|
|
354
|
+
indexed: meta.indexed || false,
|
|
355
|
+
optional: meta.optional || false,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Sort and stringify
|
|
360
|
+
const sorted = JSON.stringify(schema, Object.keys(schema).sort());
|
|
361
|
+
// Create hash - browser-compatible simple hash
|
|
362
|
+
this.schemaHash = this.simpleHash(sorted);
|
|
363
|
+
getContext().logger.debug('Schema hash updated', this.schemaHash);
|
|
364
|
+
return this.schemaHash;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Browser-compatible hash function
|
|
368
|
+
*/
|
|
369
|
+
simpleHash(str) {
|
|
370
|
+
let hash = 0;
|
|
371
|
+
for (let i = 0; i < str.length; i++) {
|
|
372
|
+
const char = str.charCodeAt(i);
|
|
373
|
+
hash = (hash << 5) - hash + char;
|
|
374
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
375
|
+
}
|
|
376
|
+
return Math.abs(hash).toString(16).padStart(8, '0');
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Static wrapper for backward compatibility
|
|
380
|
+
*/
|
|
381
|
+
/**
|
|
382
|
+
* Validate all references
|
|
383
|
+
*/
|
|
384
|
+
validateReferences() {
|
|
385
|
+
const errors = [];
|
|
386
|
+
// Check pending references
|
|
387
|
+
for (const [target, pending] of this.pendingReferences) {
|
|
388
|
+
for (const ref of pending) {
|
|
389
|
+
errors.push(`Unresolved reference: ${ref.modelName}.${ref.propertyName} -> ${target}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Check resolved references
|
|
393
|
+
for (const [modelName, refs] of this.references) {
|
|
394
|
+
for (const [propName, meta] of refs) {
|
|
395
|
+
try {
|
|
396
|
+
const target = meta.referencedModel();
|
|
397
|
+
if (!this.models.has(target.name)) {
|
|
398
|
+
errors.push(`Invalid reference: ${modelName}.${propName} -> ${target.name}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
errors.push(`Cannot resolve reference: ${modelName}.${propName}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const isValid = errors.length === 0;
|
|
407
|
+
if (isValid) {
|
|
408
|
+
getContext().logger.info('All model references are valid');
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
getContext().observability.breadcrumb('Reference validation failed', 'sync.database', 'error');
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
valid: isValid,
|
|
415
|
+
errors,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Static wrapper for backward compatibility
|
|
420
|
+
*/
|
|
421
|
+
/**
|
|
422
|
+
* Start batch registration mode to optimize performance
|
|
423
|
+
*/
|
|
424
|
+
startBatch() {
|
|
425
|
+
this.batchMode = true;
|
|
426
|
+
this.pendingHashUpdate = false;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Static wrapper for backward compatibility
|
|
430
|
+
*/
|
|
431
|
+
/**
|
|
432
|
+
* End batch registration mode and update schema hash if needed
|
|
433
|
+
*/
|
|
434
|
+
endBatch() {
|
|
435
|
+
this.batchMode = false;
|
|
436
|
+
if (this.pendingHashUpdate) {
|
|
437
|
+
this.getSchemaHash(); // This will recalculate if needed
|
|
438
|
+
this.pendingHashUpdate = false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Static wrapper for backward compatibility
|
|
443
|
+
*/
|
|
444
|
+
/**
|
|
445
|
+
* Clear registry
|
|
446
|
+
*/
|
|
447
|
+
clear() {
|
|
448
|
+
this.models.clear();
|
|
449
|
+
this.modelMetadata.clear();
|
|
450
|
+
this.properties.clear();
|
|
451
|
+
this.references.clear();
|
|
452
|
+
this.pendingReferences.clear();
|
|
453
|
+
this.registeredModels.clear();
|
|
454
|
+
this.backReferences.clear();
|
|
455
|
+
this.constructorToModelName.clear();
|
|
456
|
+
this.schemaHash = undefined;
|
|
457
|
+
this.batchMode = false;
|
|
458
|
+
this.pendingHashUpdate = false;
|
|
459
|
+
getContext().logger.info('ModelRegistry cleared');
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Static wrapper for backward compatibility
|
|
463
|
+
*/
|
|
464
|
+
/**
|
|
465
|
+
* Export for debugging
|
|
466
|
+
*/
|
|
467
|
+
export() {
|
|
468
|
+
return {
|
|
469
|
+
models: Array.from(this.models.keys()),
|
|
470
|
+
metadata: Object.fromEntries(this.modelMetadata),
|
|
471
|
+
properties: Object.fromEntries(Array.from(this.properties.entries()).map(([name, props]) => [
|
|
472
|
+
name,
|
|
473
|
+
Object.fromEntries(props),
|
|
474
|
+
])),
|
|
475
|
+
references: Object.fromEntries(Array.from(this.references.entries()).map(([name, refs]) => [
|
|
476
|
+
name,
|
|
477
|
+
Object.fromEntries(refs),
|
|
478
|
+
])),
|
|
479
|
+
pending: Object.fromEntries(Array.from(this.pendingReferences.entries()).map(([name, refs]) => [
|
|
480
|
+
name,
|
|
481
|
+
refs.map((r) => `${r.modelName}.${r.propertyName}`),
|
|
482
|
+
])),
|
|
483
|
+
schemaHash: this.getSchemaHash(),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Export registry data for debugging (backward compatibility)
|
|
488
|
+
*/
|
|
489
|
+
exportRegistryData() {
|
|
490
|
+
const models = {};
|
|
491
|
+
const properties = {};
|
|
492
|
+
const references = {};
|
|
493
|
+
const metadata = {};
|
|
494
|
+
// Export models
|
|
495
|
+
for (const [name, constructor] of this.models) {
|
|
496
|
+
models[name] = constructor.name;
|
|
497
|
+
}
|
|
498
|
+
// Export properties
|
|
499
|
+
for (const [modelName, propertyMap] of this.properties) {
|
|
500
|
+
properties[modelName] = {};
|
|
501
|
+
for (const [propName, propMetadata] of propertyMap) {
|
|
502
|
+
properties[modelName][propName] = propMetadata;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Export references
|
|
506
|
+
for (const [modelName, referenceMap] of this.references) {
|
|
507
|
+
references[modelName] = {};
|
|
508
|
+
for (const [refName, refMetadata] of referenceMap) {
|
|
509
|
+
try {
|
|
510
|
+
references[modelName][refName] = {
|
|
511
|
+
...refMetadata,
|
|
512
|
+
referencedModel: refMetadata.referencedModel().name,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
references[modelName][refName] = {
|
|
517
|
+
...refMetadata,
|
|
518
|
+
referencedModel: 'Unresolved',
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Export metadata
|
|
524
|
+
for (const [modelName, modelMetadata] of this.modelMetadata) {
|
|
525
|
+
metadata[modelName] = modelMetadata;
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
models,
|
|
529
|
+
properties,
|
|
530
|
+
references,
|
|
531
|
+
metadata,
|
|
532
|
+
schemaHash: this.getSchemaHash(),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NetworkMonitor - Network connectivity tracking with visibility awareness
|
|
3
|
+
*
|
|
4
|
+
* Monitors online/offline state using browser events AND visibility changes.
|
|
5
|
+
* When a tab becomes visible after being hidden (e.g., laptop sleep/wake),
|
|
6
|
+
* the WebSocket may have silently died without triggering online/offline events.
|
|
7
|
+
* The visibility handler detects this and emits 'online' to trigger recovery.
|
|
8
|
+
*/
|
|
9
|
+
import { EventEmitter } from 'events';
|
|
10
|
+
export declare class NetworkMonitor extends EventEmitter {
|
|
11
|
+
private isOnline;
|
|
12
|
+
private lastOnlineCheck;
|
|
13
|
+
constructor();
|
|
14
|
+
private handleOnline;
|
|
15
|
+
private handleOffline;
|
|
16
|
+
/**
|
|
17
|
+
* When the tab becomes visible, the WebSocket may have silently died
|
|
18
|
+
* (e.g., laptop sleep/wake, long background). Browser online/offline events
|
|
19
|
+
* don't fire in this case because the network itself didn't change.
|
|
20
|
+
* Emit 'visibility_online' so SyncedStore can check and recover.
|
|
21
|
+
*/
|
|
22
|
+
private handleVisibilityChange;
|
|
23
|
+
private setupListeners;
|
|
24
|
+
getStatus(): boolean;
|
|
25
|
+
getLastOnlineTime(): Date;
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NetworkMonitor - Network connectivity tracking with visibility awareness
|
|
3
|
+
*
|
|
4
|
+
* Monitors online/offline state using browser events AND visibility changes.
|
|
5
|
+
* When a tab becomes visible after being hidden (e.g., laptop sleep/wake),
|
|
6
|
+
* the WebSocket may have silently died without triggering online/offline events.
|
|
7
|
+
* The visibility handler detects this and emits 'online' to trigger recovery.
|
|
8
|
+
*/
|
|
9
|
+
import { EventEmitter } from 'events';
|
|
10
|
+
import { getContext } from './context.js';
|
|
11
|
+
export class NetworkMonitor extends EventEmitter {
|
|
12
|
+
isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
13
|
+
lastOnlineCheck = new Date();
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.setupListeners();
|
|
17
|
+
}
|
|
18
|
+
handleOnline = async () => {
|
|
19
|
+
const wasOffline = !this.isOnline;
|
|
20
|
+
this.isOnline = true;
|
|
21
|
+
this.lastOnlineCheck = new Date();
|
|
22
|
+
if (wasOffline) {
|
|
23
|
+
getContext().logger.info('Network connection restored');
|
|
24
|
+
this.emit('online');
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
handleOffline = () => {
|
|
28
|
+
const wasOnline = this.isOnline;
|
|
29
|
+
this.isOnline = false;
|
|
30
|
+
if (wasOnline) {
|
|
31
|
+
getContext().logger.warn('Network connection lost');
|
|
32
|
+
this.emit('offline');
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* When the tab becomes visible, the WebSocket may have silently died
|
|
37
|
+
* (e.g., laptop sleep/wake, long background). Browser online/offline events
|
|
38
|
+
* don't fire in this case because the network itself didn't change.
|
|
39
|
+
* Emit 'visibility_online' so SyncedStore can check and recover.
|
|
40
|
+
*/
|
|
41
|
+
handleVisibilityChange = () => {
|
|
42
|
+
if (document.visibilityState !== 'visible')
|
|
43
|
+
return;
|
|
44
|
+
// Update navigator.onLine state — it may have changed while hidden
|
|
45
|
+
this.isOnline = navigator.onLine;
|
|
46
|
+
this.lastOnlineCheck = new Date();
|
|
47
|
+
if (this.isOnline) {
|
|
48
|
+
getContext().logger.info('Tab became visible with network available — emitting visibility_online');
|
|
49
|
+
this.emit('visibility_online');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
setupListeners() {
|
|
53
|
+
if (typeof window === 'undefined')
|
|
54
|
+
return;
|
|
55
|
+
window.addEventListener('online', this.handleOnline);
|
|
56
|
+
window.addEventListener('offline', this.handleOffline);
|
|
57
|
+
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
58
|
+
}
|
|
59
|
+
getStatus() {
|
|
60
|
+
return this.isOnline;
|
|
61
|
+
}
|
|
62
|
+
getLastOnlineTime() {
|
|
63
|
+
return this.lastOnlineCheck;
|
|
64
|
+
}
|
|
65
|
+
dispose() {
|
|
66
|
+
if (typeof window !== 'undefined') {
|
|
67
|
+
window.removeEventListener('online', this.handleOnline);
|
|
68
|
+
window.removeEventListener('offline', this.handleOffline);
|
|
69
|
+
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
70
|
+
}
|
|
71
|
+
this.removeAllListeners();
|
|
72
|
+
}
|
|
73
|
+
}
|