@abloatai/ablo 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +3 -3
  4. package/dist/BaseSyncedStore.d.ts +72 -195
  5. package/dist/BaseSyncedStore.js +240 -625
  6. package/dist/Database.d.ts +8 -9
  7. package/dist/Database.js +9 -14
  8. package/dist/LazyReferenceCollection.js +5 -0
  9. package/dist/Model.js +6 -8
  10. package/dist/ModelRegistry.d.ts +25 -6
  11. package/dist/ModelRegistry.js +12 -6
  12. package/dist/NetworkMonitor.js +1 -1
  13. package/dist/ObjectPool.js +9 -3
  14. package/dist/SyncClient.d.ts +15 -5
  15. package/dist/SyncClient.js +152 -66
  16. package/dist/SyncEngineContext.d.ts +1 -3
  17. package/dist/SyncEngineContext.js +1 -2
  18. package/dist/agent/Agent.d.ts +21 -8
  19. package/dist/agent/Agent.js +26 -13
  20. package/dist/ai-sdk/coordination-context.js +9 -3
  21. package/dist/ai-sdk/wrap.d.ts +1 -1
  22. package/dist/ai-sdk/wrap.js +1 -1
  23. package/dist/auth/credentialPolicy.d.ts +10 -2
  24. package/dist/auth/credentialSource.d.ts +1 -13
  25. package/dist/auth/credentialSource.js +6 -13
  26. package/dist/auth/index.js +3 -3
  27. package/dist/cli.cjs +328 -262
  28. package/dist/client/Ablo.d.ts +35 -764
  29. package/dist/client/Ablo.js +64 -688
  30. package/dist/client/ApiClient.d.ts +14 -1
  31. package/dist/client/ApiClient.js +63 -9
  32. package/dist/client/auth.d.ts +8 -17
  33. package/dist/client/auth.js +42 -6
  34. package/dist/client/consoleLogger.d.ts +36 -0
  35. package/dist/client/consoleLogger.js +45 -0
  36. package/dist/client/createInternalComponents.js +1 -1
  37. package/dist/client/createModelProxy.js +10 -6
  38. package/dist/client/credentialEndpoint.d.ts +63 -0
  39. package/dist/client/credentialEndpoint.js +87 -0
  40. package/dist/client/hostedEndpoints.d.ts +24 -0
  41. package/dist/client/hostedEndpoints.js +24 -0
  42. package/dist/client/httpClient.d.ts +1 -1
  43. package/dist/client/identity.js +2 -1
  44. package/dist/client/modelRegistration.d.ts +14 -0
  45. package/dist/client/modelRegistration.js +321 -0
  46. package/dist/client/options.d.ts +396 -0
  47. package/dist/client/options.js +10 -0
  48. package/dist/client/resourceTypes.d.ts +344 -0
  49. package/dist/client/resourceTypes.js +13 -0
  50. package/dist/client/schemaConfig.d.ts +56 -0
  51. package/dist/client/schemaConfig.js +188 -0
  52. package/dist/client/sessionMint.d.ts +1 -1
  53. package/dist/client/validateAbloOptions.js +1 -1
  54. package/dist/client/wsMutationExecutor.d.ts +33 -0
  55. package/dist/client/wsMutationExecutor.js +77 -0
  56. package/dist/context.js +0 -3
  57. package/dist/coordination/index.d.ts +8 -1
  58. package/dist/coordination/index.js +24 -1
  59. package/dist/coordination/trace.d.ts +0 -1
  60. package/dist/coordination/trace.js +0 -1
  61. package/dist/core/DatabaseManager.d.ts +0 -1
  62. package/dist/core/DatabaseManager.js +26 -24
  63. package/dist/core/QueryProcessor.js +7 -8
  64. package/dist/core/QueryView.d.ts +14 -2
  65. package/dist/core/QueryView.js +4 -5
  66. package/dist/core/StoreManager.d.ts +2 -2
  67. package/dist/core/StoreManager.js +5 -1
  68. package/dist/core/index.d.ts +1 -1
  69. package/dist/core/openIDBWithTimeout.js +15 -11
  70. package/dist/core/query-utils.js +1 -1
  71. package/dist/core/storeContract.d.ts +143 -0
  72. package/dist/core/storeContract.js +16 -0
  73. package/dist/errorCodes.d.ts +11 -2
  74. package/dist/errorCodes.js +155 -137
  75. package/dist/errors.d.ts +18 -7
  76. package/dist/errors.js +14 -1
  77. package/dist/index.js +5 -0
  78. package/dist/interfaces/index.d.ts +6 -20
  79. package/dist/keys/index.js +18 -9
  80. package/dist/mutators/UndoManager.js +6 -1
  81. package/dist/mutators/defineMutators.d.ts +1 -3
  82. package/dist/policy/types.d.ts +1 -1
  83. package/dist/query/client.d.ts +11 -0
  84. package/dist/query/client.js +81 -52
  85. package/dist/react/AbloProvider.d.ts +6 -10
  86. package/dist/react/AbloProvider.js +22 -15
  87. package/dist/react/context.d.ts +2 -118
  88. package/dist/react/useAblo.js +1 -1
  89. package/dist/react/useErrorListener.js +1 -1
  90. package/dist/react/useMutationFailureListener.js +1 -1
  91. package/dist/react/useMutators.js +7 -1
  92. package/dist/react/useReactive.js +0 -1
  93. package/dist/react/useUndoScope.js +2 -2
  94. package/dist/schema/ddlLock.d.ts +39 -0
  95. package/dist/schema/ddlLock.js +51 -0
  96. package/dist/schema/field.js +1 -1
  97. package/dist/schema/index.d.ts +4 -3
  98. package/dist/schema/index.js +15 -5
  99. package/dist/schema/model.d.ts +3 -3
  100. package/dist/schema/model.js +2 -2
  101. package/dist/schema/openapi.js +5 -1
  102. package/dist/schema/queries.d.ts +1 -1
  103. package/dist/schema/queries.js +1 -0
  104. package/dist/schema/residency.d.ts +35 -0
  105. package/dist/schema/residency.js +27 -0
  106. package/dist/schema/roles.d.ts +25 -0
  107. package/dist/schema/roles.js +37 -0
  108. package/dist/schema/schema.d.ts +3 -3
  109. package/dist/schema/schema.js +11 -7
  110. package/dist/schema/serialize.d.ts +8 -8
  111. package/dist/schema/sync-delta-row.d.ts +2 -2
  112. package/dist/schema/sync-delta-row.js +2 -2
  113. package/dist/server/storage-mode.d.ts +8 -1
  114. package/dist/server/storage-mode.js +14 -4
  115. package/dist/source/adapter.d.ts +1 -1
  116. package/dist/source/adapters/drizzle.js +4 -3
  117. package/dist/source/adapters/kysely.js +4 -3
  118. package/dist/source/adapters/memory.js +1 -1
  119. package/dist/source/adapters/prisma.js +5 -4
  120. package/dist/source/conformance.js +10 -8
  121. package/dist/source/connector.js +4 -2
  122. package/dist/source/factory.d.ts +106 -0
  123. package/dist/source/factory.js +273 -0
  124. package/dist/source/index.d.ts +11 -461
  125. package/dist/source/index.js +14 -416
  126. package/dist/source/next.d.ts +1 -1
  127. package/dist/source/next.js +1 -1
  128. package/dist/source/pushQueue.d.ts +1 -1
  129. package/dist/source/pushQueue.js +6 -3
  130. package/dist/source/signing.d.ts +59 -0
  131. package/dist/source/signing.js +142 -0
  132. package/dist/source/types.d.ts +334 -0
  133. package/dist/source/types.js +44 -0
  134. package/dist/stores/ObjectStore.d.ts +1 -1
  135. package/dist/stores/ObjectStore.js +23 -23
  136. package/dist/stores/SyncActionStore.d.ts +1 -1
  137. package/dist/stores/SyncActionStore.js +70 -35
  138. package/dist/surface.d.ts +1 -1
  139. package/dist/surface.js +1 -0
  140. package/dist/sync/AreaOfInterestManager.d.ts +2 -2
  141. package/dist/sync/AreaOfInterestManager.js +1 -1
  142. package/dist/sync/BootstrapHelper.d.ts +1 -3
  143. package/dist/sync/BootstrapHelper.js +29 -8
  144. package/dist/sync/ConnectionManager.d.ts +8 -0
  145. package/dist/sync/ConnectionManager.js +19 -4
  146. package/dist/sync/HydrationCoordinator.d.ts +12 -0
  147. package/dist/sync/HydrationCoordinator.js +25 -5
  148. package/dist/sync/NetworkProbe.d.ts +3 -2
  149. package/dist/sync/NetworkProbe.js +7 -6
  150. package/dist/sync/SyncWebSocket.d.ts +71 -120
  151. package/dist/sync/SyncWebSocket.js +324 -630
  152. package/dist/sync/awaitClaimGrant.js +27 -19
  153. package/dist/sync/bootstrapApply.d.ts +60 -0
  154. package/dist/sync/bootstrapApply.js +65 -0
  155. package/dist/sync/commitFrames.d.ts +43 -0
  156. package/dist/sync/commitFrames.js +94 -0
  157. package/dist/sync/createClaimStream.js +3 -2
  158. package/dist/sync/createSnapshot.d.ts +1 -3
  159. package/dist/sync/credentialLifecycle.d.ts +175 -0
  160. package/dist/sync/credentialLifecycle.js +329 -0
  161. package/dist/sync/deltaPipeline.d.ts +111 -0
  162. package/dist/sync/deltaPipeline.js +262 -0
  163. package/dist/sync/groupChange.d.ts +111 -0
  164. package/dist/sync/groupChange.js +246 -0
  165. package/dist/sync/heartbeat.d.ts +62 -0
  166. package/dist/sync/heartbeat.js +91 -0
  167. package/dist/sync/participants.d.ts +8 -8
  168. package/dist/sync/syncCursor.d.ts +44 -0
  169. package/dist/sync/syncCursor.js +59 -0
  170. package/dist/sync/syncPlan.d.ts +62 -0
  171. package/dist/sync/syncPlan.js +51 -0
  172. package/dist/sync/wsFrameHandlers.d.ts +117 -0
  173. package/dist/sync/wsFrameHandlers.js +378 -0
  174. package/dist/testing/fixtures/bootstrap.d.ts +9 -11
  175. package/dist/testing/fixtures/deltas.d.ts +1 -1
  176. package/dist/testing/fixtures/models.js +1 -0
  177. package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
  178. package/dist/testing/helpers/sync-engine-harness.js +2 -1
  179. package/dist/testing/index.d.ts +1 -1
  180. package/dist/testing/index.js +1 -1
  181. package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
  182. package/dist/testing/mocks/MockSyncContext.js +1 -32
  183. package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
  184. package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
  185. package/dist/transactions/TransactionQueue.d.ts +20 -56
  186. package/dist/transactions/TransactionQueue.js +101 -487
  187. package/dist/transactions/TransactionStore.d.ts +18 -0
  188. package/dist/transactions/TransactionStore.js +51 -0
  189. package/dist/transactions/coalesceRules.d.ts +34 -0
  190. package/dist/transactions/coalesceRules.js +117 -0
  191. package/dist/transactions/commitPayload.d.ts +134 -0
  192. package/dist/transactions/commitPayload.js +152 -0
  193. package/dist/transactions/deltaConfirmation.d.ts +60 -0
  194. package/dist/transactions/deltaConfirmation.js +223 -0
  195. package/dist/transactions/optimistic.d.ts +24 -0
  196. package/dist/transactions/optimistic.js +45 -0
  197. package/dist/transactions/persistedReplay.d.ts +93 -0
  198. package/dist/transactions/persistedReplay.js +105 -0
  199. package/dist/types/index.d.ts +3 -3
  200. package/dist/types/index.js +0 -5
  201. package/dist/types/modelData.d.ts +12 -0
  202. package/dist/types/modelData.js +11 -0
  203. package/dist/types/participant.d.ts +21 -0
  204. package/dist/types/participant.js +12 -0
  205. package/dist/types/streams.d.ts +8 -14
  206. package/dist/utils/mobx-setup.d.ts +1 -1
  207. package/dist/utils/mobx-setup.js +2 -2
  208. package/dist/wire/errorEnvelope.d.ts +11 -2
  209. package/dist/wire/errorEnvelope.js +14 -2
  210. package/dist/wire/frames.d.ts +90 -0
  211. package/dist/wire/frames.js +55 -1
  212. package/dist/wire/index.d.ts +4 -1
  213. package/dist/wire/index.js +20 -1
  214. package/dist/wire/protocol.d.ts +45 -0
  215. package/dist/wire/protocol.js +45 -0
  216. package/dist/wire/protocolVersion.d.ts +56 -0
  217. package/dist/wire/protocolVersion.js +63 -0
  218. package/docs/api-keys.md +4 -3
  219. package/docs/examples/existing-python-backend.md +3 -3
  220. package/docs/identity.md +4 -4
  221. package/docs/integration-guide.md +1 -1
  222. package/docs/react.md +1 -1
  223. package/docs/sessions.md +5 -7
  224. package/package.json +15 -13
  225. package/dist/client/index.d.ts +0 -36
  226. package/dist/client/index.js +0 -33
  227. package/dist/config/index.d.ts +0 -10
  228. package/dist/config/index.js +0 -12
  229. package/dist/interfaces/headless.d.ts +0 -95
  230. package/dist/interfaces/headless.js +0 -41
  231. package/dist/query/index.d.ts +0 -6
  232. package/dist/query/index.js +0 -5
  233. package/dist/realtime/index.d.ts +0 -10
  234. package/dist/realtime/index.js +0 -9
  235. package/dist/schema/plane.d.ts +0 -23
  236. package/dist/schema/plane.js +0 -19
  237. package/dist/server/next.d.ts +0 -51
  238. package/dist/server/next.js +0 -47
  239. package/dist/sync/OfflineFlush.d.ts +0 -9
  240. package/dist/sync/OfflineFlush.js +0 -22
  241. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  242. package/dist/sync/OfflineTransactionStore.js +0 -263
  243. package/dist/transactions/index.d.ts +0 -16
  244. package/dist/transactions/index.js +0 -7
@@ -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
+ }