@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.
Files changed (246) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +45 -36
  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 +583 -287
  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/data-sources.md +12 -5
  220. package/docs/examples/existing-python-backend.md +3 -3
  221. package/docs/identity.md +4 -4
  222. package/docs/integration-guide.md +1 -1
  223. package/docs/react.md +1 -1
  224. package/docs/sessions.md +5 -7
  225. package/llms.txt +4 -2
  226. package/package.json +15 -13
  227. package/dist/client/index.d.ts +0 -36
  228. package/dist/client/index.js +0 -33
  229. package/dist/config/index.d.ts +0 -10
  230. package/dist/config/index.js +0 -12
  231. package/dist/interfaces/headless.d.ts +0 -95
  232. package/dist/interfaces/headless.js +0 -41
  233. package/dist/query/index.d.ts +0 -6
  234. package/dist/query/index.js +0 -5
  235. package/dist/realtime/index.d.ts +0 -10
  236. package/dist/realtime/index.js +0 -9
  237. package/dist/schema/plane.d.ts +0 -23
  238. package/dist/schema/plane.js +0 -19
  239. package/dist/server/next.d.ts +0 -51
  240. package/dist/server/next.js +0 -47
  241. package/dist/sync/OfflineFlush.d.ts +0 -9
  242. package/dist/sync/OfflineFlush.js +0 -22
  243. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  244. package/dist/sync/OfflineTransactionStore.js +0 -263
  245. package/dist/transactions/index.d.ts +0 -16
  246. package/dist/transactions/index.js +0 -7
@@ -1,123 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { Model } from '../Model.js';
3
- import type { ModelScope } from '../types/index.js';
4
- import type { QueryView, QueryViewOptions } from '../core/QueryView.js';
5
- import type { ViewRegistry } from '../core/ViewRegistry.js';
6
2
  import type { Schema } from '../schema/schema.js';
