@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.
Files changed (278) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +12 -0
  4. package/README.md +230 -0
  5. package/dist/BaseSyncedStore.d.ts +709 -0
  6. package/dist/BaseSyncedStore.js +1843 -0
  7. package/dist/Database.d.ts +344 -0
  8. package/dist/Database.js +1259 -0
  9. package/dist/LazyReferenceCollection.d.ts +181 -0
  10. package/dist/LazyReferenceCollection.js +460 -0
  11. package/dist/Model.d.ts +339 -0
  12. package/dist/Model.js +715 -0
  13. package/dist/ModelRegistry.d.ts +200 -0
  14. package/dist/ModelRegistry.js +535 -0
  15. package/dist/NetworkMonitor.d.ts +27 -0
  16. package/dist/NetworkMonitor.js +73 -0
  17. package/dist/ObjectPool.d.ts +202 -0
  18. package/dist/ObjectPool.js +1106 -0
  19. package/dist/SyncClient.d.ts +489 -0
  20. package/dist/SyncClient.js +1555 -0
  21. package/dist/SyncEngineContext.d.ts +46 -0
  22. package/dist/SyncEngineContext.js +74 -0
  23. package/dist/adapters/alwaysOnline.d.ts +16 -0
  24. package/dist/adapters/alwaysOnline.js +19 -0
  25. package/dist/adapters/inMemoryStorage.d.ts +30 -0
  26. package/dist/adapters/inMemoryStorage.js +94 -0
  27. package/dist/agent/Agent.d.ts +358 -0
  28. package/dist/agent/Agent.js +500 -0
  29. package/dist/agent/index.d.ts +115 -0
  30. package/dist/agent/index.js +128 -0
  31. package/dist/agent/session.d.ts +90 -0
  32. package/dist/agent/session.js +156 -0
  33. package/dist/agent/types.d.ts +73 -0
  34. package/dist/agent/types.js +10 -0
  35. package/dist/ai-sdk/coordination-context.d.ts +51 -0
  36. package/dist/ai-sdk/coordination-context.js +107 -0
  37. package/dist/ai-sdk/index.d.ts +68 -0
  38. package/dist/ai-sdk/index.js +68 -0
  39. package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
  40. package/dist/ai-sdk/intent-broadcast.js +72 -0
  41. package/dist/ai-sdk/wrap.d.ts +67 -0
  42. package/dist/ai-sdk/wrap.js +45 -0
  43. package/dist/api/index.d.ts +10 -0
  44. package/dist/api/index.js +9 -0
  45. package/dist/auth/index.d.ts +137 -0
  46. package/dist/auth/index.js +246 -0
  47. package/dist/client/Ablo.d.ts +835 -0
  48. package/dist/client/Ablo.js +1440 -0
  49. package/dist/client/ApiClient.d.ts +200 -0
  50. package/dist/client/ApiClient.js +659 -0
  51. package/dist/client/auth.d.ts +79 -0
  52. package/dist/client/auth.js +81 -0
  53. package/dist/client/createInternalComponents.d.ts +44 -0
  54. package/dist/client/createInternalComponents.js +88 -0
  55. package/dist/client/createModelProxy.d.ts +152 -0
  56. package/dist/client/createModelProxy.js +199 -0
  57. package/dist/client/identity.d.ts +63 -0
  58. package/dist/client/identity.js +156 -0
  59. package/dist/client/index.d.ts +36 -0
  60. package/dist/client/index.js +33 -0
  61. package/dist/client/persistence.d.ts +7 -0
  62. package/dist/client/persistence.js +11 -0
  63. package/dist/client/validateAbloOptions.d.ts +42 -0
  64. package/dist/client/validateAbloOptions.js +43 -0
  65. package/dist/config/index.d.ts +10 -0
  66. package/dist/config/index.js +12 -0
  67. package/dist/context.d.ts +27 -0
  68. package/dist/context.js +58 -0
  69. package/dist/core/DatabaseManager.d.ts +108 -0
  70. package/dist/core/DatabaseManager.js +361 -0
  71. package/dist/core/QueryProcessor.d.ts +77 -0
  72. package/dist/core/QueryProcessor.js +262 -0
  73. package/dist/core/QueryView.d.ts +64 -0
  74. package/dist/core/QueryView.js +219 -0
  75. package/dist/core/StoreManager.d.ts +131 -0
  76. package/dist/core/StoreManager.js +334 -0
  77. package/dist/core/ViewRegistry.d.ts +20 -0
  78. package/dist/core/ViewRegistry.js +55 -0
  79. package/dist/core/index.d.ts +34 -0
  80. package/dist/core/index.js +59 -0
  81. package/dist/core/openIDBWithTimeout.d.ts +27 -0
  82. package/dist/core/openIDBWithTimeout.js +63 -0
  83. package/dist/core/query-utils.d.ts +37 -0
  84. package/dist/core/query-utils.js +60 -0
  85. package/dist/errors.d.ts +235 -0
  86. package/dist/errors.js +243 -0
  87. package/dist/index.d.ts +41 -0
  88. package/dist/index.js +82 -0
  89. package/dist/interfaces/headless.d.ts +95 -0
  90. package/dist/interfaces/headless.js +41 -0
  91. package/dist/interfaces/index.d.ts +321 -0
  92. package/dist/interfaces/index.js +8 -0
  93. package/dist/mutators/RecordingTransaction.d.ts +36 -0
  94. package/dist/mutators/RecordingTransaction.js +216 -0
  95. package/dist/mutators/Transaction.d.ts +48 -0
  96. package/dist/mutators/Transaction.js +64 -0
  97. package/dist/mutators/UndoManager.d.ts +114 -0
  98. package/dist/mutators/UndoManager.js +143 -0
  99. package/dist/mutators/defineMutators.d.ts +55 -0
  100. package/dist/mutators/defineMutators.js +28 -0
  101. package/dist/policy/index.d.ts +19 -0
  102. package/dist/policy/index.js +18 -0
  103. package/dist/policy/types.d.ts +74 -0
  104. package/dist/policy/types.js +17 -0
  105. package/dist/principal.d.ts +44 -0
  106. package/dist/principal.js +49 -0
  107. package/dist/query/client.d.ts +43 -0
  108. package/dist/query/client.js +84 -0
  109. package/dist/query/index.d.ts +6 -0
  110. package/dist/query/index.js +5 -0
  111. package/dist/query/types.d.ts +143 -0
  112. package/dist/query/types.js +36 -0
  113. package/dist/react/AbloProvider.d.ts +205 -0
  114. package/dist/react/AbloProvider.js +398 -0
  115. package/dist/react/ClientSideSuspense.d.ts +36 -0
  116. package/dist/react/ClientSideSuspense.js +17 -0
  117. package/dist/react/DefaultFallback.d.ts +24 -0
  118. package/dist/react/DefaultFallback.js +43 -0
  119. package/dist/react/SyncGroupProvider.d.ts +19 -0
  120. package/dist/react/SyncGroupProvider.js +44 -0
  121. package/dist/react/context.d.ts +161 -0
  122. package/dist/react/context.js +35 -0
  123. package/dist/react/index.d.ts +64 -0
  124. package/dist/react/index.js +73 -0
  125. package/dist/react/internalContext.d.ts +35 -0
  126. package/dist/react/internalContext.js +3 -0
  127. package/dist/react/useAblo.d.ts +72 -0
  128. package/dist/react/useAblo.js +63 -0
  129. package/dist/react/useCurrentUserId.d.ts +21 -0
  130. package/dist/react/useCurrentUserId.js +33 -0
  131. package/dist/react/useErrorListener.d.ts +20 -0
  132. package/dist/react/useErrorListener.js +39 -0
  133. package/dist/react/useIntent.d.ts +29 -0
  134. package/dist/react/useIntent.js +42 -0
  135. package/dist/react/useMutate.d.ts +83 -0
  136. package/dist/react/useMutate.js +122 -0
  137. package/dist/react/useMutationFailureListener.d.ts +26 -0
  138. package/dist/react/useMutationFailureListener.js +38 -0
  139. package/dist/react/useMutators.d.ts +56 -0
  140. package/dist/react/useMutators.js +66 -0
  141. package/dist/react/usePresence.d.ts +32 -0
  142. package/dist/react/usePresence.js +41 -0
  143. package/dist/react/useQuery.d.ts +123 -0
  144. package/dist/react/useQuery.js +145 -0
  145. package/dist/react/useReactive.d.ts +35 -0
  146. package/dist/react/useReactive.js +111 -0
  147. package/dist/react/useReader.d.ts +69 -0
  148. package/dist/react/useReader.js +73 -0
  149. package/dist/react/useSyncStatus.d.ts +61 -0
  150. package/dist/react/useSyncStatus.js +76 -0
  151. package/dist/react/useUndoScope.d.ts +36 -0
  152. package/dist/react/useUndoScope.js +73 -0
  153. package/dist/realtime/index.d.ts +10 -0
  154. package/dist/realtime/index.js +9 -0
  155. package/dist/schema/field.d.ts +134 -0
  156. package/dist/schema/field.js +264 -0
  157. package/dist/schema/index.d.ts +29 -0
  158. package/dist/schema/index.js +38 -0
  159. package/dist/schema/model.d.ts +326 -0
  160. package/dist/schema/model.js +89 -0
  161. package/dist/schema/queries.d.ts +203 -0
  162. package/dist/schema/queries.js +145 -0
  163. package/dist/schema/relation.d.ts +172 -0
  164. package/dist/schema/relation.js +104 -0
  165. package/dist/schema/schema.d.ts +259 -0
  166. package/dist/schema/schema.js +188 -0
  167. package/dist/schema/sugar.d.ts +129 -0
  168. package/dist/schema/sugar.js +94 -0
  169. package/dist/source/index.d.ts +423 -0
  170. package/dist/source/index.js +320 -0
  171. package/dist/source/pushQueue.d.ts +112 -0
  172. package/dist/source/pushQueue.js +249 -0
  173. package/dist/stores/ObjectStore.d.ts +103 -0
  174. package/dist/stores/ObjectStore.js +371 -0
  175. package/dist/stores/ObjectStoreContract.d.ts +39 -0
  176. package/dist/stores/ObjectStoreContract.js +1 -0
  177. package/dist/stores/SyncActionStore.d.ts +101 -0
  178. package/dist/stores/SyncActionStore.js +481 -0
  179. package/dist/sync/BootstrapHelper.d.ts +127 -0
  180. package/dist/sync/BootstrapHelper.js +434 -0
  181. package/dist/sync/ConnectionManager.d.ts +136 -0
  182. package/dist/sync/ConnectionManager.js +465 -0
  183. package/dist/sync/HydrationCoordinator.d.ts +137 -0
  184. package/dist/sync/HydrationCoordinator.js +468 -0
  185. package/dist/sync/NetworkProbe.d.ts +43 -0
  186. package/dist/sync/NetworkProbe.js +113 -0
  187. package/dist/sync/OfflineFlush.d.ts +9 -0
  188. package/dist/sync/OfflineFlush.js +22 -0
  189. package/dist/sync/OfflineTransactionStore.d.ts +37 -0
  190. package/dist/sync/OfflineTransactionStore.js +263 -0
  191. package/dist/sync/SyncWebSocket.d.ts +663 -0
  192. package/dist/sync/SyncWebSocket.js +1336 -0
  193. package/dist/sync/createIntentStream.d.ts +33 -0
  194. package/dist/sync/createIntentStream.js +243 -0
  195. package/dist/sync/createPresenceStream.d.ts +46 -0
  196. package/dist/sync/createPresenceStream.js +192 -0
  197. package/dist/sync/createSnapshot.d.ts +33 -0
  198. package/dist/sync/createSnapshot.js +124 -0
  199. package/dist/sync/participants.d.ts +114 -0
  200. package/dist/sync/participants.js +336 -0
  201. package/dist/sync/schemas.d.ts +79 -0
  202. package/dist/sync/schemas.js +78 -0
  203. package/dist/testing/fixtures/bootstrap.d.ts +45 -0
  204. package/dist/testing/fixtures/bootstrap.js +53 -0
  205. package/dist/testing/fixtures/deltas.d.ts +86 -0
  206. package/dist/testing/fixtures/deltas.js +139 -0
  207. package/dist/testing/fixtures/models.d.ts +82 -0
  208. package/dist/testing/fixtures/models.js +270 -0
  209. package/dist/testing/helpers/react-wrapper.d.ts +66 -0
  210. package/dist/testing/helpers/react-wrapper.js +64 -0
  211. package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
  212. package/dist/testing/helpers/sync-engine-harness.js +70 -0
  213. package/dist/testing/helpers/wait.d.ts +25 -0
  214. package/dist/testing/helpers/wait.js +44 -0
  215. package/dist/testing/index.d.ts +21 -0
  216. package/dist/testing/index.js +32 -0
  217. package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
  218. package/dist/testing/mocks/MockMutationExecutor.js +139 -0
  219. package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
  220. package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
  221. package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
  222. package/dist/testing/mocks/MockSyncContext.js +100 -0
  223. package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
  224. package/dist/testing/mocks/MockSyncStore.js +171 -0
  225. package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
  226. package/dist/testing/mocks/MockWebSocket.js +117 -0
  227. package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
  228. package/dist/transactions/OptimisticEchoTracker.js +104 -0
  229. package/dist/transactions/TransactionQueue.d.ts +499 -0
  230. package/dist/transactions/TransactionQueue.js +1895 -0
  231. package/dist/transactions/index.d.ts +16 -0
  232. package/dist/transactions/index.js +7 -0
  233. package/dist/transactions/mutation-error-handler.d.ts +5 -0
  234. package/dist/transactions/mutation-error-handler.js +39 -0
  235. package/dist/types/global.d.ts +107 -0
  236. package/dist/types/global.js +38 -0
  237. package/dist/types/index.d.ts +241 -0
  238. package/dist/types/index.js +70 -0
  239. package/dist/types/streams.d.ts +495 -0
  240. package/dist/types/streams.js +11 -0
  241. package/dist/utils/asyncIterator.d.ts +41 -0
  242. package/dist/utils/asyncIterator.js +142 -0
  243. package/dist/utils/duration.d.ts +28 -0
  244. package/dist/utils/duration.js +47 -0
  245. package/dist/utils/mobx-setup.d.ts +42 -0
  246. package/dist/utils/mobx-setup.js +381 -0
  247. package/docs/api-keys.md +24 -0
  248. package/docs/api.md +230 -0
  249. package/docs/audit.md +81 -0
  250. package/docs/capabilities.md +163 -0
  251. package/docs/client-behavior.md +202 -0
  252. package/docs/data-sources.md +214 -0
  253. package/docs/examples/agent-human.md +84 -0
  254. package/docs/examples/ai-sdk-tool.md +92 -0
  255. package/docs/examples/existing-python-backend.md +249 -0
  256. package/docs/examples/nextjs.md +88 -0
  257. package/docs/examples/server-agent.md +86 -0
  258. package/docs/guarantees.md +148 -0
  259. package/docs/index.md +97 -0
  260. package/docs/integration-guide.md +493 -0
  261. package/docs/interaction-model.md +140 -0
  262. package/docs/mcp/claude-code.md +43 -0
  263. package/docs/mcp/cursor.md +53 -0
  264. package/docs/mcp/windsurf.md +46 -0
  265. package/docs/mcp.md +59 -0
  266. package/docs/quickstart.md +152 -0
  267. package/docs/react.md +115 -0
  268. package/docs/roadmap.md +45 -0
  269. package/examples/README.md +54 -0
  270. package/examples/data-source/README.md +102 -0
  271. package/examples/data-source/ablo-driver.ts +89 -0
  272. package/examples/data-source/customer-server.ts +208 -0
  273. package/examples/data-source/run.ts +101 -0
  274. package/examples/data-source/schema.ts +25 -0
  275. package/examples/quickstart.ts +54 -0
  276. package/examples/tsconfig.json +16 -0
  277. package/llms.txt +143 -0
  278. package/package.json +147 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Duration parser — `'3m'`, `'24h'`, `500` (ms), `'15s'`, etc.
