@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
package/dist/Model.js
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model - Clean base class for domain models
|
|
3
|
+
*
|
|
4
|
+
* Models are pure domain objects that:
|
|
5
|
+
* - Hold data and business logic
|
|
6
|
+
* - Track their own changes
|
|
7
|
+
* - Validate themselves
|
|
8
|
+
* - Return updates/changes (not perform them)
|
|
9
|
+
*
|
|
10
|
+
* Models do NOT:
|
|
11
|
+
* - Access stores or singletons
|
|
12
|
+
* - Perform side effects (saving, notifications)
|
|
13
|
+
* - Know about sync infrastructure
|
|
14
|
+
*/
|
|
15
|
+
import { runInAction, isComputedProp } from 'mobx';
|
|
16
|
+
import { v4 as uuid } from 'uuid';
|
|
17
|
+
import { M1 } from './utils/mobx-setup.js';
|
|
18
|
+
import { getActiveRegistry, hasActiveRegistry } from './ModelRegistry.js';
|
|
19
|
+
import { getContext } from './context.js';
|
|
20
|
+
import { AbloValidationError } from './errors.js';
|
|
21
|
+
/**
|
|
22
|
+
* Validation error for model validation failures
|
|
23
|
+
*/
|
|
24
|
+
export class ValidationError extends Error {
|
|
25
|
+
errors;
|
|
26
|
+
constructor(errors) {
|
|
27
|
+
super(`Validation failed: ${errors.join(', ')}`);
|
|
28
|
+
this.errors = errors;
|
|
29
|
+
this.name = 'ValidationError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Abstract Model - Base class for all domain models
|
|
34
|
+
*
|
|
35
|
+
* Pure domain object with no external dependencies
|
|
36
|
+
*/
|
|
37
|
+
export class Model {
|
|
38
|
+
/** Static reference to active SyncedStore for reactive queries */
|
|
39
|
+
static store = null;
|
|
40
|
+
/** Unique identifier - always permanent UUID */
|
|
41
|
+
id;
|
|
42
|
+
/** Client ID - always equals id, kept for compatibility */
|
|
43
|
+
clientId;
|
|
44
|
+
/** MobX observable properties storage */
|
|
45
|
+
_mobxProperties = {};
|
|
46
|
+
/** Referenced models cache */
|
|
47
|
+
_referencedModels = {};
|
|
48
|
+
/** Track property changes */
|
|
49
|
+
modifiedProperties = new Map();
|
|
50
|
+
/** Track if this is a new model */
|
|
51
|
+
_isNew = true;
|
|
52
|
+
/** Original data snapshot */
|
|
53
|
+
_originalData;
|
|
54
|
+
/** Sync status */
|
|
55
|
+
syncStatus = 'pending';
|
|
56
|
+
/** Timestamps */
|
|
57
|
+
createdAt;
|
|
58
|
+
updatedAt;
|
|
59
|
+
archivedAt;
|
|
60
|
+
/** Validation rules */
|
|
61
|
+
validationRules = {};
|
|
62
|
+
/** Lifecycle state */
|
|
63
|
+
isDisposed = false;
|
|
64
|
+
disposers = [];
|
|
65
|
+
/**
|
|
66
|
+
* Track observed LazyReferenceCollections for GC prevention
|
|
67
|
+
* When any collection is being observed by React, the model should not be GC'd
|
|
68
|
+
* Following MobX best practice: https://mobx.js.org/lazy-observables.html
|
|
69
|
+
*/
|
|
70
|
+
_observedCollections = new Set();
|
|
71
|
+
constructor(data = {}) {
|
|
72
|
+
// Always generate permanent UUID on client
|
|
73
|
+
this.id = data.id || Model.generateId();
|
|
74
|
+
this.clientId = this.id; // No more temp IDs!
|
|
75
|
+
// Ensure dates are Date objects, not strings
|
|
76
|
+
this.createdAt = data.createdAt
|
|
77
|
+
? data.createdAt instanceof Date
|
|
78
|
+
? data.createdAt
|
|
79
|
+
: new Date(data.createdAt)
|
|
80
|
+
: new Date();
|
|
81
|
+
this.updatedAt = data.updatedAt
|
|
82
|
+
? data.updatedAt instanceof Date
|
|
83
|
+
? data.updatedAt
|
|
84
|
+
: new Date(data.updatedAt)
|
|
85
|
+
: new Date();
|
|
86
|
+
this.syncStatus = data.syncStatus || 'pending';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Generate unique ID
|
|
90
|
+
*/
|
|
91
|
+
static generateId() {
|
|
92
|
+
return uuid();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Set the active SyncedStore reference for reactive queries.
|
|
96
|
+
* Called once at engine initialization.
|
|
97
|
+
*/
|
|
98
|
+
static setStore(store) {
|
|
99
|
+
Model.store = store;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the active SyncedStore reference for reactive queries.
|
|
103
|
+
*
|
|
104
|
+
* Returns `null` if no store has been registered yet (e.g. during
|
|
105
|
+
* bootstrap before the engine is ready). Subclasses should use this
|
|
106
|
+
* instead of reaching into the private static field via bracket
|
|
107
|
+
* notation — the generic parameter lets app-side Model subclasses
|
|
108
|
+
* narrow the return to their concrete store type.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // In a Slide model getter
|
|
112
|
+
* const store = Slide.getStore();
|
|
113
|
+
* if (!store) return [];
|
|
114
|
+
* return store.getByForeignKey<SlideLayer>('SlideLayer', 'slideId', this.id);
|
|
115
|
+
*/
|
|
116
|
+
static getStore() {
|
|
117
|
+
return Model.store;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Initialize MobX observability
|
|
121
|
+
*/
|
|
122
|
+
makeObservable() {
|
|
123
|
+
const modelName = this.getModelName();
|
|
124
|
+
// Get metadata from static ModelRegistry
|
|
125
|
+
const propertyMetadata = getActiveRegistry().getProperties(modelName);
|
|
126
|
+
const referenceMetadata = getActiveRegistry().getReferences(modelName);
|
|
127
|
+
// Use M1 for observability setup
|
|
128
|
+
M1(this, propertyMetadata, referenceMetadata);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Track property changes
|
|
132
|
+
*/
|
|
133
|
+
propertyChanged(propertyName, oldValue, newValue) {
|
|
134
|
+
if (oldValue === newValue)
|
|
135
|
+
return;
|
|
136
|
+
runInAction(() => {
|
|
137
|
+
// Preserve the earliest captured `old` for this field until the entry
|
|
138
|
+
// is cleared (by `clearChanges` on sync-ack or by a mutator consuming
|
|
139
|
+
// it). Consecutive in-place mutations between mutator invocations —
|
|
140
|
+
// e.g. a drag loop writing `layer.position = ...` on every frame —
|
|
141
|
+
// would otherwise overwrite `.old` with each frame's predecessor,
|
|
142
|
+
// destroying the pre-session baseline that `RecordingTransaction`
|
|
143
|
+
// relies on to record a correct undo inverse. `.new` always reflects
|
|
144
|
+
// the latest value so the transaction queue's `getChanges()` keeps
|
|
145
|
+
// sending the right payload to the server.
|
|
146
|
+
const existing = this.modifiedProperties.get(propertyName);
|
|
147
|
+
this.modifiedProperties.set(propertyName, {
|
|
148
|
+
old: existing ? existing.old : oldValue,
|
|
149
|
+
new: newValue,
|
|
150
|
+
});
|
|
151
|
+
this.updatedAt = new Date();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get changes as object
|
|
156
|
+
*/
|
|
157
|
+
getChanges() {
|
|
158
|
+
const changes = {};
|
|
159
|
+
for (const [propertyName, change] of this.modifiedProperties) {
|
|
160
|
+
changes[propertyName] = change.new;
|
|
161
|
+
}
|
|
162
|
+
return changes;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Check if model has changes
|
|
166
|
+
*/
|
|
167
|
+
get hasChanges() {
|
|
168
|
+
return this.modifiedProperties.size > 0;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Mark model as persisted (not new)
|
|
172
|
+
*/
|
|
173
|
+
markAsPersisted() {
|
|
174
|
+
this._isNew = false;
|
|
175
|
+
this._originalData = this.captureSnapshot();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if this is a new model
|
|
179
|
+
*/
|
|
180
|
+
isNew() {
|
|
181
|
+
return this._isNew;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Read-only view of the snapshot taken at `markAsPersisted()` /
|
|
185
|
+
* load. Used by recording-transaction undo to derive a pre-session
|
|
186
|
+
* baseline for fields that weren't yet pre-mutated (so
|
|
187
|
+
* `modifiedProperties` has no entry for them). Returns the same
|
|
188
|
+
* underlying object — callers must not mutate it.
|
|
189
|
+
*
|
|
190
|
+
* Architectural note: this method exists because we allow direct
|
|
191
|
+
* property writes (`slide.title = 'foo'`) AND mutator-recorded
|
|
192
|
+
* writes to coexist. Zero / Replicache structurally avoids this:
|
|
193
|
+
* every mutation MUST go through a registered mutator function,
|
|
194
|
+
* mutator args are serialized, and on server pull all unacked
|
|
195
|
+
* mutations are dropped and the mutator functions are replayed on
|
|
196
|
+
* the new basis (rebase). That makes per-instance baselines
|
|
197
|
+
* unnecessary because the b-tree at the new basis IS the
|
|
198
|
+
* authoritative pre-session state.
|
|
199
|
+
*
|
|
200
|
+
* If we ever migrate to "mutators are the only write path," this
|
|
201
|
+
* snapshot field, `_originalData`, and most of
|
|
202
|
+
* `RecordingTransaction.snapshotFields` become dead code. See
|
|
203
|
+
* `packages/replicache/src/db/rebase.ts` (rocicorp/mono) for the
|
|
204
|
+
* pattern.
|
|
205
|
+
*/
|
|
206
|
+
getOriginalSnapshot() {
|
|
207
|
+
return this._originalData;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Clear tracked changes
|
|
211
|
+
*/
|
|
212
|
+
clearChanges() {
|
|
213
|
+
runInAction(() => {
|
|
214
|
+
this.modifiedProperties.clear();
|
|
215
|
+
this._originalData = this.captureSnapshot();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Validate model
|
|
220
|
+
*/
|
|
221
|
+
validate() {
|
|
222
|
+
if (this.isDisposed) {
|
|
223
|
+
throw new AbloValidationError('Cannot validate disposed model', {
|
|
224
|
+
code: 'model_disposed',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const errors = [];
|
|
228
|
+
const modelName = this.getModelName();
|
|
229
|
+
const properties = getActiveRegistry().getProperties(modelName);
|
|
230
|
+
if (properties) {
|
|
231
|
+
const json = this.toJSON();
|
|
232
|
+
for (const [propName, metadata] of properties) {
|
|
233
|
+
// Check required fields
|
|
234
|
+
if (!metadata.nullable && !metadata.optional) {
|
|
235
|
+
const value = json[propName];
|
|
236
|
+
if (value == null || value === '') {
|
|
237
|
+
errors.push(`${propName} is required`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Run custom validation rules
|
|
241
|
+
const rules = this.validationRules[propName];
|
|
242
|
+
if (rules) {
|
|
243
|
+
const value = json[propName];
|
|
244
|
+
for (const rule of rules) {
|
|
245
|
+
const error = rule(value);
|
|
246
|
+
if (error)
|
|
247
|
+
errors.push(error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Run model-specific validation
|
|
253
|
+
const customErrors = this.customValidate();
|
|
254
|
+
errors.push(...customErrors);
|
|
255
|
+
return errors;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Override for custom validation
|
|
259
|
+
*/
|
|
260
|
+
customValidate() {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Add validation rule
|
|
265
|
+
*/
|
|
266
|
+
addValidationRule(propName, rule) {
|
|
267
|
+
if (!this.validationRules[propName]) {
|
|
268
|
+
this.validationRules[propName] = [];
|
|
269
|
+
}
|
|
270
|
+
this.validationRules[propName].push(rule);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Prepare save operation
|
|
274
|
+
* Returns the changes to be saved without side effects
|
|
275
|
+
*/
|
|
276
|
+
prepareSave() {
|
|
277
|
+
if (this.isDisposed) {
|
|
278
|
+
throw new AbloValidationError('Cannot prepare save for disposed model', {
|
|
279
|
+
code: 'model_disposed',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// Validate first
|
|
283
|
+
const errors = this.validate();
|
|
284
|
+
if (errors.length > 0) {
|
|
285
|
+
throw new ValidationError(errors);
|
|
286
|
+
}
|
|
287
|
+
if (this._isNew) {
|
|
288
|
+
// New model - return create operation
|
|
289
|
+
return {
|
|
290
|
+
type: 'create',
|
|
291
|
+
modelName: this.getModelName(), // Use Prisma model name
|
|
292
|
+
modelId: this.id,
|
|
293
|
+
timestamp: new Date(),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
else if (this.hasChanges) {
|
|
297
|
+
// Existing model with changes - return update operation
|
|
298
|
+
return {
|
|
299
|
+
type: 'update',
|
|
300
|
+
modelName: this.getModelName(), // Use Prisma model name
|
|
301
|
+
modelId: this.id,
|
|
302
|
+
changes: new Map(this.modifiedProperties),
|
|
303
|
+
timestamp: new Date(),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// No changes
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Prepare delete operation
|
|
311
|
+
*/
|
|
312
|
+
prepareDelete() {
|
|
313
|
+
if (this.isDisposed) {
|
|
314
|
+
throw new AbloValidationError('Cannot prepare delete for disposed model', {
|
|
315
|
+
code: 'model_disposed',
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
this.willDelete();
|
|
319
|
+
return {
|
|
320
|
+
type: 'delete',
|
|
321
|
+
modelName: this.getModelName(), // Use Prisma model name
|
|
322
|
+
modelId: this.id,
|
|
323
|
+
timestamp: new Date(),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Prepare archive operation
|
|
328
|
+
*/
|
|
329
|
+
prepareArchive() {
|
|
330
|
+
if (this.isDisposed) {
|
|
331
|
+
throw new AbloValidationError('Cannot prepare archive for disposed model', {
|
|
332
|
+
code: 'model_disposed',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
this.archivedAt = new Date();
|
|
336
|
+
return {
|
|
337
|
+
type: 'archive',
|
|
338
|
+
modelName: this.getModelName(), // Use Prisma model name
|
|
339
|
+
modelId: this.id,
|
|
340
|
+
timestamp: new Date(),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Prepare unarchive operation
|
|
345
|
+
*/
|
|
346
|
+
prepareUnarchive() {
|
|
347
|
+
if (this.isDisposed) {
|
|
348
|
+
throw new AbloValidationError('Cannot prepare unarchive for disposed model', {
|
|
349
|
+
code: 'model_disposed',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
this.archivedAt = null;
|
|
353
|
+
return {
|
|
354
|
+
type: 'unarchive',
|
|
355
|
+
modelName: this.getModelName(), // Use Prisma model name
|
|
356
|
+
modelId: this.id,
|
|
357
|
+
timestamp: new Date(),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Update from raw data (hydration)
|
|
362
|
+
*/
|
|
363
|
+
updateFromData(data) {
|
|
364
|
+
if (this.isDisposed) {
|
|
365
|
+
throw new AbloValidationError('Cannot update disposed model', {
|
|
366
|
+
code: 'model_disposed',
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
runInAction(() => {
|
|
370
|
+
// Temporarily disable change tracking
|
|
371
|
+
const originalTracking = this.modifiedProperties;
|
|
372
|
+
this.modifiedProperties = new Map();
|
|
373
|
+
// Update properties with safety checks for read-only/computed accessors
|
|
374
|
+
for (const [key, raw] of Object.entries(data)) {
|
|
375
|
+
if (key === 'id')
|
|
376
|
+
continue;
|
|
377
|
+
// Only attempt to set if the property exists on instance or prototype
|
|
378
|
+
if (!(this.hasOwnProperty(key) || key in this))
|
|
379
|
+
continue;
|
|
380
|
+
// Never assign to MobX computed properties (they may expose a setter that throws)
|
|
381
|
+
try {
|
|
382
|
+
if (isComputedProp(this, key)) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// If MobX internals are unavailable for some reason, fall back to descriptor checks below
|
|
388
|
+
}
|
|
389
|
+
// Resolve property descriptor from own or prototype chain
|
|
390
|
+
const ownDesc = Object.getOwnPropertyDescriptor(this, key);
|
|
391
|
+
let desc = ownDesc;
|
|
392
|
+
if (!desc) {
|
|
393
|
+
let proto = Object.getPrototypeOf(this);
|
|
394
|
+
while (proto && proto !== Object.prototype && !desc) {
|
|
395
|
+
desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
396
|
+
proto = Object.getPrototypeOf(proto);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Determine writability: allow if data descriptor writable, or accessor with setter
|
|
400
|
+
const writable = desc
|
|
401
|
+
? ('writable' in desc && !!desc.writable) ||
|
|
402
|
+
('set' in desc && typeof desc.set === 'function')
|
|
403
|
+
: true;
|
|
404
|
+
if (!writable) {
|
|
405
|
+
// Skip read-only accessor properties (getter-only)
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
// Handle date conversions
|
|
409
|
+
const value = (key === 'createdAt' || key === 'updatedAt' || key === 'archivedAt') && raw
|
|
410
|
+
? new Date(raw)
|
|
411
|
+
: raw;
|
|
412
|
+
// Dynamic property assignment for hydration - use indexed access
|
|
413
|
+
this[key] = value;
|
|
414
|
+
}
|
|
415
|
+
// Restore change tracking
|
|
416
|
+
this.modifiedProperties = originalTracking;
|
|
417
|
+
});
|
|
418
|
+
// Mark as persisted if updating existing model
|
|
419
|
+
if (!this._isNew) {
|
|
420
|
+
this._originalData = this.captureSnapshot();
|
|
421
|
+
}
|
|
422
|
+
this.didUpdate();
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Serialize to JSON
|
|
426
|
+
* This method should not trigger MobX reactions since it's used for serialization
|
|
427
|
+
* Returns Record<string, any> to allow subclass specialization with more specific return types
|
|
428
|
+
*/
|
|
429
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
430
|
+
toJSON() {
|
|
431
|
+
const modelName = this.getModelName();
|
|
432
|
+
const properties = getActiveRegistry().getProperties(modelName);
|
|
433
|
+
const result = {
|
|
434
|
+
__class: this.getModelName(), // Use Prisma model name for consistency
|
|
435
|
+
__typename: this.getModelName(), // Also add __typename for GraphQL compatibility
|
|
436
|
+
id: this.id,
|
|
437
|
+
createdAt: this.createdAt?.toISOString(),
|
|
438
|
+
updatedAt: this.updatedAt?.toISOString(),
|
|
439
|
+
clientId: this.clientId,
|
|
440
|
+
syncStatus: this.syncStatus,
|
|
441
|
+
};
|
|
442
|
+
if (this.archivedAt !== undefined) {
|
|
443
|
+
result.archivedAt = this.archivedAt?.toISOString() || null;
|
|
444
|
+
}
|
|
445
|
+
if (properties) {
|
|
446
|
+
const self = this;
|
|
447
|
+
for (const [propName, metadata] of properties) {
|
|
448
|
+
// Skip certain types
|
|
449
|
+
if (metadata.type === 'ephemeralProperty')
|
|
450
|
+
continue;
|
|
451
|
+
if (metadata.type === 'referenceModel')
|
|
452
|
+
continue;
|
|
453
|
+
if (metadata.type === 'referenceCollection')
|
|
454
|
+
continue;
|
|
455
|
+
const value = self[propName];
|
|
456
|
+
if (value !== undefined) {
|
|
457
|
+
result[propName] = value;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Clone this model
|
|
465
|
+
*/
|
|
466
|
+
clone() {
|
|
467
|
+
const Constructor = this.constructor;
|
|
468
|
+
const clone = new Constructor();
|
|
469
|
+
const data = this.toJSON();
|
|
470
|
+
delete data.id; // New ID for clone
|
|
471
|
+
delete data.createdAt;
|
|
472
|
+
delete data.updatedAt;
|
|
473
|
+
clone.updateFromData(data);
|
|
474
|
+
return clone;
|
|
475
|
+
}
|
|
476
|
+
getModelName() {
|
|
477
|
+
const registeredName = getActiveRegistry().getModelNameFromConstructor(this.constructor);
|
|
478
|
+
if (registeredName) {
|
|
479
|
+
return registeredName;
|
|
480
|
+
}
|
|
481
|
+
const className = this.constructor.name;
|
|
482
|
+
// Use consumer-provided fallback map from config (replaces hardcoded Prisma name map)
|
|
483
|
+
const fallbackMap = getContext().config.classNameFallbackMap;
|
|
484
|
+
return fallbackMap[className] || className.replace(/Model$/, '');
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Read a field value by name. Runtime-safe dynamic field access —
|
|
488
|
+
* schema-generated models store all declared fields as instance properties.
|
|
489
|
+
* Use this for generic code (sort comparators, filter predicates that work
|
|
490
|
+
* across model types) that reads fields by name string.
|
|
491
|
+
*/
|
|
492
|
+
getField(name) {
|
|
493
|
+
return Reflect.get(this, name);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Check equality
|
|
497
|
+
*/
|
|
498
|
+
equals(other) {
|
|
499
|
+
return this.id === other.id && this.constructor === other.constructor;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* String representation
|
|
503
|
+
*/
|
|
504
|
+
toString() {
|
|
505
|
+
return `${this.constructor.name}[${this.id}]`;
|
|
506
|
+
}
|
|
507
|
+
// ==========================================
|
|
508
|
+
// MobX Observation Tracking (for GC prevention)
|
|
509
|
+
// ==========================================
|
|
510
|
+
/**
|
|
511
|
+
* Register a LazyReferenceCollection as being observed
|
|
512
|
+
* Called by LazyReferenceCollection when onBecomeObserved fires
|
|
513
|
+
*/
|
|
514
|
+
_registerObservedCollection(collection) {
|
|
515
|
+
this._observedCollections.add(collection);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Unregister a LazyReferenceCollection that's no longer observed
|
|
519
|
+
* Called by LazyReferenceCollection when onBecomeUnobserved fires
|
|
520
|
+
*/
|
|
521
|
+
_unregisterObservedCollection(collection) {
|
|
522
|
+
this._observedCollections.delete(collection);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Check if any collection on this model is currently being observed by React
|
|
526
|
+
* Used by ObjectPool GC to prevent disposing models in active use
|
|
527
|
+
*/
|
|
528
|
+
hasObservedCollections() {
|
|
529
|
+
return this._observedCollections.size > 0;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get count of observed collections (for debugging)
|
|
533
|
+
*/
|
|
534
|
+
get observedCollectionCount() {
|
|
535
|
+
return this._observedCollections.size;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Dispose model
|
|
539
|
+
*/
|
|
540
|
+
dispose() {
|
|
541
|
+
if (this.isDisposed)
|
|
542
|
+
return;
|
|
543
|
+
// Clean up
|
|
544
|
+
for (const disposer of this.disposers) {
|
|
545
|
+
disposer();
|
|
546
|
+
}
|
|
547
|
+
this.disposers = [];
|
|
548
|
+
this._referencedModels = {};
|
|
549
|
+
this.modifiedProperties.clear();
|
|
550
|
+
this._observedCollections.clear();
|
|
551
|
+
// Dispose collections. Gracefully skip when no active registry
|
|
552
|
+
// exists — `dispose()` is a cleanup path and must not crash when a
|
|
553
|
+
// test (or a teardown during engine shutdown) calls it after the
|
|
554
|
+
// registry is gone. Production flows always have one set, so the
|
|
555
|
+
// collection-disposal branch still runs there.
|
|
556
|
+
if (hasActiveRegistry()) {
|
|
557
|
+
const modelName = this.getModelName();
|
|
558
|
+
const properties = getActiveRegistry().getProperties(modelName);
|
|
559
|
+
if (properties) {
|
|
560
|
+
const self = this;
|
|
561
|
+
for (const [propName, metadata] of properties) {
|
|
562
|
+
if (metadata.type === 'referenceCollection') {
|
|
563
|
+
const collection = self[propName];
|
|
564
|
+
if (collection?.dispose) {
|
|
565
|
+
collection.dispose();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
this.isDisposed = true;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Check if disposed
|
|
575
|
+
*/
|
|
576
|
+
get disposed() {
|
|
577
|
+
return this.isDisposed;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Lifecycle hooks - override in subclasses
|
|
581
|
+
*/
|
|
582
|
+
didUpdate() { }
|
|
583
|
+
willDelete() { }
|
|
584
|
+
/**
|
|
585
|
+
* Capture snapshot for change detection
|
|
586
|
+
*/
|
|
587
|
+
captureSnapshot() {
|
|
588
|
+
const snapshot = {};
|
|
589
|
+
const modelName = this.getModelName();
|
|
590
|
+
const properties = getActiveRegistry().getProperties(modelName);
|
|
591
|
+
if (properties) {
|
|
592
|
+
const json = this.toJSON();
|
|
593
|
+
for (const [propName] of properties) {
|
|
594
|
+
snapshot[propName] = json[propName];
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return snapshot;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Get field changes for activity tracking
|
|
601
|
+
*/
|
|
602
|
+
getFieldChanges() {
|
|
603
|
+
const changes = [];
|
|
604
|
+
for (const [field, change] of this.modifiedProperties) {
|
|
605
|
+
changes.push({
|
|
606
|
+
field,
|
|
607
|
+
oldValue: change.old,
|
|
608
|
+
newValue: change.new,
|
|
609
|
+
fieldType: this.getFieldType(change.new),
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return changes;
|
|
613
|
+
}
|
|
614
|
+
getFieldType(value) {
|
|
615
|
+
if (value === null || value === undefined)
|
|
616
|
+
return 'string';
|
|
617
|
+
if (typeof value === 'number')
|
|
618
|
+
return 'number';
|
|
619
|
+
if (value instanceof Date)
|
|
620
|
+
return 'date';
|
|
621
|
+
if (Array.isArray(value))
|
|
622
|
+
return 'array';
|
|
623
|
+
if (typeof value === 'string' && /^[a-fA-F0-9-]{36}$/.test(value))
|
|
624
|
+
return 'reference';
|
|
625
|
+
return 'string';
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Create model from JSON
|
|
629
|
+
*/
|
|
630
|
+
static fromJSON(data) {
|
|
631
|
+
// Support both __class and __typename, and handle both old and new naming
|
|
632
|
+
const modelIdentifier = data.__typename || data.__class || data.modelName;
|
|
633
|
+
if (!modelIdentifier) {
|
|
634
|
+
throw new AbloValidationError('Model identifier (__typename, __class, or modelName) not found in data', { code: 'model_identifier_missing' });
|
|
635
|
+
}
|
|
636
|
+
// Try to get model class by identifier
|
|
637
|
+
let ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
|
|
638
|
+
// If not found with Prisma name, try mapping to class name
|
|
639
|
+
if (!ModelClass) {
|
|
640
|
+
const classNameMap = {
|
|
641
|
+
Task: 'TaskModel',
|
|
642
|
+
Project: 'Project',
|
|
643
|
+
Comment: 'CommentModel',
|
|
644
|
+
User: 'UserModel',
|
|
645
|
+
Organization: 'OrganizationModel',
|
|
646
|
+
StatusGroup: 'StatusGroupModel',
|
|
647
|
+
Team: 'TeamModel',
|
|
648
|
+
Member: 'MemberModel',
|
|
649
|
+
Role: 'RoleModel',
|
|
650
|
+
};
|
|
651
|
+
const className = classNameMap[modelIdentifier];
|
|
652
|
+
if (className) {
|
|
653
|
+
ModelClass = getActiveRegistry().getModelByName(className);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (!ModelClass) {
|
|
657
|
+
throw new AbloValidationError(`Model class not found for: ${modelIdentifier}`, { code: 'model_class_not_registered' });
|
|
658
|
+
}
|
|
659
|
+
const instance = new ModelClass(data);
|
|
660
|
+
instance.markAsPersisted();
|
|
661
|
+
return instance;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Get sync status
|
|
665
|
+
*/
|
|
666
|
+
getSyncStatus() {
|
|
667
|
+
return this.syncStatus;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Mark model as synced
|
|
671
|
+
*/
|
|
672
|
+
markAsSynced() {
|
|
673
|
+
this.syncStatus = 'synced';
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Mark model as pending sync
|
|
677
|
+
*/
|
|
678
|
+
markAsPending() {
|
|
679
|
+
this.syncStatus = 'pending';
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Project a dynamic-class `Model` instance to the schema row shape `T`.
|
|
684
|
+
*
|
|
685
|
+
* The runtime invariant: `createDynamicModelClass(...)` attaches every
|
|
686
|
+
* field of `T` directly onto the Model prototype/instance via
|
|
687
|
+
* `Object.defineProperty` and the M1 observable bridge, so a Model
|
|
688
|
+
* instance structurally satisfies `T` at runtime. The static type
|
|
689
|
+
* system can't see this because `T` is a free generic — there's no
|
|
690
|
+
* common ancestor between `Model` (base class) and the schema row
|
|
691
|
+
* interface produced by `defineSchema`.
|
|
692
|
+
*
|
|
693
|
+
* This is a typed boundary, not a bypass: every call site is the
|
|
694
|
+
* dynamic-class duality where Model-with-extras-and-T-fields is being
|
|
695
|
+
* returned to a consumer that only sees `T`. Concentrating the cast
|
|
696
|
+
* here means there's one place to look when the boundary changes.
|
|
697
|
+
*/
|
|
698
|
+
export function modelAsRow(model) {
|
|
699
|
+
return model;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Inverse of `modelAsRow`: accept a row-shaped value (schema-derived
|
|
703
|
+
* `T` with at minimum an `id`) and surface it as a `Model`. Used by
|
|
704
|
+
* `BaseSyncedStore.save / delete / archive / unarchive` so consumers
|
|
705
|
+
* can pass either a typed schema row or a Model instance and the
|
|
706
|
+
* SDK's persistence path sees a uniform Model surface.
|
|
707
|
+
*
|
|
708
|
+
* Same runtime invariant as `modelAsRow`: dynamic-class instances
|
|
709
|
+
* carry both the row fields and the Model methods on the same object
|
|
710
|
+
* — one structural identity, two static views. The helper does no
|
|
711
|
+
* runtime conversion (no allocation, no copy) — it's a pure type cast.
|
|
712
|
+
*/
|
|
713
|
+
export function rowAsModel(entity) {
|
|
714
|
+
return entity;
|
|
715
|
+
}
|