7
- import type { SyncStatus } from '../BaseSyncedStore.js';
8
- import type { ParticipantScope } from '../sync/participants.js';
9
- /**
10
- * A single LOCAL mutation as observed off the commit stream — the substrate
11
- * the undo system records from. One is emitted per local create/update/
12
- * delete/archive (remote/collaborator deltas never appear here: they apply
13
- * through a separate pool path that doesn't queue mutations). `previousData`
14
- * holds the pre-edit field values (captured from the model's
15
- * `modifiedProperties` first-old-wins baseline), so an inverse op is fully
16
- * derivable from the event alone — no separate snapshot pass.
17
- *
18
- * This mirrors how Yjs's `UndoManager` derives reverse-ops by observing the
19
- * doc and Liveblocks' `room.history` records room ops: undo listens to the
20
- * one place all local writes converge, rather than wrapping the write call.
21
- */
22
- export interface LocalMutation {
23
- type: 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
24
- /** Registered model name (e.g. `'SlideLayer'`); resolved to a schema key by the recorder. */
25
- modelName: string;
26
- modelId: string;
27
- /** New field values (create/update). */
28
- data?: Record<string, unknown> | null;
29
- /** Pre-edit field values (update → inverse patch; delete → full re-create row). */
30
- previousData?: Record<string, unknown> | null;
31
- }
32
- /**
33
- * Minimal store interface that the SDK hooks need.
34
- * Consumers provide their concrete store (e.g., SyncedStore) that implements this.
35
- */
36
- export interface SyncStoreContract {
37
- /**
38
- * Subscribe to the LOCAL mutation stream (optimistic, pre-ack) for undo
39
- * recording. Optional so minimal test doubles can omit it — when absent,
40
- * undo scopes simply record nothing. The concrete store
41
- * (`BaseSyncedStore`) wires this to the TransactionQueue's
42
- * `transaction:created` event. Returns an unsubscribe function.
43
- */
44
- subscribeLocalMutations?(handler: (mutation: LocalMutation) => void): () => void;
45
- retrieve(modelClass: abstract new (...args: never[]) => Model, id: string): Model | undefined;
46
- queryByClass(modelClass: abstract new (...args: never[]) => Model, options?: {
47
- predicate?: (model: Model) => boolean;
48
- scope?: ModelScope;
49
- orderBy?: keyof Model;
50
- order?: 'asc' | 'desc';
51
- limit?: number;
52
- offset?: number;
53
- }): {
54
- data: Model[];
55
- };
56
- /**
57
- * Save (create or update) one entity. Calling `save` in a tight loop
58
- * produces a single wire commit with one `batchIndex`: the SyncClient
59
- * debounces IDB persistence and the server push to one microtask, and
60
- * TransactionQueue coalesces every transaction staged in the tick into
61
- * one batch. There is intentionally no `saveMany` — Zero, Replicache,
62
- * and the rest of the local-first lineage all expose one-row writes
63
- * and rely on the implicit tick boundary.
64
- *
65
- * `skipValidation` exists for trusted bulk paths (AI sandbox layer
66
- * generation, PPTX import, hydration) where the producer has already
67
- * type-checked and per-row Zod is a measurable cost.
68
- */
69
- save(model: Model, options?: {
70
- skipValidation?: boolean;
71
- }): Promise<void>;
72
- delete(model: Model): Promise<void>;
73
- archive(model: Model): Promise<void>;
74
- unarchive(model: Model): Promise<void>;
75
- /** The ObjectPool — for entity/collection lookups by ID or typename. */
76
- pool: {
77
- get(id: string): Model | undefined;
78
- getByTypeName(typename: string, scope?: ModelScope): Model[];
79
- getByForeignKey(modelName: string, fieldName: string, fieldValue: string): Model[];
80
- createFromData(data: Record<string, unknown>): Model | null;
81
- hasForeignKeyIndex(typename: string, fieldName: string): boolean;
82
- createView<T extends Record<string, unknown>>(typename: string, options?: QueryViewOptions<T>): QueryView<T>;
83
- viewRegistry: ViewRegistry;
84
- };
85
- /**
86
- * Reactive sync-status getters. Powered by MobX `computed` inside
87
- * `BaseSyncedStore`, so they're safe to read in `observer` components
88
- * and inside `reaction(() => store.isReady, ...)`. Consumers that
89
- * don't want to touch MobX should prefer the `useSyncStatus()` hook.
90
- */
91
- readonly isReady: boolean;
92
- readonly isSyncing: boolean;
93
- readonly isOffline: boolean;
94
- readonly isReconnecting: boolean;
95
- readonly isError: boolean;
96
- readonly hasUnsyncedChanges: boolean;
97
- /**
98
- * Area-of-interest (dynamic read subscription). `enterScope`/`leaveScope`
99
- * move the connection's read interest as the user navigates (open/close a
100
- * deck, sheet, doc); `pinScope`/`unpinScope` express prominence (an active
101
- * claim keeps a group subscribed). Each resolves the scope through the same
102
- * resolver the claim path uses, so read interest and write claims agree on
103
- * the sync-group string. Optional so minimal test doubles can omit them;
104
- * no-ops before the socket exists. The concrete store (`BaseSyncedStore`)
105
- * forwards to its `AreaOfInterestManager`.
106
- */
107
- enterScope?(scope: ParticipantScope, opts?: {
108
- hydrate?: boolean;
109
- }): Promise<void>;
110
- leaveScope?(scope: ParticipantScope): Promise<void>;
111
- pinScope?(scope: ParticipantScope): Promise<void>;
112
- unpinScope?(scope: ParticipantScope): Promise<void>;
113
- /**
114
- * Raw MobX-observable `SyncStatus` record. `useSyncStatus()` reads
115
- * `state`, `progress`, `pendingChanges`, `isSessionError`, `error`
116
- * from this to build its tagged union. Exposed on the contract so
117
- * consumer-facing hooks and test doubles can manipulate it directly.
118
- */
119
- readonly syncStatus: SyncStatus;
120
- }
3
+ import type { SyncStoreContract } from '../core/storeContract.js';
4
+ export type { SyncStoreContract, LocalMutation } from '../core/storeContract.js';
121
5
  export interface SyncReactContext {
122
6
  store: SyncStoreContract;
123
7
  /** Current organization ID for default entity context */
@@ -58,7 +58,7 @@ export function useAblo(modelOrSelect, id, options) {
58
58
  useEffect(() => {
59
59
  if (!engine || id === undefined)
60
60
  return;
61
- return engine.claims.onChange(() => setClaimVersion((version) => version + 1));
61
+ return engine.claims.onChange(() => { setClaimVersion((version) => version + 1); });
62
62
  }, [engine, id]);
63
63
  const selected = useReactive(() => {
64
64
  if (!engine || !isSelectorOnly || typeof modelOrSelect !== 'function') {
@@ -34,6 +34,6 @@ export function useErrorListener(listener) {
34
34
  const ref = useRef(listener);
35
35
  ref.current = listener;
36
36
  useEffect(() => {
37
- return ctx.subscribeError((err) => ref.current(err));
37
+ return ctx.subscribeError((err) => { ref.current(err); });
38
38
  }, [ctx]);
39
39
  }
@@ -33,6 +33,6 @@ export function useMutationFailureListener(listener) {
33
33
  const engine = ctx.engine;
34
34
  if (!engine)
35
35
  return;
36
- return engine.onMutationFailure((payload) => ref.current(payload));
36
+ return engine.onMutationFailure((payload) => { ref.current(payload); });
37
37
  }, [ctx, ctx.engine]);
38
38
  }
@@ -30,7 +30,13 @@ export function useMutators(schemaOrMutators, mutatorsOrOptions, maybeOptions) {
30
30
  continue;
31
31
  const invokers = {};
32
32
  for (const mutatorName of Object.keys(group)) {
33
- const fn = group[mutatorName];
33
+ const maybeFn = group[mutatorName];
34
+ if (!maybeFn)
35
+ continue;
36
+ // Bind the narrowed value: `noUncheckedIndexedAccess` types the indexed
37
+ // read as `Fn | undefined`, and that narrowing doesn't survive into the
38
+ // deferred invoker closures below — a non-optional local does.
39
+ const fn = maybeFn;
34
40
  const label = `${String(modelKey)}.${mutatorName}`;
35
41
  invokers[mutatorName] = async (args) => {
36
42
  // Recording path: wrap the transaction so each write snapshots its
@@ -98,7 +98,6 @@ export function useReactive(compute, equals = defaultEquals) {
98
98
  onChange();
99
99
  }
100
100
  });
101
- // eslint-disable-next-line react-hooks/exhaustive-deps
102
101
  }, [subscribeVersion]);
103
102
  const getSnapshot = useCallback(() => snapshotRef.current.value, []);
104
103
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
@@ -30,7 +30,7 @@ function getManager(key, factory) {
30
30
  export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
31
31
  const { store, organizationId, schema: ctxSchema } = useSyncContext();
32
32
  const isExplicit = typeof schemaOrName !== 'string';
33
- const schema = isExplicit ? schemaOrName : ctxSchema;
33
+ const schema = isExplicit ? (schemaOrName) : ctxSchema;
34
34
  const name = isExplicit ? nameOrOptions : schemaOrName;
35
35
  const options = (isExplicit ? maybeOptions : nameOrOptions);
36
36
  if (!schema) {
@@ -58,7 +58,7 @@ export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
58
58
  // `canUndo`/`canRedo` go stale in every consumer that didn't itself call
59
59
  // undo/redo (e.g. a keyboard handler whose Cmd+Z gate then never fires).
60
60
  useEffect(() => {
61
- return scope.onChange(() => setTick((t) => t + 1));
61
+ return scope.onChange(() => { setTick((t) => t + 1); });
62
62
  }, [scope]);
63
63
  const size = scope.size();
64
64
  return {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Safe-DDL LOCKING knobs — the ONE reader for how a schema change acquires
3
+ * (and retries for) table locks, shared by BOTH executors of the provision
4
+ * plan:
5
+ *
6
+ * - `ablo migrate` (cli/migrate.ts) — the customer runs the DDL themselves
7
+ * - the hosted executor (`apps/sync-server/src/schema/ddlExec.ts`)
8
+ *
9
+ * The two used to carry copy-pasted constants and had already drifted: the
10
+ * server honored `ABLO_SCHEMA_LOCK_ATTEMPTS` while the CLI hardcoded 5, so an
11
+ * operator tuning the knob got it applied by hosted push but silently ignored
12
+ * by `ablo migrate`. Both now resolve through here.
13
+ *
14
+ * The settings themselves are the battle-tested ones every mature migration
15
+ * tool uses (GitLab `with_lock_retries`, Strong Migrations, Doctolib
16
+ * `safe-pg-migrations`): a LOW `lock_timeout` so a blocked ALTER aborts fast
17
+ * instead of parking an ACCESS EXCLUSIVE request at the head of the lock
18
+ * queue (which would freeze every query on that table behind it), plus a
19
+ * bounded retry-with-backoff on the `55P03` abort.
20
+ *
21
+ * Env knobs (read at CALL time, not import time, so tests and long-lived
22
+ * processes see updates): `ABLO_SCHEMA_LOCK_TIMEOUT` / `ABLO_SCHEMA_LOCK_ATTEMPTS`
23
+ * — the older `ABLO_DDL_*` names are still honored so existing setups don't
24
+ * break.
25
+ */
26
+ /** Postgres SQLSTATE `lock_not_available` — a `lock_timeout` abort. The ONE
27
+ * retryable DDL failure; everything else is a real error. */
28
+ export declare const PG_LOCK_NOT_AVAILABLE = "55P03";
29
+ /** The env subset the resolvers read — injectable for tests. */
30
+ export type DdlLockEnv = Readonly<Record<string, string | undefined>>;
31
+ /** `lock_timeout` for the DDL transaction (a Postgres duration string). */
32
+ export declare function resolveDdlLockTimeout(env?: DdlLockEnv): string;
33
+ /** How many times the whole DDL transaction is attempted on `55P03` lock
34
+ * contention. Always ≥ 1; a malformed value falls back to the default
35
+ * instead of silently disabling retries. */
36
+ export declare function resolveDdlMaxLockAttempts(env?: DdlLockEnv): number;
37
+ /** Backoff before retry N (1-based): capped exponential + a little jitter so
38
+ * two contending migrators don't re-collide in lockstep. */
39
+ export declare function ddlLockRetryBackoffMs(attempt: number): number;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Safe-DDL LOCKING knobs — the ONE reader for how a schema change acquires
3
+ * (and retries for) table locks, shared by BOTH executors of the provision
4
+ * plan:
5
+ *
6
+ * - `ablo migrate` (cli/migrate.ts) — the customer runs the DDL themselves
7
+ * - the hosted executor (`apps/sync-server/src/schema/ddlExec.ts`)
8
+ *
9
+ * The two used to carry copy-pasted constants and had already drifted: the
10
+ * server honored `ABLO_SCHEMA_LOCK_ATTEMPTS` while the CLI hardcoded 5, so an
11
+ * operator tuning the knob got it applied by hosted push but silently ignored
12
+ * by `ablo migrate`. Both now resolve through here.
13
+ *
14
+ * The settings themselves are the battle-tested ones every mature migration
15
+ * tool uses (GitLab `with_lock_retries`, Strong Migrations, Doctolib
16
+ * `safe-pg-migrations`): a LOW `lock_timeout` so a blocked ALTER aborts fast
17
+ * instead of parking an ACCESS EXCLUSIVE request at the head of the lock
18
+ * queue (which would freeze every query on that table behind it), plus a
19
+ * bounded retry-with-backoff on the `55P03` abort.
20
+ *
21
+ * Env knobs (read at CALL time, not import time, so tests and long-lived
22
+ * processes see updates): `ABLO_SCHEMA_LOCK_TIMEOUT` / `ABLO_SCHEMA_LOCK_ATTEMPTS`
23
+ * — the older `ABLO_DDL_*` names are still honored so existing setups don't
24
+ * break.
25
+ */
26
+ /** Postgres SQLSTATE `lock_not_available` — a `lock_timeout` abort. The ONE
27
+ * retryable DDL failure; everything else is a real error. */
28
+ export const PG_LOCK_NOT_AVAILABLE = '55P03';
29
+ const DEFAULT_LOCK_TIMEOUT = '5s';
30
+ const DEFAULT_MAX_LOCK_ATTEMPTS = 5;
31
+ /** `lock_timeout` for the DDL transaction (a Postgres duration string). */
32
+ export function resolveDdlLockTimeout(env = process.env) {
33
+ return env.ABLO_SCHEMA_LOCK_TIMEOUT ?? env.ABLO_DDL_LOCK_TIMEOUT ?? DEFAULT_LOCK_TIMEOUT;
34
+ }
35
+ /** How many times the whole DDL transaction is attempted on `55P03` lock
36
+ * contention. Always ≥ 1; a malformed value falls back to the default
37
+ * instead of silently disabling retries. */
38
+ export function resolveDdlMaxLockAttempts(env = process.env) {
39
+ const raw = env.ABLO_SCHEMA_LOCK_ATTEMPTS ?? env.ABLO_DDL_LOCK_ATTEMPTS;
40
+ if (raw === undefined)
41
+ return DEFAULT_MAX_LOCK_ATTEMPTS;
42
+ const parsed = Number(raw);
43
+ if (!Number.isFinite(parsed))
44
+ return DEFAULT_MAX_LOCK_ATTEMPTS;
45
+ return Math.max(1, Math.floor(parsed));
46
+ }
47
+ /** Backoff before retry N (1-based): capped exponential + a little jitter so
48
+ * two contending migrators don't re-collide in lockstep. */
49
+ export function ddlLockRetryBackoffMs(attempt) {
50
+ return Math.min(60_000, 10 * 2 ** attempt) + Math.floor(Math.random() * 50);
51
+ }
@@ -42,7 +42,7 @@ function encodeMeta(meta) {
42
42
  return META_PREFIX + JSON.stringify(meta);
43
43
  }
44
44
  function decodeMeta(description) {
45
- if (!description || !description.startsWith(META_PREFIX))
45
+ if (!description?.startsWith(META_PREFIX))
46
46
  return null;
47
47
  try {
48
48
  return JSON.parse(description.slice(META_PREFIX.length));
@@ -24,16 +24,17 @@ export { z } from 'zod';
24
24
  export { field, indexed, getFieldMeta, type FieldBuilder, type FieldMeta } from './field.js';
25
25
  export { relation, type RelationDef, type RelationType } from './relation.js';
26
26
  export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, type Tenancy, type ScopedViaRef, type PolicyInput, } from './tenancy.js';
27
- export { planeSchema, DEFAULT_PLANE, type SchemaPlane } from './plane.js';
28
- export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_PLANES, type SyncDeltaCore, type DeltaAttribution, type DeltaProvenance, type SyncDeltaRow, type ParticipantKind, type ConfirmationState, type BackfillProvenance, } from './sync-delta-row.js';
27
+ export { residencySchema, DEFAULT_RESIDENCY, type ModelResidency, planeSchema, DEFAULT_PLANE, type SchemaPlane, } from './residency.js';
28
+ export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_RESIDENCY, type SyncDeltaCore, type DeltaAttribution, type DeltaProvenance, type SyncDeltaRow, type ParticipantKind, type ConfirmationState, type BackfillProvenance, } from './sync-delta-row.js';
29
29
  export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncDeltaWireCoreSchema, clientSyncDeltaSchema, serverSyncDeltaSchema, type SyncDeltaAction, type WireDeltaData, type ParticipantRef, type SyncDeltaWireCore, type ClientSyncDelta, type ServerSyncDelta, } from './sync-delta-wire.js';
30
30
  export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, type ConflictAxis, } from './model.js';
31
31
  export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwrite, agentsReject, agentsNotify, systemOverwrite, systemReject, systemNotify, type ConflictRule, } from './coordination.js';
32
32
  export { mutable, readOnly, type SugarOptions } from './sugar.js';
33
- export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './schema.js';
33
+ export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, intersectRequestedWithAllowed, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './schema.js';
34
34
  export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
35
35
  export { selectModels } from './select.js';
36
36
  export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
37
+ export { PG_LOCK_NOT_AVAILABLE, resolveDdlLockTimeout, resolveDdlMaxLockAttempts, ddlLockRetryBackoffMs, type DdlLockEnv, } from './ddlLock.js';
37
38
  export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, type BackfillValue, type MigrationStep, type FieldChanges, type FieldColumnChange, type FieldTypeChange, type NullabilityChange, type EnumValuesChange, type IndexChange, type CastSafety, type FieldType, type RenameHints, type MigrationSignal, type MigrationClassification, type WarningCode, type BlockerCode, } from './diff.js';