3
+ *
4
+ * The same TTL flavor every config-driven tool uses (Vercel's `ms`,
5
+ * Zod's `.duration()`, a hundred CLIs). Zero deps, one regex, three
6
+ * units that cover everything the SDK needs:
7
+ *
8
+ * - `'500ms'` → 500 ms
9
+ * - `'30s'` → 30 000 ms
10
+ * - `'3m'` → 180 000 ms
11
+ * - `'24h'` → 86 400 000 ms
12
+ *
13
+ * Back-compat escape hatch: plain numbers are kept as-is and
14
+ * interpreted in the caller's existing unit (seconds for TTL APIs).
15
+ * This lets us retrofit the string form to every `ttlSeconds` field
16
+ * without breaking numeric callers — the wrapper below branches on
17
+ * the input type.
18
+ */
19
+ export type Duration = number | `${number}ms` | `${number}s` | `${number}m` | `${number}h`;
20
+ /**
21
+ * Parse a duration expressed as a number-of-seconds OR a unit-suffixed
22
+ * string. Returns milliseconds. A bare number is interpreted as
23
+ * **seconds** (matches the existing `ttlSeconds` semantics — prevents
24
+ * silent breakage when a caller migrates from numeric to string).
25
+ */
26
+ export declare function toMs(input: Duration): number;
27
+ /** Convenience: same as `toMs` but divides out to seconds. */
28
+ export declare function toSeconds(input: Duration): number;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Duration parser — `'3m'`, `'24h'`, `500` (ms), `'15s'`, etc.
3
+ *
4
+ * The same TTL flavor every config-driven tool uses (Vercel's `ms`,
5
+ * Zod's `.duration()`, a hundred CLIs). Zero deps, one regex, three
6
+ * units that cover everything the SDK needs:
7
+ *
8
+ * - `'500ms'` → 500 ms
9
+ * - `'30s'` → 30 000 ms
10
+ * - `'3m'` → 180 000 ms
11
+ * - `'24h'` → 86 400 000 ms
12
+ *
13
+ * Back-compat escape hatch: plain numbers are kept as-is and
14
+ * interpreted in the caller's existing unit (seconds for TTL APIs).
15
+ * This lets us retrofit the string form to every `ttlSeconds` field
16
+ * without breaking numeric callers — the wrapper below branches on
17
+ * the input type.
18
+ */
19
+ const PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/;
20
+ const UNIT_MS = {
21
+ ms: 1,
22
+ s: 1_000,
23
+ m: 60_000,
24
+ h: 3_600_000,
25
+ };
26
+ /**
27
+ * Parse a duration expressed as a number-of-seconds OR a unit-suffixed
28
+ * string. Returns milliseconds. A bare number is interpreted as
29
+ * **seconds** (matches the existing `ttlSeconds` semantics — prevents
30
+ * silent breakage when a caller migrates from numeric to string).
31
+ */
32
+ export function toMs(input) {
33
+ if (typeof input === 'number')
34
+ return input * 1_000;
35
+ const match = PATTERN.exec(input);
36
+ if (!match) {
37
+ throw new Error(`Invalid duration "${input}" — expected number (seconds) or ` +
38
+ `a string like "500ms" | "30s" | "3m" | "24h".`);
39
+ }
40
+ const value = Number(match[1]);
41
+ const unit = match[2];
42
+ return value * UNIT_MS[unit];
43
+ }
44
+ /** Convenience: same as `toMs` but divides out to seconds. */
45
+ export function toSeconds(input) {
46
+ return Math.floor(toMs(input) / 1_000);
47
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * M1 Helper - Simplified MobX Setup
3
+ *
4
+ * Fixed version that doesn't conflict with existing getters/setters
5
+ */
6
+ import { type AnnotationMapEntry } from 'mobx';
7
+ import { PropertyMetadata, ReferenceMetadata } from '../types/index.js';
8
+ /**
9
+ * The internal contract M1 relies on. Models invoke M1 from inside
10
+ * their constructor, where these fields are guaranteed to exist on
11
+ * `this`. Declared here as the bound on M1's generic so the body can
12
+ * reference them without `as any` casts. Each field is optional so a
13
+ * partial Model implementation (e.g., a test fixture) still satisfies
14
+ * the bound.
15
+ */
16
+ interface M1Target {
17
+ _hasCustomObservability?: boolean;
18
+ _isConstructing?: boolean;
19
+ _extraMobxAnnotations?: Record<string, AnnotationMapEntry>;
20
+ setupObservability?(): void;
21
+ propertyChanged?(name: string, oldValue: unknown, newValue: unknown): void;
22
+ }
23
+ /**
24
+ * M1 - Make properties observable with proper MobX setup
25
+ *
26
+ * Simplified version that respects existing getters/setters
27
+ */
28
+ export declare function M1<T extends M1Target>(target: T, propertyMetadata: Map<string, PropertyMetadata>, referenceMetadata?: Map<string, ReferenceMetadata>): void;
29
+ /**
30
+ * Helper to make a class observable
31
+ * For classes that don't have custom observability
32
+ */
33
+ export declare function makeModelObservable(modelClass: any, propertyMetadata: Map<string, PropertyMetadata>, referenceMetadata?: Map<string, any>): any;
34
+ /**
35
+ * Utility to check if a property is observable
36
+ */
37
+ export declare function isObservableProperty(target: any, propName: string, propertyMetadata: Map<string, PropertyMetadata>): boolean;
38
+ /**
39
+ * Utility to get computed properties
40
+ */
41
+ export declare function getComputedProperties(propertyMetadata: Map<string, PropertyMetadata>): string[];
42
+ export {};
@@ -0,0 +1,381 @@
1
+ /**
2
+ * M1 Helper - Simplified MobX Setup
3
+ *
4
+ * Fixed version that doesn't conflict with existing getters/setters
5
+ */
6
+ import { observable, makeObservable, action, computed, observe, } from 'mobx';
7
+ import { PropertyType } from '../types/index.js';
8
+ import { getContext } from '../context.js';
9
+ /**
10
+ * M1 - Make properties observable with proper MobX setup
11
+ *
12
+ * Simplified version that respects existing getters/setters
13
+ */
14
+ export function M1(target, propertyMetadata, referenceMetadata) {
15
+ // MobX accepts an annotations map keyed by PropertyKey. We build it
16
+ // from the runtime metadata, so the keys are strings — not
17
+ // statically derivable from `keyof T`.
18
+ const annotations = {};
19
+ // Helper to check if property has a getter
20
+ const hasGetter = (propName) => {
21
+ let obj = target;
22
+ while (obj) {
23
+ const descriptor = Object.getOwnPropertyDescriptor(obj, propName);
24
+ if (descriptor && descriptor.get)
25
+ return true;
26
+ obj = Object.getPrototypeOf(obj);
27
+ if (obj === Object.prototype)
28
+ break;
29
+ }
30
+ return false;
31
+ };
32
+ // Helper to check if property has a setter
33
+ const hasSetter = (propName) => {
34
+ let obj = target;
35
+ while (obj) {
36
+ const descriptor = Object.getOwnPropertyDescriptor(obj, propName);
37
+ if (descriptor && descriptor.set)
38
+ return true;
39
+ obj = Object.getPrototypeOf(obj);
40
+ if (obj === Object.prototype)
41
+ break;
42
+ }
43
+ return false;
44
+ };
45
+ // Skip if target has its own observability setup
46
+ // This allows models like Task to handle their own MobX setup
47
+ if (target.setupObservability || target._hasCustomObservability) {
48
+ getContext().modelDebugLogger?.logDebug(`${target.constructor.name} has custom observability, skipping M1`);
49
+ return;
50
+ }
51
+ // Process each property based on its type
52
+ for (const [propName, metadata] of propertyMetadata) {
53
+ const hasCustomGetter = hasGetter(propName);
54
+ const hasCustomSetter = hasSetter(propName);
55
+ // If property has custom getter/setter, respect it
56
+ if (hasCustomGetter && hasCustomSetter) {
57
+ // Property is fully managed, skip it
58
+ continue;
59
+ }
60
+ switch (metadata.type) {
61
+ case PropertyType.property:
62
+ case PropertyType.ephemeralProperty:
63
+ if (hasCustomGetter) {
64
+ // Has getter but no setter - mark as computed
65
+ annotations[propName] = computed;
66
+ }
67
+ else {
68
+ // Pick the cheapest observability that still captures
69
+ // reactivity at the granularity consumers subscribe to. Default
70
+ // `observable` is deep, which on JSON-blob fields produces a
71
+ // recursive atom tree (catastrophic on chart specs, ProseMirror
72
+ // docs, style maps) — see PropertyMetadata.observability docs.
73
+ switch (metadata.observability) {
74
+ case 'ref':
75
+ annotations[propName] = observable.ref;
76
+ break;
77
+ case 'shallow':
78
+ annotations[propName] = observable.shallow;
79
+ break;
80
+ case 'deep':
81
+ case undefined:
82
+ default:
83
+ annotations[propName] = observable;
84
+ break;
85
+ }
86
+ }
87
+ break;
88
+ case PropertyType.reference:
89
+ // Foreign key ID property
90
+ const idPropName = propName.endsWith('Id') ? propName : `${propName}Id`;
91
+ if (!hasGetter(idPropName) && !hasSetter(idPropName)) {
92
+ annotations[idPropName] = observable;
93
+ }
94
+ break;
95
+ case PropertyType.referenceModel:
96
+ // Computed getter for referenced model
97
+ if (!hasCustomGetter && !hasCustomSetter) {
98
+ annotations[propName] = computed;
99
+ }
100
+ break;
101
+ case PropertyType.referenceCollection:
102
+ // Observable collection
103
+ if (!hasCustomGetter && !hasCustomSetter) {
104
+ annotations[propName] = observable;
105
+ }
106
+ break;
107
+ case PropertyType.backReference:
108
+ // Computed back-reference
109
+ if (!hasCustomGetter && !hasCustomSetter) {
110
+ annotations[propName] = computed;
111
+ }
112
+ break;
113
+ case PropertyType.referenceArray:
114
+ // Observable array of IDs
115
+ if (!propName.endsWith('Ids')) {
116
+ const idsPropName = `${propName}Ids`;
117
+ if (!hasGetter(idsPropName) && !hasSetter(idsPropName)) {
118
+ annotations[idsPropName] = observable;
119
+ }
120
+ }
121
+ if (!hasCustomGetter && !hasCustomSetter) {
122
+ annotations[propName] = observable;
123
+ }
124
+ break;
125
+ }
126
+ }
127
+ // Add standard model properties only if they don't exist
128
+ if (!hasGetter('id') && !hasSetter('id')) {
129
+ annotations.id = observable;
130
+ }
131
+ if (!hasGetter('createdAt') && !hasSetter('createdAt')) {
132
+ annotations.createdAt = observable;
133
+ }
134
+ if (!hasGetter('updatedAt') && !hasSetter('updatedAt')) {
135
+ annotations.updatedAt = observable;
136
+ }
137
+ if (!hasGetter('modifiedProperties') && !hasSetter('modifiedProperties')) {
138
+ annotations.modifiedProperties = observable;
139
+ }
140
+ // Add actions only if methods exist and aren't already actions.
141
+ // `Reflect.get` keeps the read typed without an index-signature
142
+ // cast — `target` is `M1Target`, which deliberately doesn't index
143
+ // by arbitrary string (we don't want to sneak random fields in).
144
+ const actionMethods = [
145
+ 'propertyChanged',
146
+ 'markAsPersisted',
147
+ 'clearChanges',
148
+ 'updateFromData',
149
+ ];
150
+ for (const methodName of actionMethods) {
151
+ if (typeof Reflect.get(target, methodName) === 'function') {
152
+ annotations[methodName] = action;
153
+ }
154
+ }
155
+ // Log setup for debugging
156
+ const modelName = target.constructor.name;
157
+ const observableProps = Object.keys(annotations).filter((k) => annotations[k] === observable);
158
+ const computedProps = Object.keys(annotations).filter((k) => annotations[k] === computed);
159
+ getContext().modelDebugLogger?.logObservableSetup(modelName, observableProps, computedProps);
160
+ // Merge any extra annotations declared by the model (e.g., computed for query getters)
161
+ if (target._extraMobxAnnotations && typeof target._extraMobxAnnotations === 'object') {
162
+ Object.assign(annotations, target._extraMobxAnnotations);
163
+ }
164
+ // Apply MobX decorators
165
+ try {
166
+ // Only apply if we have annotations to apply
167
+ if (Object.keys(annotations).length > 0) {
168
+ makeObservable(target, annotations);
169
+ // Bridge MobX's observable setter to `propertyChanged()` so the
170
+ // dynamic-class mutation path sees direct assignments like
171
+ // `layer.position = newPos` — i.e., the transaction queue gets an
172
+ // update and the server eventually sees it.
173
+ //
174
+ // History: the old hand-coded models wired setters via
175
+ // `setupSimplePropertyTracking` which overrode MobX's accessors and
176
+ // broke reactivity — that function was correctly kept off the
177
+ // schema-driven dynamic-class path. The intended replacement was
178
+ // "use `store.mutate.slideLayers.update(...)` from callers," but a
179
+ // large amount of existing product code (drag, resize, formatting,
180
+ // keyboard nudge, AI tools, etc.) still assigns properties directly,
181
+ // and making that silently not sync was the regression that broke
182
+ // all slide-layer edits.
183
+ //
184
+ // `observe()` attaches a post-set listener WITHOUT replacing MobX's
185
+ // accessors — so the observable keeps its normal reactivity and we
186
+ // get a synchronous change event we can forward to
187
+ // `propertyChanged()`. We scope it to `PropertyType.property`
188
+ // (persisted fields) so ephemeral UI state and computed/reference
189
+ // virtual fields don't leak into `modifiedProperties`.
190
+ //
191
+ // Construction-time writes (the constructor's initial field
192
+ // population from wire data) also fire `observe` — so we gate with
193
+ // `target._isNew` and a transient `_isConstructing` flag: any change
194
+ // that happens before the model is marked persisted is an initial
195
+ // hydration, not a user edit.
196
+ if (!target._hasCustomObservability) {
197
+ for (const [propName, metadata] of propertyMetadata) {
198
+ if (metadata.type !== PropertyType.property)
199
+ continue;
200
+ // Only `annotations[propName] === observable` entries are
201
+ // safe to `observe()`. DON'T gate on
202
+ // `Object.getOwnPropertyDescriptor(target, propName).get/set` —
203
+ // `makeObservable(target, annotations)` has ALREADY installed
204
+ // its own getter/setter by this point, so that descriptor
205
+ // check flags every field as "custom" and silently skips
206
+ // every observer. That was the root cause of `input: {}` on
207
+ // the wire: `modifiedProperties` stayed empty for dynamic
208
+ // models, the transaction queue couldn't find any changes to
209
+ // send, and the server acked a no-op mutation.
210
+ if (!(propName in annotations))
211
+ continue;
212
+ // Accept any flavor of `observable` (deep, ref, shallow). `observe()`
213
+ // works on all three — the listener fires on the property
214
+ // reassignment, regardless of how the value is enhanced
215
+ // internally. Crucially, this lets `propertyChanged()` (and the
216
+ // transaction queue) still see writes to JSON-blob fields
217
+ // annotated as `observable.ref`. Gating on the bare `observable`
218
+ // constant alone would silently drop those writes — see the
219
+ // `input: {}` regression captured in the comment above for the
220
+ // same failure mode.
221
+ const ann = annotations[propName];
222
+ if (ann !== observable && ann !== observable.ref && ann !== observable.shallow)
223
+ continue;
224
+ try {
225
+ // Cross the runtime/static boundary: propName is a string
226
+ // from the propertyMetadata Map iteration. At runtime it's
227
+ // guaranteed to be a key on `target` (we just wrote the
228
+ // annotation entry for it via makeObservable above). The
229
+ // cast to `keyof T` reflects that runtime invariant —
230
+ // it's not "I don't know," it's "I know but TS can't see
231
+ // the proof from here." MobX's IValueDidChange<T[K]>
232
+ // gives the change parameter a concrete typed value.
233
+ const key = propName;
234
+ observe(target, key, (change) => {
235
+ // Only track updates, not add/delete. For scalar observables,
236
+ // MobX emits `{ type: 'update', oldValue, newValue }`.
237
+ if (change.type !== 'update')
238
+ return;
239
+ // Skip initial hydration writes. `_isNew` stays true until
240
+ // the model is `markAsPersisted()`, and the first wave of
241
+ // setters runs during construction BEFORE this observer
242
+ // would exist (observers are installed now, on this line);
243
+ // but defensively still gate, because callers that
244
+ // pre-construct models with partial data then bulk-assign
245
+ // would otherwise spuriously fill `modifiedProperties`.
246
+ if (target._isConstructing)
247
+ return;
248
+ if (typeof target.propertyChanged === 'function') {
249
+ target.propertyChanged(propName, change.oldValue, change.newValue);
250
+ }
251
+ });
252
+ }
253
+ catch {
254
+ // If a property isn't observable for any reason (e.g. it
255
+ // was filtered out by the annotations logic but still shows
256
+ // up in metadata), silently skip — propertyChanged tracking
257
+ // is best-effort, not a correctness guarantee.
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ catch (error) {
264
+ const errorMessage = error instanceof Error ? error.message : String(error);
265
+ getContext().modelDebugLogger?.logError(modelName, 'OBSERVABLE_SETUP', errorMessage, {
266
+ annotations: Object.keys(annotations),
267
+ target: Object.keys(target),
268
+ });
269
+ throw error;
270
+ }
271
+ }
272
+ /**
273
+ * Setup simple property tracking for change detection
274
+ * Only for properties without existing getters/setters
275
+ */
276
+ function setupSimplePropertyTracking(target, propertyMetadata) {
277
+ for (const [propName, metadata] of propertyMetadata) {
278
+ // Only track regular properties
279
+ if (metadata.type !== PropertyType.property &&
280
+ metadata.type !== PropertyType.ephemeralProperty) {
281
+ continue;
282
+ }
283
+ // Check if property already has custom getter/setter
284
+ const descriptor = Object.getOwnPropertyDescriptor(target, propName);
285
+ if (descriptor && (descriptor.get || descriptor.set)) {
286
+ // Property already managed, skip
287
+ continue;
288
+ }
289
+ // Check prototype chain
290
+ let proto = Object.getPrototypeOf(target);
291
+ let hasCustomAccessor = false;
292
+ while (proto && proto !== Object.prototype) {
293
+ const protoDescriptor = Object.getOwnPropertyDescriptor(proto, propName);
294
+ if (protoDescriptor && (protoDescriptor.get || protoDescriptor.set)) {
295
+ hasCustomAccessor = true;
296
+ break;
297
+ }
298
+ proto = Object.getPrototypeOf(proto);
299
+ }
300
+ if (hasCustomAccessor) {
301
+ continue;
302
+ }
303
+ // Only add tracking if property exists and isn't already tracked
304
+ if (propName in target) {
305
+ const currentValue = target[propName];
306
+ // Store value in a private field
307
+ const privateField = `_tracked_${propName}`;
308
+ target[privateField] = currentValue;
309
+ // Create simple getter/setter for tracking
310
+ Object.defineProperty(target, propName, {
311
+ get() {
312
+ return this[privateField];
313
+ },
314
+ set(newValue) {
315
+ const oldValue = this[privateField];
316
+ if (oldValue !== newValue) {
317
+ this[privateField] = newValue;
318
+ // Only track changes for non-ephemeral properties
319
+ if (metadata.type === PropertyType.property && this.propertyChanged) {
320
+ this.propertyChanged(propName, oldValue, newValue);
321
+ }
322
+ }
323
+ },
324
+ enumerable: true,
325
+ configurable: true,
326
+ });
327
+ }
328
+ }
329
+ }
330
+ /**
331
+ * Helper to make a class observable
332
+ * For classes that don't have custom observability
333
+ */
334
+ export function makeModelObservable(modelClass, propertyMetadata, referenceMetadata) {
335
+ // Check if class already handles observability
336
+ if (modelClass.prototype.setupObservability || modelClass.prototype._hasCustomObservability) {
337
+ return modelClass;
338
+ }
339
+ // Create wrapper class
340
+ const WrappedClass = class extends modelClass {
341
+ constructor(...args) {
342
+ super(...args);
343
+ M1(this, propertyMetadata, referenceMetadata);
344
+ }
345
+ };
346
+ // Preserve class name
347
+ Object.defineProperty(WrappedClass, 'name', {
348
+ value: modelClass.name,
349
+ configurable: true,
350
+ });
351
+ // Copy static properties
352
+ Object.setPrototypeOf(WrappedClass, modelClass);
353
+ return WrappedClass;
354
+ }
355
+ /**
356
+ * Utility to check if a property is observable
357
+ */
358
+ export function isObservableProperty(target, propName, propertyMetadata) {
359
+ const metadata = propertyMetadata.get(propName);
360
+ if (!metadata)
361
+ return false;
362
+ return [
363
+ PropertyType.property,
364
+ PropertyType.ephemeralProperty,
365
+ PropertyType.referenceCollection,
366
+ PropertyType.referenceArray,
367
+ ].includes(metadata.type);
368
+ }
369
+ /**
370
+ * Utility to get computed properties
371
+ */
372
+ export function getComputedProperties(propertyMetadata) {
373
+ const computed = [];
374
+ for (const [propName, metadata] of propertyMetadata) {
375
+ if (metadata.type === PropertyType.referenceModel ||
376
+ metadata.type === PropertyType.backReference) {
377
+ computed.push(propName);
378
+ }
379
+ }
380
+ return computed;
381
+ }
@@ -0,0 +1,24 @@
1
+ # API Keys
2
+
3
+ Trusted runtimes authenticate with an API key.
4
+
5
+ ```ts
6
+ import Ablo from '@ablo/sync-engine';
7
+
8
+ const ablo = Ablo({ apiKey: process.env.ABLO_API_KEY });
9
+ ```
10
+
11
+ The key identifies the Ablo account. Application code does not pass an organization id; Ablo derives scope from the credential.
12
+
13
+ Use the root `@ablo/sync-engine` import with a schema for app clients.
14
+
15
+ ## Server-Side API Keys
16
+
17
+ Use API keys from trusted runtimes:
18
+
19
+ - backend route handlers
20
+ - workers and agents
21
+ - CLI tools
22
+ - webhooks
23
+
24
+ Never ship a secret API key to a browser bundle.