@abloatai/ablo 0.11.1 → 0.11.2

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 (74) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +10 -2
  3. package/dist/Model.d.ts +39 -0
  4. package/dist/Model.js +68 -0
  5. package/dist/auth/credentialPolicy.d.ts +145 -0
  6. package/dist/auth/credentialPolicy.js +130 -0
  7. package/dist/cli.cjs +39 -6
  8. package/dist/client/Ablo.d.ts +39 -88
  9. package/dist/client/Ablo.js +38 -98
  10. package/dist/client/ApiClient.d.ts +10 -1
  11. package/dist/client/ApiClient.js +19 -11
  12. package/dist/client/auth.d.ts +12 -5
  13. package/dist/client/auth.js +2 -1
  14. package/dist/client/createModelProxy.d.ts +49 -10
  15. package/dist/client/createModelProxy.js +6 -0
  16. package/dist/client/httpClient.d.ts +17 -3
  17. package/dist/client/httpClient.js +1 -0
  18. package/dist/client/identity.js +134 -122
  19. package/dist/client/index.d.ts +1 -1
  20. package/dist/client/sessionMint.d.ts +15 -0
  21. package/dist/client/sessionMint.js +86 -0
  22. package/dist/errorCodes.d.ts +2 -0
  23. package/dist/errorCodes.js +2 -0
  24. package/dist/errors.d.ts +3 -2
  25. package/dist/errors.js +3 -2
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +4 -7
  28. package/dist/mutators/RecordingTransaction.js +14 -42
  29. package/dist/react/AbloProvider.d.ts +1 -6
  30. package/dist/react/AbloProvider.js +1 -5
  31. package/dist/react/context.d.ts +1 -31
  32. package/dist/react/context.js +2 -2
  33. package/dist/react/index.d.ts +0 -6
  34. package/dist/react/index.js +0 -7
  35. package/dist/react/useSyncStatus.d.ts +1 -1
  36. package/dist/realtime/index.d.ts +1 -1
  37. package/dist/schema/generate.js +1 -2
  38. package/dist/schema/schema.d.ts +13 -2
  39. package/dist/schema/schema.js +26 -0
  40. package/dist/surface.d.ts +29 -0
  41. package/dist/surface.js +60 -0
  42. package/dist/sync/ConnectionManager.d.ts +16 -5
  43. package/dist/sync/ConnectionManager.js +42 -7
  44. package/dist/transactions/TransactionQueue.d.ts +0 -11
  45. package/dist/transactions/TransactionQueue.js +12 -56
  46. package/dist/types/global.d.ts +3 -0
  47. package/dist/types/streams.d.ts +0 -22
  48. package/dist/utils/mobx-setup.js +1 -0
  49. package/docs/api-keys.md +49 -0
  50. package/docs/api.md +3 -2
  51. package/docs/client-behavior.md +1 -0
  52. package/docs/coordination.md +75 -21
  53. package/docs/examples/existing-python-backend.md +9 -5
  54. package/docs/examples/scoped-agent.md +1 -1
  55. package/docs/guarantees.md +4 -3
  56. package/docs/identity.md +89 -82
  57. package/docs/integration-guide.md +19 -10
  58. package/docs/migration.md +9 -2
  59. package/docs/quickstart.md +6 -2
  60. package/docs/react.md +3 -3
  61. package/docs/schema-contract.md +23 -5
  62. package/llms-full.txt +18 -16
  63. package/llms.txt +6 -6
  64. package/package.json +1 -1
  65. package/dist/api/index.d.ts +0 -10
  66. package/dist/api/index.js +0 -9
  67. package/dist/principal.d.ts +0 -44
  68. package/dist/principal.js +0 -49
  69. package/dist/react/SyncGroupProvider.d.ts +0 -19
  70. package/dist/react/SyncGroupProvider.js +0 -44
  71. package/dist/react/useClaim.d.ts +0 -29
  72. package/dist/react/useClaim.js +0 -42
  73. package/dist/react/usePresence.d.ts +0 -32
  74. package/dist/react/usePresence.js +0 -41