38
39
  export { generateTypes } from './generate.js';
39
40
  export { query, defineQueries, type QueryDef, type QueryRecord, type Queries, type InferQueryInput, type InferQueryResult, } from './queries.js';
@@ -28,13 +28,19 @@ export { field, indexed, getFieldMeta } from './field.js';
28
28
  export { relation } from './relation.js';
29
29
  // Tenancy — the single source of truth for how a model's rows are tenant-scoped.
30
30
  export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
31
- // Database plane — which DB a model's rows live in (`tenant` portable to a BYO
32
- // customer DB, `control` = Ablo's own). Sibling axis to `tenancy`.
33
- export { planeSchema, DEFAULT_PLANE } from './plane.js';
31
+ // Model residency — which DB a model's rows live in (`tenant` portable to a
32
+ // customer's own DB, `control` = Ablo's own). Sibling axis to `tenancy`.
33
+ // (Renamed from "plane" that word is the server's tenancy scope.)
34
+ export { residencySchema, DEFAULT_RESIDENCY,
35
+ // Deliberate back-compat re-export of the deprecated aliases:
36
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
37
+ planeSchema,
38
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
39
+ DEFAULT_PLANE, } from './residency.js';
34
40
  // Decomposed sync-delta storage row (P0 of the control/tenant plane split —
