@abloatai/ablo 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +33 -0
- package/README.md +45 -36
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +583 -287
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/data-sources.md +12 -5
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/llms.txt +4 -2
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema → Model-class registration — `registerModelsFromSchema` walks the
|
|
3
|
+
* declarative schema and populates the `ModelRegistry`: a dynamic `Model`
|
|
4
|
+
* subclass per model (`createDynamicModelClass`, with `${field}Json` getters,
|
|
5
|
+
* computed getters, and opt-in MobX field observability), property/relation
|
|
6
|
+
* registration, and IDB index selection.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from `Ablo.ts` — the factory calls `registerModelsFromSchema`
|
|
9
|
+
* right after `createInternalComponents` builds the registry. The zod unwrap
|
|
10
|
+
* helpers stay module-local.
|
|
11
|
+
*/
|
|
12
|
+
import type { Schema } from '../schema/schema.js';
|
|
13
|
+
import type { ModelRegistry } from '../ModelRegistry.js';
|
|
14
|
+
export declare function registerModelsFromSchema(schema: Schema, registry: ModelRegistry): void;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema → Model-class registration — `registerModelsFromSchema` walks the
|
|
3
|
+
* declarative schema and populates the `ModelRegistry`: a dynamic `Model`
|
|
4
|
+
* subclass per model (`createDynamicModelClass`, with `${field}Json` getters,
|
|
5
|
+
* computed getters, and opt-in MobX field observability), property/relation
|
|
6
|
+
* registration, and IDB index selection.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from `Ablo.ts` — the factory calls `registerModelsFromSchema`
|
|
9
|
+
* right after `createInternalComponents` builds the registry. The zod unwrap
|
|
10
|
+
* helpers stay module-local.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { baseFieldsSchema } from '../schema/schema.js';
|
|
14
|
+
import { Model } from '../Model.js';
|
|
15
|
+
import { LoadStrategy, PropertyType } from '../types/index.js';
|
|
16
|
+
// ── Auto model registration from schema ───────────────────────────────────
|
|
17
|
+
export function registerModelsFromSchema(schema, registry) {
|
|
18
|
+
registry.startBatch();
|
|
19
|
+
for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
|
|
20
|
+
// Use typename as the model name — this is the wire-format name that
|
|
21
|
+
// the server sends in bootstrap responses and sync deltas. The pool's
|
|
22
|
+
// typeIndex, the ModelRegistry, and getModelName() all use this name.
|
|
23
|
+
// Schema key (camelCase plural) is only for the consumer-facing proxy API.
|
|
24
|
+
const modelName = modelDef.typename ?? schemaKey;
|
|
25
|
+
// Collect JSON sub-property fields to generate ${field}Json getters
|
|
26
|
+
const jsonSubFields = [];
|
|
27
|
+
for (const [fieldName, zodType] of Object.entries(modelDef.shape)) {
|
|
28
|
+
const inner = unwrapZodType(zodType);
|
|
29
|
+
if (isZodObject(inner)) {
|
|
30
|
+
jsonSubFields.push({ fieldName, subSchema: inner });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Create a dynamic Model subclass with JSON sub-property getters.
|
|
34
|
+
//
|
|
35
|
+
// Field-level MobX observability is ON BY DEFAULT. A reactive read like
|
|
36
|
+
// `useAblo((a) => a.documents.get(id))` must re-render when a remote delta
|
|
37
|
+
// mutates the row IN PLACE (the common collaborative case); without
|
|
38
|
+
// per-field observability that update fires no reaction and the UI silently
|
|
39
|
+
// goes stale. Models opt OUT with an explicit `lazyObservable: false` —
|
|
40
|
+
// appropriate only for very large read-only list models where per-field
|
|
41
|
+
// atoms cost more than the QueryView's entry-replaced reactivity already
|
|
42
|
+
// provides. json fields register as `observable.ref` (see
|
|
43
|
+
// `registerModelsFromSchema`), so the default is ~one atom per scalar field
|
|
44
|
+
// per loaded row — cheap — not a deep atom tree per blob.
|
|
45
|
+
const isLazy = modelDef.lazyObservable !== false;
|
|
46
|
+
// Base provenance fields (`organizationId`, `createdBy`) live in
|
|
47
|
+
// `baseFieldsSchema`, not the per-model `shape`. The server stamps + emits
|
|
48
|
+
// them (camelCased on the wire), but hydration (`Model.assignFieldsFromData`)
|
|
49
|
+
// only assigns keys that already exist as an own/prototype property — so
|
|
50
|
+
// without a slot here, `deck.createdBy` / `deck.organizationId` silently read
|
|
51
|
+
// `undefined` (this is why the profile decks tab showed nothing: it filters
|
|
52
|
+
// `decks.filter(d => d.createdBy === userId)`). `id`/`createdAt`/`updatedAt`
|
|
53
|
+
// are already seeded by the base Model constructor, so they're excluded.
|
|
54
|
+
const fieldNames = [
|
|
55
|
+
...Object.keys(modelDef.shape),
|
|
56
|
+
...Object.keys(baseFieldsSchema.shape).filter((f) => f !== 'id' && f !== 'createdAt' && f !== 'updatedAt' && !(f in modelDef.shape)),
|
|
57
|
+
];
|
|
58
|
+
const computed = modelDef.computed;
|
|
59
|
+
const DynamicModel = createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, isLazy);
|
|
60
|
+
// Respect the schema's load strategy so lazy models skip IDB hydration + bootstrap
|
|
61
|
+
const loadStrategy = modelDef.load === 'lazy' || modelDef.load === 'manual'
|
|
62
|
+
? LoadStrategy.lazy
|
|
63
|
+
: LoadStrategy.instant;
|
|
64
|
+
registry.registerModel(modelName, DynamicModel, {
|
|
65
|
+
loadStrategy,
|
|
66
|
+
fields: modelDef.fields,
|
|
67
|
+
autoFill: modelDef.autoFill,
|
|
68
|
+
requiredFields: modelDef.requiredFields,
|
|
69
|
+
});
|
|
70
|
+
// Collect the set of fields that should get an IDB secondary index.
|
|
71
|
+
//
|
|
72
|
+
// Matches Linear's opt-in model (see wzhudev/reverse-linear-sync-engine):
|
|
73
|
+
// `@Reference(..., { indexed: true })`. Only `belongsTo` relations that
|
|
74
|
+
// explicitly set `{ index: true }` in their options get an IDB secondary
|
|
75
|
+
// index. Every other FK (and every scalar) is resolved via in-memory
|
|
76
|
+
// ObjectPool scans, which are fast enough at org-scope sizes (~10k rows)
|
|
77
|
+
// and reactive via MobX.
|
|
78
|
+
//
|
|
79
|
+
// Auto-indexing every belongsTo was wrong: it bloated write amplification
|
|
80
|
+
// for the vast majority of FKs that are never queried by fk. Indexing
|
|
81
|
+
// every scalar (like the legacy Go backend did) is even worse.
|
|
82
|
+
const indexedFields = new Set();
|
|
83
|
+
for (const relDef of Object.values(modelDef.relations)) {
|
|
84
|
+
if (relDef.type === 'belongsTo' && relDef.foreignKey && relDef.options?.index === true) {
|
|
85
|
+
indexedFields.add(relDef.foreignKey);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Register fields as properties (from Zod shape).
|
|
89
|
+
for (const [fieldName, rawZodType] of Object.entries(modelDef.shape)) {
|
|
90
|
+
const zodType = rawZodType;
|
|
91
|
+
const isOptional = zodType.isOptional?.() ?? false;
|
|
92
|
+
// A field is indexed if it's the FK of a `belongsTo({ index: true })`
|
|
93
|
+
// relation. Legacy `description === 'indexed'` still works for
|
|
94
|
+
// consumers using `field.*().indexed()`.
|
|
95
|
+
const isIndexed = indexedFields.has(fieldName) || zodType.description === 'indexed';
|
|
96
|
+
// JSON-typed fields (per the schema's wire-type tag) are opaque
|
|
97
|
+
// blobs from MobX's perspective — chart specs, ProseMirror docs,
|
|
98
|
+
// style maps. Deep observability on them recursively walks every
|
|
99
|
+
// nested property and creates an atom for each leaf, producing a
|
|
100
|
+
// microtask storm on every commit/streaming update. `ref` tracks
|
|
101
|
+
// only reassignment, which is how blob consumers actually use them.
|
|
102
|
+
const wireType = modelDef.fields?.[fieldName]?.type;
|
|
103
|
+
const observability = wireType === 'json' ? 'ref' : undefined;
|
|
104
|
+
registry.registerProperty(modelName, fieldName, {
|
|
105
|
+
type: PropertyType.property,
|
|
106
|
+
indexed: isIndexed,
|
|
107
|
+
optional: isOptional,
|
|
108
|
+
observability,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Register relations
|
|
112
|
+
for (const [relName, relDef] of Object.entries(modelDef.relations)) {
|
|
113
|
+
if (relDef.type === 'belongsTo') {
|
|
114
|
+
registry.registerReference(modelName, relName, {
|
|
115
|
+
referencedModel: () => {
|
|
116
|
+
const targetModel = registry.getModelByName(relDef.target);
|
|
117
|
+
return targetModel ?? DynamicModel;
|
|
118
|
+
},
|
|
119
|
+
indexed: true,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else if (relDef.type === 'hasMany') {
|
|
123
|
+
// Generate a getter on the parent model that returns all children
|
|
124
|
+
// matching the FK via Model.getStore().getByForeignKey(). The FK
|
|
125
|
+
// index on the target model is registered by deriveSyncPlanFromSchema.
|
|
126
|
+
const targetName = relDef.target;
|
|
127
|
+
const foreignKey = relDef.foreignKey;
|
|
128
|
+
const orderByField = relDef._orderBy;
|
|
129
|
+
// Resolve the target typename from the schema (might differ from the key)
|
|
130
|
+
const targetDef = schema.models[targetName];
|
|
131
|
+
const targetTypename = targetDef?.typename ?? targetName;
|
|
132
|
+
Object.defineProperty(DynamicModel.prototype, relName, {
|
|
133
|
+
get() {
|
|
134
|
+
const store = Model.getStore();
|
|
135
|
+
if (!store)
|
|
136
|
+
return [];
|
|
137
|
+
const results = store.getByForeignKey(targetTypename, foreignKey, this.id);
|
|
138
|
+
if (orderByField && results.length > 1) {
|
|
139
|
+
return [...results].sort((a, b) => {
|
|
140
|
+
// `orderByField` is a runtime string from the schema's
|
|
141
|
+
// hasMany({ orderBy }) — Models have dynamic typed
|
|
142
|
+
// fields produced by createDynamicModelClass, so the
|
|
143
|
+
// static type doesn't carry an index signature for
|
|
144
|
+
// arbitrary field reads. `Reflect.get` is the typed
|
|
145
|
+
// bridge — returns `unknown`, narrowed below.
|
|
146
|
+
const va = Reflect.get(a, orderByField);
|
|
147
|
+
const vb = Reflect.get(b, orderByField);
|
|
148
|
+
if (typeof va === 'number' && typeof vb === 'number')
|
|
149
|
+
return va - vb;
|
|
150
|
+
if (typeof va === 'string' && typeof vb === 'string')
|
|
151
|
+
return va.localeCompare(vb);
|
|
152
|
+
return 0;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return results;
|
|
156
|
+
},
|
|
157
|
+
enumerable: true,
|
|
158
|
+
configurable: true,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
registry.endBatch();
|
|
164
|
+
}
|
|
165
|
+
// ── JSON sub-property helpers ─────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Unwrap a Zod schema through .optional(), .nullable(), .default(),
|
|
168
|
+
* .readonly() to find the innermost type. Needed to detect whether a
|
|
169
|
+
* field.json() call wraps a ZodObject (has sub-properties) or a plain
|
|
170
|
+
* type (ZodUnknown, ZodArray, etc.).
|
|
171
|
+
*
|
|
172
|
+
* Uses Zod's public `.unwrap()` API per wrapper type — no `_def`
|
|
173
|
+
* digging. Bounded loop guards against pathological self-referential
|
|
174
|
+
* wrappers.
|
|
175
|
+
*/
|
|
176
|
+
function unwrapZodType(schema) {
|
|
177
|
+
let current = schema;
|
|
178
|
+
for (let i = 0; i < 10; i++) {
|
|
179
|
+
if (current instanceof z.ZodOptional) {
|
|
180
|
+
current = current.unwrap();
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (current instanceof z.ZodNullable) {
|
|
184
|
+
current = current.unwrap();
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (current instanceof z.ZodDefault) {
|
|
188
|
+
// v4 deprecates removeDefault in favor of unwrap, but the
|
|
189
|
+
// installed @types declarations only expose removeDefault on
|
|
190
|
+
// ZodDefault. Use it — it's the same runtime function.
|
|
191
|
+
current = current.unwrap();
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (current instanceof z.ZodReadonly) {
|
|
195
|
+
current = current.unwrap();
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
return current;
|
|
201
|
+
}
|
|
202
|
+
/** Type guard: is this a ZodObject with a .shape property? */
|
|
203
|
+
function isZodObject(schema) {
|
|
204
|
+
return schema instanceof z.ZodObject;
|
|
205
|
+
}
|
|
206
|
+
/** Create a Model subclass for a schema-defined model */
|
|
207
|
+
function createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, lazyObservable = false) {
|
|
208
|
+
const ModelClass = class extends Model {
|
|
209
|
+
_modelName = modelName;
|
|
210
|
+
constructor(data) {
|
|
211
|
+
super(data);
|
|
212
|
+
// Gate `propertyChanged`-via-`observe` tracking during initial
|
|
213
|
+
// hydration. M1 installs a MobX `observe()` listener per schema
|
|
214
|
+
// property that forwards writes to `propertyChanged()` so direct
|
|
215
|
+
// assignments like `layer.position = newPos` still round-trip
|
|
216
|
+
// through the transaction queue. During construction we're writing
|
|
217
|
+
// wire data, NOT user edits — flagging this as "constructing" lets
|
|
218
|
+
// the listener early-return on those writes so `modifiedProperties`
|
|
219
|
+
// doesn't get polluted with every field of every hydrated model.
|
|
220
|
+
//
|
|
221
|
+
// The listener is installed by `makeObservable()` below (inside
|
|
222
|
+
// M1), so writes that happen BEFORE that line won't fire it; this
|
|
223
|
+
// flag is defensive in case a subclass or call path reorders the
|
|
224
|
+
// steps later.
|
|
225
|
+
this._isConstructing = true;
|
|
226
|
+
// MobX 6 requires fields to exist as own properties BEFORE makeObservable().
|
|
227
|
+
// Model base only sets id/createdAt/updatedAt. Schema fields (title, userId, etc.)
|
|
228
|
+
// must be initialized here so M1's annotations can find them.
|
|
229
|
+
for (const field of fieldNames) {
|
|
230
|
+
if (!(field in this)) {
|
|
231
|
+
this[field] = data?.[field] ?? undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Per-field MobX observability opt-in via `lazyObservable: true` on
|
|
235
|
+
// the model definition. Defaults to plain objects — reactivity comes
|
|
236
|
+
// from the QueryView "entry replaced" pattern, which is cheap for
|
|
237
|
+
// read-only list UIs but invisible to in-place field mutations.
|
|
238
|
+
//
|
|
239
|
+
// Multiplayer editors need live field-level reactivity so remote
|
|
240
|
+
// deltas AND local drag/resize/rename mutations surface through
|
|
241
|
+
// `observer()` components without the whole pool entry being
|
|
242
|
+
// replaced. Without observability, `layer.position.x = 500` emits
|
|
243
|
+
// nothing and the UI lags until some unrelated state change triggers
|
|
244
|
+
// a pass (toolbar close, deselect).
|
|
245
|
+
//
|
|
246
|
+
// Delegates to `Model.makeObservable()` (the inherited method) so
|
|
247
|
+
// MobX annotations are derived from the same registry that M1 reads.
|
|
248
|
+
// That means computed getters, reference collections, custom
|
|
249
|
+
// getters/setters, and property-change tracking all integrate
|
|
250
|
+
// correctly — reimplementing `makeObservable` inline here would miss
|
|
251
|
+
// those seams.
|
|
252
|
+
if (lazyObservable) {
|
|
253
|
+
this.makeObservable();
|
|
254
|
+
}
|
|
255
|
+
this._isConstructing = false;
|
|
256
|
+
}
|
|
257
|
+
getModelName() {
|
|
258
|
+
return this._modelName;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
// Generate ${field}Json getters for JSON fields with sub-properties.
|
|
262
|
+
//
|
|
263
|
+
// The getter reads the raw JSON string from the instance (set via
|
|
264
|
+
// updateFromData), parses it, applies Zod defaults, and caches by
|
|
265
|
+
// raw value. This replaces the hand-coded metadataObject + sub-property
|
|
266
|
+
// getter pattern that 11+ Ablo models currently repeat.
|
|
267
|
+
//
|
|
268
|
+
// Example: field named 'metadata' with sub-schema { icon: z.string().default('presentation') }
|
|
269
|
+
// → model.metadataJson returns { icon: 'presentation', ... } (typed, cached)
|
|
270
|
+
for (const { fieldName, subSchema } of jsonSubFields) {
|
|
271
|
+
const getterName = `${fieldName}Json`;
|
|
272
|
+
const cacheKey = `__${fieldName}JsonCache`;
|
|
273
|
+
Object.defineProperty(ModelClass.prototype, getterName, {
|
|
274
|
+
get() {
|
|
275
|
+
const raw = this[fieldName];
|
|
276
|
+
// Cache check: same raw value → same parsed result
|
|
277
|
+
const cache = this[cacheKey];
|
|
278
|
+
if (cache && cache.raw === raw)
|
|
279
|
+
return cache.parsed;
|
|
280
|
+
// Parse: handle string (from DB/wire), object (already parsed), null/undefined
|
|
281
|
+
let input;
|
|
282
|
+
try {
|
|
283
|
+
if (typeof raw === 'string') {
|
|
284
|
+
input = JSON.parse(raw);
|
|
285
|
+
}
|
|
286
|
+
else if (raw && typeof raw === 'object') {
|
|
287
|
+
input = raw;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
input = {};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
input = {};
|
|
295
|
+
}
|
|
296
|
+
// Apply Zod parse for type coercion + defaults. safeParse so
|
|
297
|
+
// malformed metadata doesn't crash — falls back to all defaults.
|
|
298
|
+
const result = subSchema.safeParse(input);
|
|
299
|
+
const parsed = result.success ? result.data : subSchema.safeParse({}).data ?? {};
|
|
300
|
+
this[cacheKey] = { raw, parsed };
|
|
301
|
+
return parsed;
|
|
302
|
+
},
|
|
303
|
+
enumerable: true,
|
|
304
|
+
configurable: true,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// Install schema-declared computed getters on the prototype.
|
|
308
|
+
// Each getter receives `this` (the model instance) and returns the computed value.
|
|
309
|
+
if (computed) {
|
|
310
|
+
for (const [name, fn] of Object.entries(computed)) {
|
|
311
|
+
Object.defineProperty(ModelClass.prototype, name, {
|
|
312
|
+
get() {
|
|
313
|
+
return fn(this);
|
|
314
|
+
},
|
|
315
|
+
enumerable: true,
|
|
316
|
+
configurable: true,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return ModelClass;
|
|
321
|
+
}
|