package/dist/errors.d.ts CHANGED
@@ -186,8 +186,9 @@ export declare function formatClaimedErrorMessage(args: {
186
186
  * The target entity is currently claimed by another participant and the caller
187
187
  * asked the SDK not to read/write through that claim.
188
188
  *
189
- * Use `ifClaimed: 'wait'` to wait for the claim to clear, or
190
- * `ifClaimed: 'return'` to inspect active claims yourself.
189
+ * Pass `ifClaimed: 'return'` to inspect active claims yourself instead of
190
+ * throwing; to wait for the claim to clear, take `ablo.<model>.claim({ id })`
191
+ * (it queues fairly) rather than blocking the read.
191
192
  */
192
193
  export declare class AbloClaimedError extends AbloError {
193
194
  readonly type: "AbloClaimedError";
package/dist/errors.js CHANGED
@@ -208,8 +208,9 @@ export function formatClaimedErrorMessage(args) {
208
208
  * The target entity is currently claimed by another participant and the caller
209
209
  * asked the SDK not to read/write through that claim.
210
210
  *
211
- * Use `ifClaimed: 'wait'` to wait for the claim to clear, or
212
- * `ifClaimed: 'return'` to inspect active claims yourself.
211
+ * Pass `ifClaimed: 'return'` to inspect active claims yourself instead of
212
+ * throwing; to wait for the claim to clear, take `ablo.<model>.claim({ id })`
213
+ * (it queues fairly) rather than blocking the read.
213
214
  */
214
215
  export class AbloClaimedError extends AbloError {
215
216
  type = 'AbloClaimedError';
package/dist/index.d.ts CHANGED
@@ -43,7 +43,6 @@
43
43
  * Advanced — opt-in, most apps never import these (each is tagged
44
44
  * "Advanced —" at its export below, with the one situation it's for):
45
45
  * • `dataSource` / `abloSource` — only if your own DB stays canonical
46
- * • `session` / `agent` — only for delegated agent principals
47
46
  * • `defaultPolicy` — only to customize conflict resolution
48
47
  * • `defineMutators` / `createTransaction` — only for custom mutators
49
48
  * If you don't recognize one, you don't need it — the default path covers you.
@@ -51,11 +50,10 @@
51
50
  export { Ablo } from './client/Ablo.js';
52
51
  export type { MutationExecutor } from './interfaces/index.js';
53
52
  export type { HttpClaimApi, InternalAbloOptions } from './client/Ablo.js';
54
- export { createAbloHttpClient, type AbloHttpClientOptions, type AbloHttpClient, type HttpModelClient, } from './client/httpClient.js';
53
+ export { type AbloHttpClientOptions, type AbloHttpClient, type HttpModelClient, } from './client/httpClient.js';
55
54
  export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
56
- export type { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './client/Ablo.js';
55
+ export type { AbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './client/Ablo.js';
57
56
  export type { AbloPersistence } from './client/persistence.js';
58
- export { session, agent } from './principal.js';
59
57
  import { Ablo } from './client/Ablo.js';
60
58
  export default Ablo;
61
59
  export { dataSource, abloSource, sourceEventForOperation, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
@@ -70,6 +68,8 @@ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './cl
70
68
  export type { WriteOptionsInput } from './client/writeOptionsSchema.js';
71
69
  export type { WriteOptions, MutationOptions } from './interfaces/index.js';
72
70
  export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
71
+ export { PUBLIC_MODEL_VERBS, PUBLIC_LIST_OPTION_KEYS, PUBLIC_ABLO_OPTION_KEYS, } from './surface.js';
72
+ export type { ModelVerb, ListOptionKey, AbloOptionKey } from './surface.js';
73
73
  export type { Register, DefaultSyncShape } from './types/global.js';
74
74
  export { defineMutators } from './mutators/defineMutators.js';
75
75
  export { createTransaction, type Transaction } from './mutators/Transaction.js';
package/dist/index.js CHANGED
@@ -43,7 +43,6 @@
43
43
  * Advanced — opt-in, most apps never import these (each is tagged
44
44
  * "Advanced —" at its export below, with the one situation it's for):
45
45
  * • `dataSource` / `abloSource` — only if your own DB stays canonical
46
- * • `session` / `agent` — only for delegated agent principals
47
46
  * • `defaultPolicy` — only to customize conflict resolution
48
47
  * • `defineMutators` / `createTransaction` — only for custom mutators
49
48
  * If you don't recognize one, you don't need it — the default path covers you.
@@ -56,17 +55,11 @@
56
55
  // `import Ablo from '@abloatai/ablo'` works; named export so
57
56
  // `import { Ablo }` also compiles.
58
57
  export { Ablo } from './client/Ablo.js';
59
- export { createAbloHttpClient, } from './client/httpClient.js';
60
58
  export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
61
59
  // Participant types live under `Ablo.Participant.*` —
62
60
  // `Ablo.Participant.Joined`, `Ablo.Participant.Manager`,
63
61
  // `Ablo.Participant.JoinOptions`, etc. Same dot-access shape as
64
62
  // `Ablo.Peer`, `Ablo.Claim`. No flat re-exports.
65
- // Advanced — most apps never import this. Principal constructors for
66
- // delegated agent paths (`Ablo({ kind: 'agent', as: session({...}) })`).
67
- // The default `Ablo({ schema, apiKey })` resolves identity from the key;
68
- // reach for these only when minting a delegated agent principal.
69
- export { session, agent } from './principal.js';
70
63
  import { Ablo } from './client/Ablo.js';
71
64
  export default Ablo;
72
65
  // Advanced — most apps never import this. Customer-owned storage adapter
@@ -100,6 +93,10 @@ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './cl
100
93
  // Storage-wedge detection — lets app shells render a recovery screen when the
101
94
  // IndexedDB backing store is stuck (see core/openIDBWithTimeout.ts).
102
95
  export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
96
+ // Machine-checked surface manifest — the SDK's own description of its public
97
+ // verb/option names, compile-time-bound to the real types (see surface.ts).
98
+ // The MCP `get_api_surface` imports these so docs can't name a phantom verb.
99
+ export { PUBLIC_MODEL_VERBS, PUBLIC_LIST_OPTION_KEYS, PUBLIC_ABLO_OPTION_KEYS, } from './surface.js';
103
100
  // Advanced — most apps never import this. Custom (Zero-style) mutators:
104
101
  // `ablo.<model>.create/update/delete` already covers normal writes. Reach
105
102
  // for `defineMutators` only when you need a named, multi-step mutation with
@@ -59,55 +59,27 @@ function wrapMutateForKey(modelKey, mutate, store, inverses, forwards) {
59
59
  // wider shape is exactly right.
60
60
  return model.toJSON();
61
61
  };
62
+ // Before-image for the undo inverse. Delegates to `Model.capturePreviousValues`
63
+ // — the SINGLE shared implementation (the stream path's
64
+ // `TransactionQueue.extractPreviousData` calls the same method). `fallbackToLive`
65
+ // is ON here: the manual-record path wants the live value as a last resort for
66
+ // a field that was neither pre-mutated nor in the original snapshot. (The
67
+ // stream path passes `false` so it can omit-and-drop instead — that flag is
68
+ // the one intentional difference between the two callers.)
62
69
  const snapshotFields = (id, fieldNames) => {
63
70
  const model = store.pool.get(id);
64
71
  if (!model)
65
72
  return null;
66
- const out = {};
67
- // `modifiedProperties` is populated by M1's `observe()` listener the
68
- // moment the caller mutates an observable field directly. Thanks to
69
- // `Model.propertyChanged`'s first-old-wins policy, `.old` holds the TRUE
70
- // pre-session baseline even after many in-place mutations (e.g. a drag
71
- // frame loop). That makes it the authoritative source for the undo
72
- // inverse when the caller pre-mutates before invoking the mutator.
73
- //
74
- // Fallback chain for models/fields that weren't pre-mutated (so no
75
- // `modifiedProperties` entry exists yet): `getOriginalSnapshot()`
76
- // (populated on load/`markAsPersisted`/sync-ack), then the live
77
- // observable. The live read is correct only when the caller didn't
78
- // touch the field first.
79
- const original = model.getOriginalSnapshot();
80
- for (const f of fieldNames) {
81
- if (f === 'id')
82
- continue;
83
- const mod = model.modifiedProperties.get(f);
84
- if (mod) {
85
- out[f] = mod.old;
86
- }
87
- else if (original && f in original) {
88
- out[f] = original[f];
89
- }
90
- else {
91
- out[f] = Reflect.get(model, f);
92
- }
93
- }
94
- return out;
73
+ return model.capturePreviousValues(fieldNames, { fallbackToLive: true });
95
74
  };
96
75
  // After a mutator's `base.update` succeeds, drop the `modifiedProperties`
97
- // entries we snapshotted from. The next mutator call should see THIS
98
- // update's result as its baseline, not the pre-session old value. The
99
- // transaction queue already captured its frozen copy synchronously inside
100
- // `store.save` (via `captureModelChanges`/`extractPreviousData`), so this
101
- // clear is safe for server rollback.
76
+ // entries we snapshotted from so the next mutator call sees THIS update's
77
+ // result as its baseline, not the pre-session old value. The transaction
78
+ // queue already captured its frozen copy synchronously inside `store.save`,
79
+ // so this clear is safe for server rollback. Shared with the stream path via
80
+ // `Model.consumeModifiedFields`.
102
81
  const consumeModifiedFields = (id, fieldNames) => {
103
- const model = store.pool.get(id);
104
- if (!model)
105
- return;
106
- for (const f of fieldNames) {
107
- if (f === 'id')
108
- continue;
109
- model.modifiedProperties.delete(f);
110
- }
82
+ store.pool.get(id)?.consumeModifiedFields(fieldNames);
111
83
  };
112
84
  return {
113
85
  // Overloaded — single row or array. The recorder dispatches the
@@ -36,7 +36,7 @@ import { type SyncStoreContract } from './context.js';
36
36
  * // Build once at module scope — a new instance per render tears down the socket.
37
37
  * const ablo = Ablo({
38
38
  * schema,
39
- * getToken: () =>
39
+ * apiKey: () =>
40
40
  * fetch('/api/ablo-session', { method: 'POST' })
41
41
  * .then((r) => r.json())
42
42
  * .then((d) => d.token),
@@ -120,12 +120,7 @@ export type { EngineParticipant, ParticipantScope, ParticipantStatus };
120
120
  */
121
121
  export interface UseParticipantOptions {
122
122
  readonly scope?: ParticipantScope;
123
- readonly label?: string;
124
- readonly as?: unknown;
125
123
  readonly ttlSeconds?: number | string | null;
126
- readonly agent?: unknown;
127
- readonly idempotencyKey?: string | null;
128
- readonly autoRefreshThresholdSeconds?: number | null;
129
124
  /** Tear down + don't re-join while true. */
130
125
  readonly paused?: boolean;
131
126
  /**
@@ -36,7 +36,7 @@ export function AbloProvider(props) {
36
36
  // REACTIVE binding over it (context + bootstrap gate + error/session
37
37
  // forwarding); it does NOT construct, configure, or own the connection. The
38
38
  // client owns auth, the credential lifecycle (first mint, refresh, and
39
- // wake/online/focus re-mint — see `Ablo({ getToken })`), transport, and
39
+ // wake/online/focus re-mint — see `Ablo({ apiKey })`), transport, and
40
40
  // `dispose()`. The CONSUMER built the client, so the consumer owns teardown;
41
41
  // the provider never disposes it.
42
42
  const engine = client;
@@ -338,10 +338,6 @@ export function useParticipant(opts) {
338
338
  unsubClaims();
339
339
  };
340
340
  }, [participant, paused]);
341
- // `opts.as`, `opts.agent`, `opts.idempotencyKey`, and
342
- // `opts.autoRefreshThresholdSeconds` remain migration placeholders
343
- // for future capability-mint/attenuation wiring. `scope` is already
344
- // active: it opens a multiplexed claim on the engine WebSocket.
345
341
  return { participant, peers, claims, status, error };
346
342
  }
347
343
  /**
@@ -137,24 +137,6 @@ export interface SyncReactContext {
137
137
  * augmentation — see `src/types/global.ts`.
138
138
  */
139
139
  schema?: Schema;
140
- /**
141
- * Optional presence source. When set, `usePresence()` returns this
142
- * value cast to the consumer's `ResolvePresence` type (declared via
143
- * `interface Register { Presence: ... }`). The SDK doesn't own a
144
- * presence wire format — consumers plug whatever backs their cursors,
145
- * status, or activity state (a MobX store, a Zustand slice, a custom
146
- * subscription). The typed-global gives it a call-site-ergonomic
147
- * type without the SDK dictating the transport.
148
- */
149
- presence?: unknown;
150
- /**
151
- * Optional claim initiator. Same pattern as presence — consumers
152
- * plug a function that turns an claim claim into a handle they
153
- * control (WebSocket send, optimistic local update, whatever).
154
- * `useClaim(name)` returns a typed invoker for the named claim
155
- * from `interface Register { Claims: ... }`.
156
- */
157
- beginClaim?: (claimName: string, claim: unknown) => unknown;
158
140
  }
159
141
  export declare const SyncContext: import("react").Context<SyncReactContext | null>;
160
142
  /**
@@ -177,18 +159,6 @@ export interface SyncProviderProps {
177
159
  * their legacy `(schema, modelKey, …)` signatures.
178
160
  */
179
161
  schema?: Schema;
180
- /**
181
- * Optional presence source for `usePresence()`. See
182
- * {@link SyncReactContext.presence} — the consumer plugs whatever
183
- * backs their presence state; the hook returns it with
184
- * `ResolvePresence` typing.
185
- */
186
- presence?: unknown;
187
- /**
188
- * Optional claim initiator for `useClaim()`. See
189
- * {@link SyncReactContext.beginClaim}.
190
- */
191
- beginClaim?: (claimName: string, claim: unknown) => unknown;
192
162
  children?: ReactNode;
193
163
  }
194
164
  /**
@@ -206,4 +176,4 @@ export interface SyncProviderProps {
206
176
  * );
207
177
  * }
208
178
  */
209
- export declare function SyncProvider({ store, organizationId, schema, presence, beginClaim, children, }: SyncProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SyncReactContext | null>>;
179
+ export declare function SyncProvider({ store, organizationId, schema, children, }: SyncProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SyncReactContext | null>>;
@@ -30,6 +30,6 @@ export function useSyncContext() {
30
30
  * );
31
31
  * }
32
32
  */
33
- export function SyncProvider({ store, organizationId, schema, presence, beginClaim, children, }) {
34
- return createElement(SyncContext.Provider, { value: { store, organizationId, schema, presence, beginClaim } }, children);
33
+ export function SyncProvider({ store, organizationId, schema, children, }) {
34
+ return createElement(SyncContext.Provider, { value: { store, organizationId, schema } }, children);
35
35
  }
@@ -6,7 +6,6 @@
6
6
  * — owns sync engine + multiplayer lifecycle; the `fallback` prop
7
7
  * gates children on first bootstrap. Pass `fallback="passthrough"`
8
8
  * to disable the gate.
9
- * <SyncGroupProvider id="matter:..."> — per-entity scope
10
9
  * <ClientSideSuspense fallback={<Skeleton/>}> — NESTED gate inside an
11
10
  * already-ready provider. Use only when you need a separate gate
12
11
  * for a heavy subtree (e.g. a canvas) while app chrome renders
@@ -29,8 +28,6 @@
29
28
  * Multiplayer (always available — `<AbloProvider>` always constructs a client):
30
29
  * useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
31
30
  * useParticipant({ scope }) — join multiplayer for a scope, get peers/claims
32
- * usePresence() — typed presence view
33
- * useClaim(name) — typed claim dispatcher
34
31
  *
35
32
  * ── Breaking changes from v0.2.x ───────────────────────────────────
36
33
  * Removed: <SyncProvider>, SyncContext, useSyncContext — folded into
@@ -45,7 +42,6 @@
45
42
  */
46
43
  export type { DefaultSyncShape, ResolveSchema, ResolvePresence, ResolveClaims, ResolveUserMeta, ResolveModelKey, } from '../types/global.js';
47
44
  export { AbloProvider, useParticipant, usePeers, useSync, useSyncStore, type AbloProviderProps, type ParticipantScope, type ParticipantStatus, type UseParticipantOptions, type UseParticipantReturn, type MeshParticipantStatus, } from './AbloProvider.js';
48
- export { SyncGroupProvider, useSyncGroup, type SyncGroupProviderProps, } from './SyncGroupProvider.js';
49
45
  export { ClientSideSuspense, type ClientSideSuspenseProps, } from './ClientSideSuspense.js';
50
46
  export { DefaultFallback } from './DefaultFallback.js';
51
47
  export type { SyncStoreContract } from './context.js';
@@ -59,6 +55,4 @@ export type { ReaderActions, ReaderFindOptions } from '../mutators/readerActions
59
55
  export { useMutators, type MutatorInvokers, type InvokerFor, type UseMutatorsOptions, } from './useMutators.js';
60
56
  export { useUndoScope, type UseUndoScopeResult } from './useUndoScope.js';
61
57
  export { useAblo, type UseAbloHydratedModelResult, type UseAbloModelOptions, type UseAbloModelResult, } from './useAblo.js';
62
- export { usePresence } from './usePresence.js';
63
- export { useClaim } from './useClaim.js';
64
58
  export { ModelScope } from '../types/index.js';
@@ -6,7 +6,6 @@
6
6
  * — owns sync engine + multiplayer lifecycle; the `fallback` prop
7
7
  * gates children on first bootstrap. Pass `fallback="passthrough"`
8
8
  * to disable the gate.
9
- * <SyncGroupProvider id="matter:..."> — per-entity scope
10
9
  * <ClientSideSuspense fallback={<Skeleton/>}> — NESTED gate inside an
11
10
  * already-ready provider. Use only when you need a separate gate
12
11
  * for a heavy subtree (e.g. a canvas) while app chrome renders
@@ -29,8 +28,6 @@
29
28
  * Multiplayer (always available — `<AbloProvider>` always constructs a client):
30
29
  * useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
31
30
  * useParticipant({ scope }) — join multiplayer for a scope, get peers/claims
32
- * usePresence() — typed presence view
33
- * useClaim(name) — typed claim dispatcher
34
31
  *
35
32
  * ── Breaking changes from v0.2.x ───────────────────────────────────
36
33
  * Removed: <SyncProvider>, SyncContext, useSyncContext — folded into
@@ -45,7 +42,6 @@
45
42
  */
46
43
  // ── Umbrella provider + lifecycle hooks ────────────────────────────
47
44
  export { AbloProvider, useParticipant, usePeers, useSync, useSyncStore, } from './AbloProvider.js';
48
- export { SyncGroupProvider, useSyncGroup, } from './SyncGroupProvider.js';
49
45
  export { ClientSideSuspense, } from './ClientSideSuspense.js';
50
46
  export { DefaultFallback } from './DefaultFallback.js';
51
47
  // ── Status + errors + identity ─────────────────────────────────────
@@ -63,8 +59,5 @@ export { useReactive } from './useReactive.js';
63
59
  export { useMutators, } from './useMutators.js';
64
60
  export { useUndoScope } from './useUndoScope.js';
65
61
  export { useAblo, } from './useAblo.js';
66
- // ── Presence + claim (typed via Register module augmentation) ─────
67
- export { usePresence } from './usePresence.js';
68
- export { useClaim } from './useClaim.js';
69
62
  // ── ModelScope re-export ───────────────────────────────────────────
70
63
  export { ModelScope } from '../types/index.js';
@@ -17,7 +17,7 @@
17
17
  * `reason` carries the human-readable close reason when available.
18
18
  * - `disconnected` — network failure, server error, or the retry loop
19
19
  * gave up. Show the offline / error UI.
20
- * - `needs-auth` — server rejected the session (1008/4001/4003). The
20
+ * - `needs-auth` — server rejected the auth token (1008/4001/4003). The
21
21
  * consumer's `onSessionExpired` callback has already been invoked
22
22
  * by `<AbloProvider>`; this variant exists for UI that wants to
23
23
  * reflect the auth state itself.
@@ -5,6 +5,6 @@
5
5
  * subscriptions, presence, offline queueing, and a long-lived WebSocket.
6
6
  */
7
7
  export { Ablo, computeFKDepthPriority } from '../client/Ablo.js';
8
- export type { AbloOptions, InternalAbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelOperations, } from '../client/Ablo.js';
8
+ export type { AbloOptions, InternalAbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelOperations, } from '../client/Ablo.js';
9
9
  import { Ablo } from '../client/Ablo.js';
10
10
  export default Ablo;
@@ -14,8 +14,7 @@
14
14
  * unions). Relations are resolved by the runtime SDK's typed accessors and are
15
15
  * not expanded here.
16
16
  */
17
- /** The base columns every model carries (mirrors `baseFieldsSchema`). */
18
- const BASE_FIELDS = ['id', 'createdAt', 'updatedAt', 'organizationId', 'createdBy'];
17
+ import { BASE_FIELDS } from './schema.js';
19
18
  function tsType(meta) {
20
19
  switch (meta.type) {
21
20
  case 'string':
@@ -78,6 +78,15 @@ export declare const baseFieldsSchema: z.ZodObject<{
78
78
  organizationId: z.ZodOptional<z.ZodString>;
79
79
  createdBy: z.ZodOptional<z.ZodString>;
80
80
  }, z.core.$strip>;
81
+ /**
82
+ * The base-column names every model carries automatically (the keys of
83
+ * {@link baseFieldsSchema}). The single source of truth — `generate.ts`
84
+ * imports this to avoid double-emitting a redeclared base column, and the
85
+ * `defineSchema` field loop uses it to reject a model that tries to redeclare
86
+ * one (Zod `.merge` would otherwise silently overwrite the base field with the
87
+ * user's, producing a `string & Date` type that breaks the build).
88
+ */
89
+ export declare const BASE_FIELDS: readonly ["id", "createdAt", "updatedAt", "organizationId", "createdBy"];
81
90
  /** The base fields type — pure data columns. */
82
91
  export type BaseModelFields = z.infer<typeof baseFieldsSchema>;
83
92
  /**
@@ -138,7 +147,8 @@ export type Model<A, B = never> = [B] extends [never] ? A extends keyof Register
138
147
  * Drizzle deprecated its own `InferModel` for the same reason. Kept as an
139
148
  * alias; no behavior difference.
140
149
  */
141
- export type InferModel<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape, infer R, infer C> ? z.infer<z.ZodObject<Shape>> & BaseModelFields & BaseModelMethods & InferComputed<C> & InferRelations<S, R> : never;
150
+ export type InferModel<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape, infer R, infer C> ? // `Omit<…, keyof BaseModelFields>` so a model that (wrongly) redeclares a
151
+ Omit<z.infer<z.ZodObject<Shape>>, keyof BaseModelFields> & BaseModelFields & BaseModelMethods & InferComputed<C> & InferRelations<S, R> : never;
142
152
  /**
143
153
  * Infer relation accessor types from a model's relations record.
144
154
  *
@@ -184,7 +194,8 @@ export type InferComputed<C> = string extends keyof C ? unknown : {
184
194
  * // createdAt, updatedAt are NOT accepted — they're auto-generated
185
195
  * ```
186
196
  */
187
- export type InferCreate<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape> ? z.input<z.ZodObject<Shape>> & Partial<BaseModelFields> : never;
197
+ export type InferCreate<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape> ? // Same reserved-field guard as InferModel: drop any (wrongly) redeclared
198
+ Omit<z.input<z.ZodObject<Shape>>, keyof BaseModelFields> & Partial<BaseModelFields> : never;
188
199
  /**
189
200
  * Extract all model names from a schema.
190
201
  */
@@ -58,6 +58,21 @@ export const baseFieldsSchema = z.object({
58
58
  organizationId: z.string().optional(),
59
59
  createdBy: z.string().optional(),
60
60
  });
61
+ /**
62
+ * The base-column names every model carries automatically (the keys of
63
+ * {@link baseFieldsSchema}). The single source of truth — `generate.ts`
64
+ * imports this to avoid double-emitting a redeclared base column, and the
65
+ * `defineSchema` field loop uses it to reject a model that tries to redeclare
66
+ * one (Zod `.merge` would otherwise silently overwrite the base field with the
67
+ * user's, producing a `string & Date` type that breaks the build).
68
+ */
69
+ export const BASE_FIELDS = [
70
+ 'id',
71
+ 'createdAt',
72
+ 'updatedAt',
73
+ 'organizationId',
74
+ 'createdBy',
75
+ ];
61
76
  // ── Factory ───────────────────────────────────────────────────────────────
62
77
  /**
63
78
  * Define a sync engine schema.
@@ -146,6 +161,17 @@ export function defineSchema(models, options) {
146
161
  // failure immediate and unambiguous.
147
162
  for (const fieldName of Object.keys(def.shape)) {
148
163
  assertRoundTrippableCamelCase(name, fieldName);
164
+ // Reserved base columns are merged in below via `baseFieldsSchema.merge`,
165
+ // and Zod `.merge` silently OVERWRITES the base field with the user's —
166
+ // e.g. a model declaring `createdAt: z.string()` ends up with a field
167
+ // typed `string & Date`, which breaks the build. Reject the collision at
168
+ // definition time so the author sees an unambiguous error instead.
169
+ if (BASE_FIELDS.includes(fieldName)) {
170
+ throw new AbloValidationError(`[defineSchema] ${name}.${fieldName}: field \`${fieldName}\` collides with a ` +
171
+ `reserved field that the SDK provides automatically ` +
172
+ `(${BASE_FIELDS.join(', ')}). Remove it from your model — redeclaring it ` +
173
+ `produces a \`string & Date\` type and breaks the build.`, { code: 'schema_reserved_field', param: `${name}.${fieldName}` });
174
+ }
149
175
  }
150
176
  validators[name] = baseFieldsSchema.merge(def.schema);
151
177
  // Resolve every relation's `foreignKeyColumn` once, now. The builder
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Machine-checked public API-surface manifest — the SDK owns the description of
3
+ * its OWN surface, bound to the real exported types at COMPILE TIME so the MCP
4
+ * `get_api_surface` / docs can never drift from reality.
5
+ *
6
+ * This exists because the hand-authored surface (apps/sync-web/.../api-surface.ts)
7
+ * once named `load` / `count` / `scope` — verbs/options that don't exist — with no
8
+ * coupling to the code. The fix: the name lists live HERE, next to the types, and
9
+ * each is proven EXACTLY equal to the keys of its source interface via
10
+ * `Expect<Equal<…>>`. Add or remove a verb/option without updating the matching
11
+ * tuple and THIS FILE FAILS TO COMPILE (the `Equal` constraint is checked eagerly
12
+ * at the alias declaration — both directions: no phantom name, no missing name).
13
+ *
14
+ * Consumers (the MCP `get_api_surface`) import these NAME tuples and build their
15
+ * prose from them, so a summary can never reference a verb that doesn't exist.
16
+ * NAMES are guaranteed; descriptions stay hand-written (prose can't be type-checked).
17
+ */
18
+ /** Every method on `ablo.<model>` (the stateful `ModelOperations`). The single
19
+ * source of truth for the model-verb names the docs/MCP may describe. */
20
+ export declare const PUBLIC_MODEL_VERBS: readonly ["retrieve", "list", "get", "getAll", "getCount", "create", "update", "delete", "claim", "watch", "onChange"];
21
+ /** Keys accepted by `list`/`getAll`/`onChange` options (`LocalReadOptions`).
22
+ * Note `state` (lifecycle filter) — NOT `scope` (a historic doc drift). */
23
+ export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orderBy", "limit", "offset", "state"];
24
+ /** Public keys of `AbloOptions`. `schema` is required; the rest are optional
25
+ * (the locked happy path is `Ablo({ schema, apiKey, databaseUrl, transport })`). */
26
+ export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "databaseUrl", "persistence", "transport", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
27
+ export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
28
+ export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
29
+ export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Machine-checked public API-surface manifest — the SDK owns the description of
3
+ * its OWN surface, bound to the real exported types at COMPILE TIME so the MCP
4
+ * `get_api_surface` / docs can never drift from reality.
5
+ *
6
+ * This exists because the hand-authored surface (apps/sync-web/.../api-surface.ts)
7
+ * once named `load` / `count` / `scope` — verbs/options that don't exist — with no
8
+ * coupling to the code. The fix: the name lists live HERE, next to the types, and
9
+ * each is proven EXACTLY equal to the keys of its source interface via
10
+ * `Expect<Equal<…>>`. Add or remove a verb/option without updating the matching
11
+ * tuple and THIS FILE FAILS TO COMPILE (the `Equal` constraint is checked eagerly
12
+ * at the alias declaration — both directions: no phantom name, no missing name).
13
+ *
14
+ * Consumers (the MCP `get_api_surface`) import these NAME tuples and build their
15
+ * prose from them, so a summary can never reference a verb that doesn't exist.
16
+ * NAMES are guaranteed; descriptions stay hand-written (prose can't be type-checked).
17
+ */
18
+ // ── the per-`ablo.<model>` verb surface ────────────────────────────────────
19
+ /** Every method on `ablo.<model>` (the stateful `ModelOperations`). The single
20
+ * source of truth for the model-verb names the docs/MCP may describe. */
21
+ export const PUBLIC_MODEL_VERBS = [
22
+ 'retrieve',
23
+ 'list',
24
+ 'get',
25
+ 'getAll',
26
+ 'getCount',
27
+ 'create',
28
+ 'update',
29
+ 'delete',
30
+ 'claim',
31
+ 'watch',
32
+ 'onChange',
33
+ ];
34
+ // ── the read/list query option surface ─────────────────────────────────────
35
+ /** Keys accepted by `list`/`getAll`/`onChange` options (`LocalReadOptions`).
36
+ * Note `state` (lifecycle filter) — NOT `scope` (a historic doc drift). */
37
+ export const PUBLIC_LIST_OPTION_KEYS = [
38
+ 'where',
39
+ 'filter',
40
+ 'orderBy',
41
+ 'limit',
42
+ 'offset',
43
+ 'state',
44
+ ];
45
+ // ── the `Ablo({ … })` constructor option surface ───────────────────────────
46
+ /** Public keys of `AbloOptions`. `schema` is required; the rest are optional
47
+ * (the locked happy path is `Ablo({ schema, apiKey, databaseUrl, transport })`). */
48
+ export const PUBLIC_ABLO_OPTION_KEYS = [
49
+ 'schema',
50
+ 'apiKey',
51
+ 'databaseUrl',
52
+ 'persistence',
53
+ 'transport',
54
+ 'authToken',
55
+ 'baseURL',
56
+ 'fetch',
57
+ 'defaultHeaders',
58
+ 'defaultQuery',
59
+ 'dangerouslyAllowBrowser',
60
+ ];
@@ -20,18 +20,29 @@
20
20
  * Designed to be embedded by `BaseSyncedStore`: one instance per store,
21
21
  * started on first successful connect, disposed on teardown.
22
22
  *
23
- * CONNECTED ──► OFFLINE ──► PROBING_NETWORK ──► RECONNECTING ──► CONNECTED
24
- *
25
- *
26
- * WAITING_FOR_NETWORK SESSION_EXPIRED BACKOFF ──► PROBING_NETWORK
23
+ * CONNECTED ──(socket drop)──► PROBING_NETWORK ──► RECONNECTING ──► CONNECTED
24
+ *
25
+ * (network lost)
26
+ *SESSION_EXPIRED BACKOFF ──► PROBING_NETWORK
27
+ * OFFLINE ──(online)──► PROBING_NETWORK
28
+ * │
29
+ * ▼
30
+ * WAITING_FOR_NETWORK
27
31
  *
28
- * Includes two fixes over the original app-side FSM:
32
+ * Includes three fixes over the original app-side FSM:
29
33
  * 1. `backoff` accepts `NETWORK_ONLINE` / `TAB_VISIBLE` — jumps to
30
34
  * probing immediately when the network comes back, without
31
35
  * waiting for the backoff timer to elapse.
32
36
  * 2. `scheduleBackoff` parks in `waiting_for_network` (resetting
33
37
  * `attempt`) when `navigator.onLine === false` at max retries,
34
38
  * instead of hard-reloading an already-offline browser.
39
+ * 3. A socket drop (`WS_DISCONNECTED`, typically code 1006) goes
40
+ * STRAIGHT to `probing_network`, not the passive `offline` state.
41
+ * 1006 is browser-local and carries no connectivity signal, so on a
42
+ * healthy machine no `online`/`offline` event ever fires — parking in
43
+ * `offline` stranded recovery until the 30s watchdog, long enough for
44
+ * queued commits to roll back. Only a genuine OS-level `NETWORK_LOST`
45
+ * parks in `offline` and waits for the `online` event.
35
46
  */
36
47
  import { type ProbeResult } from './NetworkProbe.js';
37
48
  import type { AuthTokenGetter } from '../auth/credentialSource.js';