35
41
  // see docs/plans/sync-delta-zod-decomposition.md). Describes the existing
36
42
  // `sync_deltas` columns as Zod schemas grouped by subsystem + database plane.
37
- export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_PLANES, } from './sync-delta-row.js';
43
+ export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_RESIDENCY, } from './sync-delta-row.js';
38
44
  // Canonical WIRE delta contract — the broadcast (server→client) projection of
39
45
  // the stored row. The SDK client and the sync-server both derive their
40
46
  // `SyncDelta` type from these via `z.infer` so the contract cannot drift.
@@ -49,13 +55,17 @@ export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwr
49
55
  // falls back to sensible defaults. See sugar.ts for the full pattern.
50
56
  export { mutable, readOnly } from './sugar.js';
51
57
  // Schema definition + type inference
52
- export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './schema.js';
58
+ export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './schema.js';
53
59
  // Schema ⇄ JSON (control-plane transport for hosted multi-tenant)
54
60
  export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, } from './serialize.js';
55
61
  // Schema projection — derive an app's subset from one canonical schema.
56
62
  export { selectModels } from './select.js';
57
63
  // Schema → Postgres DDL (pure; shared by the hosted server and the CLI)
58
64
  export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, } from './ddl.js';
65
+ // Safe-DDL locking knobs (lock_timeout + bounded 55P03 retry) — the ONE
66
+ // reader shared by `ablo migrate` and the hosted executor (ddlExec.ts), so an
67
+ // operator tuning ABLO_SCHEMA_LOCK_* gets the same behavior on both paths.
68
+ export { PG_LOCK_NOT_AVAILABLE, resolveDdlLockTimeout, resolveDdlMaxLockAttempts, ddlLockRetryBackoffMs, } from './ddlLock.js';
59
69
  // Schema diff + migration planning (pure; SQL emission lowered by ddl.ts)
60
70
  export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, } from './diff.js';
61
71
  // Schema → TypeScript type emission (the `generate` half; pure)
@@ -22,7 +22,7 @@ import type { EntityRole, GroupsInput } from './roles.js';
22
22
  import { type FieldMeta } from './field.js';
23
23
  import { type Tenancy, type PolicyInput } from './tenancy.js';
24
24
  export type { ScopedViaRef, Tenancy, PolicyInput } from './tenancy.js';
25
- import { type SchemaPlane } from './plane.js';
25
+ import { type ModelResidency } from './residency.js';
26
26
  import type { ConflictAxis } from '../policy/types.js';
27
27
  export type { ConflictAxis } from '../policy/types.js';
28
28
  /**
@@ -124,7 +124,7 @@ export interface ModelOptions {
124
124
  * provisioning. `control` = Ablo's control plane (sync log, attribution,
125
125
  * audit) — never emitted into a customer DB. See `./plane.ts`.
126
126
  */
127
- plane?: SchemaPlane;
127
+ plane?: ModelResidency;
128
128
  /**
129
129
  * **Axis 2 — sync-group routing.** Decides which delta *channels* a row fans
130
130
  * into. Orthogonal to {@link policy} (read access). One namespaced object
@@ -318,7 +318,7 @@ export interface ModelDef<Shape extends z.ZodRawShape = z.ZodRawShape, R extends
318
318
  readonly tenancy: Tenancy;
319
319
  /** Database plane — `tenant` (default) is portable to a customer DB; `control`
320
320
  * is Ablo-only. See {@link ModelOptions.plane} and `./plane.ts`. */
321
- readonly plane?: SchemaPlane;
321
+ readonly plane?: ModelResidency;
322
322
  /** Scope-root marker. See {@link ModelOptions.scope}. */
323
323
  readonly scope?: boolean | string;
324
324
  /** Membership edge granting identity → scope-root access. See {@link ModelOptions.grants}. */
@@ -24,7 +24,7 @@ import { getFieldMeta, inferFieldMetaFromZod } from './field.js';
24
24
  // Postgres/Supabase RLS), normalized to the canonical `Tenancy` by
25
25
  // `resolvePolicy` at build time.
26
26
  import { resolvePolicy } from './tenancy.js';
27
- import { DEFAULT_PLANE } from './plane.js';
27
+ import { DEFAULT_RESIDENCY } from './residency.js';
28
28
  /** Normalize the `entityRoles` option (single | array | undefined) to an array. */
29
29
  function normalizeEntityRoles(input) {
30
30
  if (!input)
@@ -93,7 +93,7 @@ export function model(shape, relations, options) {
93
93
  // Axis 1 — normalize the `policy` authoring option into the one canonical
94
94
  // tenancy descriptor (defaults to a row-local org column).
95
95
  tenancy: resolvePolicy(options?.policy),
96
- plane: options?.plane ?? DEFAULT_PLANE,
96
+ plane: options?.plane ?? DEFAULT_RESIDENCY,
97
97
  // Axis 2 — unpack the `groups` routing namespace into the wire fields the
98
98
  // server reads (`scope`/`grants`/`entityRoles` on ModelDef/ModelJSON).
99
99
  scope: options?.groups?.root,
@@ -1,3 +1,7 @@
1
+ // Dependency-free leaf (NOT client/auth.ts — that would pull the error
2
+ // registry + credential policy into the schema subpath and close a
3
+ // schema → client → errors → coordination → schema cycle).
4
+ import { ABLO_HOSTED_HTTP_BASE_URL } from '../client/hostedEndpoints.js';
1
5
  function fieldSchema(f) {
2
6
  switch (f.type) {
3
7
  case 'number':
@@ -105,7 +109,7 @@ export function schemaToOpenApi(schema, options = {}) {
105
109
  description: 'Generated from your pushed Ablo schema — these routes are your models. ' +
106
110
  'Authenticate every request with your API key as a Bearer token.',
107
111
  },
108
- servers: [{ url: options.serverUrl ?? 'https://api.abloatai.com/api' }],
112
+ servers: [{ url: options.serverUrl ?? `${ABLO_HOSTED_HTTP_BASE_URL}/api` }],
109
113
  security: [{ bearerAuth: [] }],
110
114
  components: {
111
115
  securitySchemes: {
@@ -158,7 +158,7 @@ export type InferQueryInput<Q extends QueryDef> = z.infer<Q['input']>;
158
158
  * // Array<SlideLayer>
159
159
  * ```
160
160
  */
161
- export type InferQueryResult<S extends Schema, Q extends QueryDef> = Q extends QueryDef<z.ZodType, infer R> ? R extends InferModelNames<S> ? Array<InferModel<S, R>> : never : never;
161
+ export type InferQueryResult<S extends Schema, Q extends QueryDef> = Q extends QueryDef<z.ZodType, infer R> ? R extends InferModelNames<S> ? InferModel<S, R>[] : never : never;
162
162
  /**
163
163
  * Define a typed query set against a schema.
164
164
  *
@@ -56,6 +56,7 @@
56
56
  * and lets the generic loader hydrate via a single
57
57
  * `schema.models[queryDef.returns]` lookup.
58
58
  */
59
+ import { z } from 'zod';
59
60
  import { AbloValidationError } from '../errors.js';
60
61
  // ── query() factory ───────────────────────────────────────────────────────
61
62
  /**
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Model RESIDENCY — which database a model's rows live in. A sibling axis to
3
+ * `tenancy` (which says how rows are isolated *within* a database):
4
+ *
5
+ * - `tenant` — the tenant data plane. For a customer-connected database
6
+ * this is THEIR database; provisioning emits these tables there.
7
+ * - `control` — Ablo's control plane (the sync log, attribution, audit, …).
8
+ * Never emitted into a customer DB; lives only in Ablo's own DB.
9
+ *
10
+ * Named `residency` (not `plane`) because "plane" is the server's tenancy
11
+ * scope — (org, environment[, project, sandbox]) — the dominant meaning in the
12
+ * engine. The serialized model option is still the `plane` KEY (wire format,
13
+ * frozen until the next wire-version bump); only the TS vocabulary moved.
14
+ *
15
+ * P1 of the sync-delta decomposition (`docs/plans/sync-delta-zod-decomposition.md`):
16
+ * declaring the boundary lets provisioning *derive* "what a customer DB gets"
17
+ * (`residency === 'tenant'`) instead of hand-coding it. Defaults to `tenant`.
18
+ */
19
+ import { z } from 'zod';
20
+ export declare const residencySchema: z.ZodEnum<{
21
+ tenant: "tenant";
22
+ control: "control";
23
+ }>;
24
+ export type ModelResidency = z.infer<typeof residencySchema>;
25
+ /** Default residency for a model that doesn't declare one — the tenant data plane. */
26
+ export declare const DEFAULT_RESIDENCY: ModelResidency;
27
+ /** @deprecated Use `residencySchema`. */
28
+ export declare const planeSchema: z.ZodEnum<{
29
+ tenant: "tenant";
30
+ control: "control";
31
+ }>;
32
+ /** @deprecated Use `ModelResidency`. */
33
+ export type SchemaPlane = ModelResidency;
34
+ /** @deprecated Use `DEFAULT_RESIDENCY`. */
35
+ export declare const DEFAULT_PLANE: "tenant";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Model RESIDENCY — which database a model's rows live in. A sibling axis to
3
+ * `tenancy` (which says how rows are isolated *within* a database):
4
+ *
5
+ * - `tenant` — the tenant data plane. For a customer-connected database
6
+ * this is THEIR database; provisioning emits these tables there.
7
+ * - `control` — Ablo's control plane (the sync log, attribution, audit, …).
8
+ * Never emitted into a customer DB; lives only in Ablo's own DB.
9
+ *
10
+ * Named `residency` (not `plane`) because "plane" is the server's tenancy
11
+ * scope — (org, environment[, project, sandbox]) — the dominant meaning in the
12
+ * engine. The serialized model option is still the `plane` KEY (wire format,
13
+ * frozen until the next wire-version bump); only the TS vocabulary moved.
14
+ *
15
+ * P1 of the sync-delta decomposition (`docs/plans/sync-delta-zod-decomposition.md`):
16
+ * declaring the boundary lets provisioning *derive* "what a customer DB gets"
17
+ * (`residency === 'tenant'`) instead of hand-coding it. Defaults to `tenant`.
18
+ */
19
+ import { z } from 'zod';
20
+ export const residencySchema = z.enum(['tenant', 'control']);
21
+ /** Default residency for a model that doesn't declare one — the tenant data plane. */
22
+ export const DEFAULT_RESIDENCY = 'tenant';
23
+ // ── Deprecated aliases (published schema subpath) ───────────────────────────
24
+ /** @deprecated Use `residencySchema`. */
25
+ export const planeSchema = residencySchema;
26
+ /** @deprecated Use `DEFAULT_RESIDENCY`. */
27
+ export const DEFAULT_PLANE = DEFAULT_RESIDENCY;
@@ -229,3 +229,28 @@ export declare function composeIdentitySyncGroups(identity: IdentityContext, sch
229
229
  export declare function composeEntitySyncGroups(record: EntityContext, def: {
230
230
  readonly entityRoles?: readonly EntityRole[];
231
231
  }): readonly SyncGroup[];
232
+ /**
233
+ * Apply capability-style intersection to a client-requested sync-group
234
+ * set. Mirrors the cap-bearer path where Biscuit caveats narrow the
235
+ * client's requested set to what's actually authorized. Fully generic —
236
+ * it never inspects the group strings — so it lives here beside the
237
+ * composition helpers rather than in any product schema package
238
+ * (`@ablo/schema` re-exports it for compat).
239
+ *
240
+ * Behaviour:
241
+ * - If the client requested no groups, return the full identity-
242
+ * derived set (the default subscription scope for this participant).
243
+ * - If the client requested a non-empty subset, intersect against the
244
+ * identity-derived allowed set — drop any group the participant is
245
+ * not authorized to subscribe to. Logs the dropped groups via the
246
+ * optional `logDropped` callback for observability.
247
+ * - If the intersection is empty after filtering, fall back to the
248
+ * full allowed set rather than emit `[]` (which would degenerate to
249
+ * the server-side `['default']` fallback and produce silent zero-
250
+ * delta delivery).
251
+ */
252
+ export declare function intersectRequestedWithAllowed(args: {
253
+ readonly requested: readonly string[];
254
+ readonly allowed: readonly string[];
255
+ readonly logDropped?: (dropped: readonly string[]) => void;
256
+ }): readonly string[];
@@ -197,3 +197,40 @@ export function composeEntitySyncGroups(record, def) {
197
197
  }
198
198
  return Array.from(out);
199
199
  }
200
+ /**
201
+ * Apply capability-style intersection to a client-requested sync-group
202
+ * set. Mirrors the cap-bearer path where Biscuit caveats narrow the
203
+ * client's requested set to what's actually authorized. Fully generic —
204
+ * it never inspects the group strings — so it lives here beside the
205
+ * composition helpers rather than in any product schema package
206
+ * (`@ablo/schema` re-exports it for compat).
207
+ *
208
+ * Behaviour:
209
+ * - If the client requested no groups, return the full identity-
210
+ * derived set (the default subscription scope for this participant).
211
+ * - If the client requested a non-empty subset, intersect against the
212
+ * identity-derived allowed set — drop any group the participant is
213
+ * not authorized to subscribe to. Logs the dropped groups via the
214
+ * optional `logDropped` callback for observability.
215
+ * - If the intersection is empty after filtering, fall back to the
216
+ * full allowed set rather than emit `[]` (which would degenerate to
217
+ * the server-side `['default']` fallback and produce silent zero-
218
+ * delta delivery).
219
+ */
220
+ export function intersectRequestedWithAllowed(args) {
221
+ const { requested, allowed, logDropped } = args;
222
+ if (requested.length === 0)
223
+ return allowed;
224
+ const allowedSet = new Set(allowed);
225
+ const accepted = [];
226
+ const dropped = [];
227
+ for (const g of requested) {
228
+ if (allowedSet.has(g))
229
+ accepted.push(g);
230
+ else
231
+ dropped.push(g);
232
+ }
233
+ if (dropped.length > 0 && logDropped)
234
+ logDropped(dropped);
235
+ return accepted.length > 0 ? accepted : allowed;
236
+ }
@@ -23,7 +23,7 @@ import { z } from 'zod';
23
23
  import type { ModelDef, RelationRecord } from './model.js';
24
24
  import type { RelationDef } from './relation.js';
25
25
  import type { IdentityRole } from './roles.js';
26
- export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './roles.js';
26
+ export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './roles.js';
27
27
  /** The set of built-in casing conventions supported by `defineSchema`. */
28
28
  export type CasingConvention = 'snake_case' | 'camelCase';
29
29
  /** Plug point for custom conventions (e.g. mixed legacy databases). */
@@ -228,7 +228,7 @@ export type UpdateValue<S extends Schema, ModelName extends keyof S['models']> =
228
228
  *
229
229
  * Matches Zero's `DeleteID<TableSchema>` from `zql/src/mutate/crud.ts`.
230
230
  */
231
- export type DeleteId<S extends Schema, ModelName extends keyof S['models']> = {
231
+ export interface DeleteId<S extends Schema, ModelName extends keyof S['models']> {
232
232
  id: string;
233
- };
233
+ }
234
234
  export declare function defineSchema<const S extends SchemaRecord>(models: S, options?: DefineSchemaOptions): Schema<S>;