@abloatai/ablo 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +3 -3
  4. package/dist/BaseSyncedStore.d.ts +72 -195
  5. package/dist/BaseSyncedStore.js +240 -625
  6. package/dist/Database.d.ts +8 -9
  7. package/dist/Database.js +9 -14
  8. package/dist/LazyReferenceCollection.js +5 -0
  9. package/dist/Model.js +6 -8
  10. package/dist/ModelRegistry.d.ts +25 -6
  11. package/dist/ModelRegistry.js +12 -6
  12. package/dist/NetworkMonitor.js +1 -1
  13. package/dist/ObjectPool.js +9 -3
  14. package/dist/SyncClient.d.ts +15 -5
  15. package/dist/SyncClient.js +152 -66
  16. package/dist/SyncEngineContext.d.ts +1 -3
  17. package/dist/SyncEngineContext.js +1 -2
  18. package/dist/agent/Agent.d.ts +21 -8
  19. package/dist/agent/Agent.js +26 -13
  20. package/dist/ai-sdk/coordination-context.js +9 -3
  21. package/dist/ai-sdk/wrap.d.ts +1 -1
  22. package/dist/ai-sdk/wrap.js +1 -1
  23. package/dist/auth/credentialPolicy.d.ts +10 -2
  24. package/dist/auth/credentialSource.d.ts +1 -13
  25. package/dist/auth/credentialSource.js +6 -13
  26. package/dist/auth/index.js +3 -3
  27. package/dist/cli.cjs +328 -262
  28. package/dist/client/Ablo.d.ts +35 -764
  29. package/dist/client/Ablo.js +64 -688
  30. package/dist/client/ApiClient.d.ts +14 -1
  31. package/dist/client/ApiClient.js +63 -9
  32. package/dist/client/auth.d.ts +8 -17
  33. package/dist/client/auth.js +42 -6
  34. package/dist/client/consoleLogger.d.ts +36 -0
  35. package/dist/client/consoleLogger.js +45 -0
  36. package/dist/client/createInternalComponents.js +1 -1
  37. package/dist/client/createModelProxy.js +10 -6
  38. package/dist/client/credentialEndpoint.d.ts +63 -0
  39. package/dist/client/credentialEndpoint.js +87 -0
  40. package/dist/client/hostedEndpoints.d.ts +24 -0
  41. package/dist/client/hostedEndpoints.js +24 -0
  42. package/dist/client/httpClient.d.ts +1 -1
  43. package/dist/client/identity.js +2 -1
  44. package/dist/client/modelRegistration.d.ts +14 -0
  45. package/dist/client/modelRegistration.js +321 -0
  46. package/dist/client/options.d.ts +396 -0
  47. package/dist/client/options.js +10 -0
  48. package/dist/client/resourceTypes.d.ts +344 -0
  49. package/dist/client/resourceTypes.js +13 -0
  50. package/dist/client/schemaConfig.d.ts +56 -0
  51. package/dist/client/schemaConfig.js +188 -0
  52. package/dist/client/sessionMint.d.ts +1 -1
  53. package/dist/client/validateAbloOptions.js +1 -1
  54. package/dist/client/wsMutationExecutor.d.ts +33 -0
  55. package/dist/client/wsMutationExecutor.js +77 -0
  56. package/dist/context.js +0 -3
  57. package/dist/coordination/index.d.ts +8 -1
  58. package/dist/coordination/index.js +24 -1
  59. package/dist/coordination/trace.d.ts +0 -1
  60. package/dist/coordination/trace.js +0 -1
  61. package/dist/core/DatabaseManager.d.ts +0 -1
  62. package/dist/core/DatabaseManager.js +26 -24
  63. package/dist/core/QueryProcessor.js +7 -8
  64. package/dist/core/QueryView.d.ts +14 -2
  65. package/dist/core/QueryView.js +4 -5
  66. package/dist/core/StoreManager.d.ts +2 -2
  67. package/dist/core/StoreManager.js +5 -1
  68. package/dist/core/index.d.ts +1 -1
  69. package/dist/core/openIDBWithTimeout.js +15 -11
  70. package/dist/core/query-utils.js +1 -1
  71. package/dist/core/storeContract.d.ts +143 -0
  72. package/dist/core/storeContract.js +16 -0
  73. package/dist/errorCodes.d.ts +11 -2
  74. package/dist/errorCodes.js +155 -137
  75. package/dist/errors.d.ts +18 -7
  76. package/dist/errors.js +14 -1
  77. package/dist/index.js +5 -0
  78. package/dist/interfaces/index.d.ts +6 -20
  79. package/dist/keys/index.js +18 -9
  80. package/dist/mutators/UndoManager.js +6 -1
  81. package/dist/mutators/defineMutators.d.ts +1 -3
  82. package/dist/policy/types.d.ts +1 -1
  83. package/dist/query/client.d.ts +11 -0
  84. package/dist/query/client.js +81 -52
  85. package/dist/react/AbloProvider.d.ts +6 -10
  86. package/dist/react/AbloProvider.js +22 -15
  87. package/dist/react/context.d.ts +2 -118
  88. package/dist/react/useAblo.js +1 -1
  89. package/dist/react/useErrorListener.js +1 -1
  90. package/dist/react/useMutationFailureListener.js +1 -1
  91. package/dist/react/useMutators.js +7 -1
  92. package/dist/react/useReactive.js +0 -1
  93. package/dist/react/useUndoScope.js +2 -2
  94. package/dist/schema/ddlLock.d.ts +39 -0
  95. package/dist/schema/ddlLock.js +51 -0
  96. package/dist/schema/field.js +1 -1
  97. package/dist/schema/index.d.ts +4 -3
  98. package/dist/schema/index.js +15 -5
  99. package/dist/schema/model.d.ts +3 -3
  100. package/dist/schema/model.js +2 -2
  101. package/dist/schema/openapi.js +5 -1
  102. package/dist/schema/queries.d.ts +1 -1
  103. package/dist/schema/queries.js +1 -0
  104. package/dist/schema/residency.d.ts +35 -0
  105. package/dist/schema/residency.js +27 -0
  106. package/dist/schema/roles.d.ts +25 -0
  107. package/dist/schema/roles.js +37 -0
  108. package/dist/schema/schema.d.ts +3 -3
  109. package/dist/schema/schema.js +11 -7
  110. package/dist/schema/serialize.d.ts +8 -8
  111. package/dist/schema/sync-delta-row.d.ts +2 -2
  112. package/dist/schema/sync-delta-row.js +2 -2
  113. package/dist/server/storage-mode.d.ts +8 -1
  114. package/dist/server/storage-mode.js +14 -4
  115. package/dist/source/adapter.d.ts +1 -1
  116. package/dist/source/adapters/drizzle.js +4 -3
  117. package/dist/source/adapters/kysely.js +4 -3
  118. package/dist/source/adapters/memory.js +1 -1
  119. package/dist/source/adapters/prisma.js +5 -4
  120. package/dist/source/conformance.js +10 -8
  121. package/dist/source/connector.js +4 -2
  122. package/dist/source/factory.d.ts +106 -0
  123. package/dist/source/factory.js +273 -0
  124. package/dist/source/index.d.ts +11 -461
  125. package/dist/source/index.js +14 -416
  126. package/dist/source/next.d.ts +1 -1
  127. package/dist/source/next.js +1 -1
  128. package/dist/source/pushQueue.d.ts +1 -1
  129. package/dist/source/pushQueue.js +6 -3
  130. package/dist/source/signing.d.ts +59 -0
  131. package/dist/source/signing.js +142 -0
  132. package/dist/source/types.d.ts +334 -0
  133. package/dist/source/types.js +44 -0
  134. package/dist/stores/ObjectStore.d.ts +1 -1
  135. package/dist/stores/ObjectStore.js +23 -23
  136. package/dist/stores/SyncActionStore.d.ts +1 -1
  137. package/dist/stores/SyncActionStore.js +70 -35
  138. package/dist/surface.d.ts +1 -1
  139. package/dist/surface.js +1 -0
  140. package/dist/sync/AreaOfInterestManager.d.ts +2 -2
  141. package/dist/sync/AreaOfInterestManager.js +1 -1
  142. package/dist/sync/BootstrapHelper.d.ts +1 -3
  143. package/dist/sync/BootstrapHelper.js +29 -8
  144. package/dist/sync/ConnectionManager.d.ts +8 -0
  145. package/dist/sync/ConnectionManager.js +19 -4
  146. package/dist/sync/HydrationCoordinator.d.ts +12 -0
  147. package/dist/sync/HydrationCoordinator.js +25 -5
  148. package/dist/sync/NetworkProbe.d.ts +3 -2
  149. package/dist/sync/NetworkProbe.js +7 -6
  150. package/dist/sync/SyncWebSocket.d.ts +71 -120
  151. package/dist/sync/SyncWebSocket.js +324 -630
  152. package/dist/sync/awaitClaimGrant.js +27 -19
  153. package/dist/sync/bootstrapApply.d.ts +60 -0
  154. package/dist/sync/bootstrapApply.js +65 -0
  155. package/dist/sync/commitFrames.d.ts +43 -0
  156. package/dist/sync/commitFrames.js +94 -0
  157. package/dist/sync/createClaimStream.js +3 -2
  158. package/dist/sync/createSnapshot.d.ts +1 -3
  159. package/dist/sync/credentialLifecycle.d.ts +175 -0
  160. package/dist/sync/credentialLifecycle.js +329 -0
  161. package/dist/sync/deltaPipeline.d.ts +111 -0
  162. package/dist/sync/deltaPipeline.js +262 -0
  163. package/dist/sync/groupChange.d.ts +111 -0
  164. package/dist/sync/groupChange.js +246 -0
  165. package/dist/sync/heartbeat.d.ts +62 -0
  166. package/dist/sync/heartbeat.js +91 -0
  167. package/dist/sync/participants.d.ts +8 -8
  168. package/dist/sync/syncCursor.d.ts +44 -0
  169. package/dist/sync/syncCursor.js +59 -0
  170. package/dist/sync/syncPlan.d.ts +62 -0
  171. package/dist/sync/syncPlan.js +51 -0
  172. package/dist/sync/wsFrameHandlers.d.ts +117 -0
  173. package/dist/sync/wsFrameHandlers.js +378 -0
  174. package/dist/testing/fixtures/bootstrap.d.ts +9 -11
  175. package/dist/testing/fixtures/deltas.d.ts +1 -1
  176. package/dist/testing/fixtures/models.js +1 -0
  177. package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
  178. package/dist/testing/helpers/sync-engine-harness.js +2 -1
  179. package/dist/testing/index.d.ts +1 -1
  180. package/dist/testing/index.js +1 -1
  181. package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
  182. package/dist/testing/mocks/MockSyncContext.js +1 -32
  183. package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
  184. package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
  185. package/dist/transactions/TransactionQueue.d.ts +20 -56
  186. package/dist/transactions/TransactionQueue.js +101 -487
  187. package/dist/transactions/TransactionStore.d.ts +18 -0
  188. package/dist/transactions/TransactionStore.js +51 -0
  189. package/dist/transactions/coalesceRules.d.ts +34 -0
  190. package/dist/transactions/coalesceRules.js +117 -0
  191. package/dist/transactions/commitPayload.d.ts +134 -0
  192. package/dist/transactions/commitPayload.js +152 -0
  193. package/dist/transactions/deltaConfirmation.d.ts +60 -0
  194. package/dist/transactions/deltaConfirmation.js +223 -0
  195. package/dist/transactions/optimistic.d.ts +24 -0
  196. package/dist/transactions/optimistic.js +45 -0
  197. package/dist/transactions/persistedReplay.d.ts +93 -0
  198. package/dist/transactions/persistedReplay.js +105 -0
  199. package/dist/types/index.d.ts +3 -3
  200. package/dist/types/index.js +0 -5
  201. package/dist/types/modelData.d.ts +12 -0
  202. package/dist/types/modelData.js +11 -0
  203. package/dist/types/participant.d.ts +21 -0
  204. package/dist/types/participant.js +12 -0
  205. package/dist/types/streams.d.ts +8 -14
  206. package/dist/utils/mobx-setup.d.ts +1 -1
  207. package/dist/utils/mobx-setup.js +2 -2
  208. package/dist/wire/errorEnvelope.d.ts +11 -2
  209. package/dist/wire/errorEnvelope.js +14 -2
  210. package/dist/wire/frames.d.ts +90 -0
  211. package/dist/wire/frames.js +55 -1
  212. package/dist/wire/index.d.ts +4 -1
  213. package/dist/wire/index.js +20 -1
  214. package/dist/wire/protocol.d.ts +45 -0
  215. package/dist/wire/protocol.js +45 -0
  216. package/dist/wire/protocolVersion.d.ts +56 -0
  217. package/dist/wire/protocolVersion.js +63 -0
  218. package/docs/api-keys.md +4 -3
  219. package/docs/examples/existing-python-backend.md +3 -3
  220. package/docs/identity.md +4 -4
  221. package/docs/integration-guide.md +1 -1
  222. package/docs/react.md +1 -1
  223. package/docs/sessions.md +5 -7
  224. package/package.json +15 -13
  225. package/dist/client/index.d.ts +0 -36
  226. package/dist/client/index.js +0 -33
  227. package/dist/config/index.d.ts +0 -10
  228. package/dist/config/index.js +0 -12
  229. package/dist/interfaces/headless.d.ts +0 -95
  230. package/dist/interfaces/headless.js +0 -41
  231. package/dist/query/index.d.ts +0 -6
  232. package/dist/query/index.js +0 -5
  233. package/dist/realtime/index.d.ts +0 -10
  234. package/dist/realtime/index.js +0 -9
  235. package/dist/schema/plane.d.ts +0 -23
  236. package/dist/schema/plane.js +0 -19
  237. package/dist/server/next.d.ts +0 -51
  238. package/dist/server/next.js +0 -47
  239. package/dist/sync/OfflineFlush.d.ts +0 -9
  240. package/dist/sync/OfflineFlush.js +0 -22
  241. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  242. package/dist/sync/OfflineTransactionStore.js +0 -263
  243. package/dist/transactions/index.d.ts +0 -16
  244. package/dist/transactions/index.js +0 -7
package/AGENTS.md CHANGED
@@ -6,16 +6,18 @@ Claims don't lock. If another writer holds the row, `claim` waits for them and r
6
6
 
7
7
  ## Start here — scaffold with `ablo init`
8
8
 
9
- Don't hand-write the integration. Run the CLI; it generates the current-API schema, client, Data Source endpoint, and (for Next.js) the browser provider + session route:
9
+ Don't hand-write the integration. Run the CLI; it generates the current-API schema, client, the database connection (logical replication by default, or a signed Data Source endpoint as the fallback), and (for Next.js) the browser provider + session route:
10
10
 
11
11
  - **Scaffold:** `npx ablo init --yes` — flag-driven, never prompts. Override defaults with `--framework <nextjs|vite|remix|vanilla>`, `--auth <apikey|…>`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`. (Plain `ablo init` needs a TTY and will **HANG** in an agent/CI run — always pass `--yes`.)
12
12
  - **Auth:** set `ABLO_API_KEY` in the environment. Do **NOT** run `ablo login` — it opens a browser device flow and blocks an agent.
13
- - **Provision your DB (Data Source mode):** `npx ablo migrate` creates the tables for your Ablo models plus the adapter's bookkeeping tables (`ablo_outbox`, `ablo_idempotency`). It does **not** touch your other tableskeep your own migrations (drizzle-kit / prisma migrate) for auth and anything not in the Ablo schema.
13
+ - **Connect your database — logical replication (the primary path):** `npx ablo connect` prints the setup SQL (`wal_level=logical`, a publication, a `REPLICATION` role); `npx ablo connect --register` registers the source with Ablo in one step. Ablo **consumes your Postgres' logical-replication stream** it never runs DDL on, writes to, owns, or migrates your database, and your application keeps the write path. Registration **is** the enable; there is no tier or flag to pick. (Ablo hosts only the transaction log + coordination, never your rows.)
14
+ - **Fallback — signed Data Source endpoint** (DB can't grant a `REPLICATION` role): the generated `ablo/data-source.ts` exposes one route; Ablo sends signed requests and your app touches its own DB. **Only in this mode** does `npx ablo migrate` provision the adapter's bookkeeping tables (`ablo_outbox`, `ablo_idempotency`) plus your Ablo models — it does **not** touch your other tables. Keep your own migrations (drizzle-kit / prisma migrate) for auth and anything outside the Ablo schema.
15
+ - **No database yet?** A sandbox `sk_test` key holds throwaway **test data** (Stripe-test-mode style) so you can try Ablo before connecting your own Postgres. Test-mode only — in production every row lives in your database.
14
16
  - **Adopt an existing DB schema:** `npx ablo pull prisma [path]` / `pull drizzle <module>` (lossless) or `pull` (live DB, lossy). Writes `ablo/schema.ts`.
15
17
  - **Push your schema — REQUIRED before any write works.** The server keeps its OWN copy of the schema. After you create or edit `ablo/schema.ts`, run `npx ablo push` (one-shot) — or `npx ablo dev --no-watch`. **Skip this and every write to a new or changed model fails with `server_execute_unknown_model`.** (Plain `ablo dev` watches forever — never run it bare in an agent.)
16
18
  - **Other long-running:** `npx ablo logs --no-follow` (default tails forever). `npx ablo mode test|live` ALWAYS pass the argument. `status`, `push`, `pull`, `check`, `generate` are one-shot — safe as-is.
17
19
 
18
- The generated `ablo/data-source.ts` is the whole Data Source endpoint and needs no hand-editing: `dataSourceNext({ schema, apiKey, adapter: prismaDataSource(prisma, schema) })` (or `drizzleDataSource(db, schema)`). The adapter owns commit / idempotency / outbox.
20
+ When you use the signed-endpoint fallback, the generated `ablo/data-source.ts` is the whole endpoint and needs no hand-editing: `dataSourceNext({ schema, apiKey, adapter: prismaDataSource(prisma, schema) })` (or `drizzleDataSource(db, schema)`). The adapter owns commit / idempotency / outbox.
19
21
 
20
22
  ## Rule
21
23
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **`ablo connect --register` works against any deployment.** Registering your database as an Ablo data source over logical replication is now a first-class path everywhere — the gate that could refuse a new connection-string registration is gone. Point `ablo connect --register` at your Postgres and Ablo begins consuming its replication stream while your application keeps owning the write path; registering the source _is_ the enable, with no tier or flag to choose. The signed `dataSource()` endpoint remains the explicit fallback for databases that can't grant replication.
8
+
9
+ - **More engine failures surface as typed errors you can branch on**, instead of opaque `500`s. New `AbloError` codes: `schema_provisioning_forbidden` (`403` — a push tried to create tables in a database where the engine isn't permitted to run DDL), `request_too_large` (`413` — a query or commit body exceeded the size limit), `commit_operation_invalid` (`400` — a malformed commit operation), and `upload_not_configured` (`503` — an upload was attempted with no blob store wired).
10
+
11
+ - **`@abloatai/ablo/wire` now exports the protocol schemas.** `commitOperationSchema`, `commitPayloadSchema`, and the frame schemas let tooling and tests validate client↔engine frames against the same contract the engine enforces.
12
+
13
+ - **Protocol version negotiation is surfaced.** `@abloatai/ablo/wire` exports `PROTOCOL_VERSION`, `MIN_SUPPORTED_PROTOCOL_VERSION`, `WS_CLOSE_PROTOCOL_VERSION`, `PROTOCOL_VERSION_HEADER`, `protocolVersionProblem`, and `ERROR_CONTRACT_VERSION` — the single integer the client and server compare on connect to detect an incompatible peer and fail with a clear reason instead of a cryptic wire error.
14
+
15
+ - **`@abloatai/ablo/coordination` exposes the conflict-policy vocabulary** — `defaultPolicy`, `capabilityPreemptPolicy`, and `interpretConflictAxis` — for server-side consumers building on the claim model. `@abloatai/ablo/schema` gains the role primitives (`identityRole`, `entityRole`, and their types and schemas) for declaring identity- and entity-scoped roles.
16
+
17
+ - **Removed unused API surface.** Dropped the `@abloatai/ablo/server/next` subpath export and the unused `MutationDispatcher` interface (plus its `MockMutationDispatcher` test double) and the offline-mutation-queue internals behind them — none had documented consumers. Every supported entry point (`@abloatai/ablo`, `/ai-sdk`, `/react`, `/source`, `/coordination`, `/schema`, `/keys`, `/auth`, `/wire`, …) is unchanged.
18
+
19
+ - **Internal:** the SDK's largest modules — the `Ablo` client, the data-source runtime, the transaction queue, and the WebSocket sync loop — were decomposed into cohesive leaf modules. No public runtime API or import path changed.
20
+
21
+ - **CLI & docs.** `ablo push` prints a calmer, information-first deploy banner (a production push still requires typing the project name to confirm). The README and `AGENTS.md` now lead with the one-path logical-replication model and clarify that sandbox is test-mode only — in production your rows stay in your database and Ablo holds only the transaction log.
22
+
3
23
  ## 0.25.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -322,10 +322,10 @@ import Ablo from '@abloatai/ablo';
322
322
  import { AbloProvider, useAblo } from '@abloatai/ablo/react';
323
323
  import { schema } from './ablo/schema';
324
324
 
325
- // Build the client once — it authenticates via your session route, no key in the browser.
325
+ // Build the client once — authEndpoint is your session route; no key in the browser.
326
326
  const ablo = Ablo({
327
327
  schema,
328
- apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
328
+ authEndpoint: '/api/ablo-session',
329
329
  });
330
330
 
331
331
  function App() {
@@ -381,7 +381,7 @@ to sync-group strings.
381
381
  // team membership is asserted server-side when the session route mints the token.
382
382
  const ablo = Ablo({
383
383
  schema,
384
- apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
384
+ authEndpoint: '/api/ablo-session',
385
385
  });
386
386
 
387
387
  <AbloProvider client={ablo} userId={user.id}>
@@ -11,6 +11,7 @@
11
11
  * implementation stays in the app's SyncedStore.ts until we incrementally
12
12
  * pull generic methods into this base class.
13
13
  */
14
+ import type { RecoveryClass } from './errorCodes.js';
14
15
  import { ConnectionManager } from './sync/ConnectionManager.js';
15
16
  import { AreaOfInterestManager } from './sync/AreaOfInterestManager.js';
16
17
  import { type ParticipantScope } from './sync/participants.js';
@@ -18,19 +19,22 @@ import type { SyncClient } from './SyncClient.js';
18
19
  import type { Database, BootstrapResult } from './Database.js';
19
20
  import type { ObjectPool } from './ObjectPool.js';
20
21
  import { ModelRegistry } from './ModelRegistry.js';
21
- import { SyncWebSocket, type SyncDelta, type SyncGroupChangePayload, type GroupAddedPayload, type GroupRemovedPayload, type VersionVector, type BootstrapHint, type BootstrapDataEvent, type PresenceUpdateEvent, type EventMap, type DefaultCollaborationEvents } from './sync/SyncWebSocket.js';
22
+ import { SyncWebSocket, type SyncDelta, type SyncGroupChangePayload, type GroupAddedPayload, type GroupRemovedPayload, type BootstrapHint, type BootstrapDataEvent, type PresenceUpdateEvent, type EventMap, type DefaultCollaborationEvents } from './sync/SyncWebSocket.js';
22
23
  import { QueryProcessor } from './core/QueryProcessor.js';
23
24
  import { Model } from './Model.js';
24
25
  import { ModelScope } from './ObjectPool.js';
25
26
  import type { Schema } from './schema/schema.js';
26
- import type { LocalMutation } from './react/context.js';
27
+ import type { SyncStatus, LocalMutation } from './core/storeContract.js';
27
28
  import type { AuthCredentialSource } from './auth/credentialSource.js';
29
+ import type { ModelData } from './types/modelData.js';
30
+ import type { EnrichmentPlanEntry, ForeignKeyIndexSpec } from './sync/syncPlan.js';
31
+ import { type CredentialRefresher } from './sync/credentialLifecycle.js';
32
+ import type { RehydrationStats } from './sync/bootstrapApply.js';
28
33
  /** Constructor type for Model subclasses (accepts abstract classes) */
29
34
  export type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
30
35
  /** Concrete constructor type for instantiation */
31
36
  export type ConcreteModelConstructor<T extends Model> = new (data?: any) => T;
32
- /** Generic record type for model data */
33
- export type ModelData = Record<string, unknown>;
37
+ export type { ModelData } from './types/modelData.js';
34
38
  /** Query result interface */
35
39
  export interface QueryResult<T extends Model> {
36
40
  data: T[];
@@ -38,55 +42,12 @@ export interface QueryResult<T extends Model> {
38
42
  hasMore: boolean;
39
43
  fromCache?: boolean;
40
44
  }
41
- /** A foreign-key index to register on the ObjectPool at construction time. */
42
- export interface ForeignKeyIndexSpec {
43
- /**
44
- * The child model name (where the FK field lives) — this is the type
45
- * that will be passed to `pool.registerForeignKey(modelName, fieldName)`
46
- * and later to `pool.getByForeignKey(modelName, fieldName, value)`.
47
- *
48
- * Use the wire `__typename` casing (e.g., `'SlideLayer'`, not
49
- * `'slideLayer'`) — that's the value `createFromData` stamps onto
50
- * models and the pool indexes by.
51
- */
52
- readonly modelName: string;
53
- /** The FK field name on the child model, e.g. `'slideId'`. */
54
- readonly fieldName: string;
55
- }
56
- /**
57
- * A declarative enrichment rule for the delta-apply path.
58
- *
59
- * When a delta for `modelName` arrives, after the model is constructed
60
- * the base store reads `data[foreignKey]` from the payload, looks up
61
- * the matching parent in the ObjectPool, and attaches it as
62
- * `data[relationKey]`. Best-effort: if the parent isn't yet in the
63
- * pool (e.g., arrived later in the same bootstrap batch), enrichment
64
- * silently no-ops.
65
- *
66
- * Replaces the previous pattern of overriding `enrichRelations` on a
67
- * subclass to hardcode per-model enrichment logic.
68
- */
69
- export interface EnrichmentPlanEntry {
70
- /** The child model whose incoming deltas should be enriched. */
71
- readonly modelName: string;
72
- /** The FK field on the child that points at the parent's id. */
73
- readonly foreignKey: string;
74
- /** The property name under which to attach the parent model. */
75
- readonly relationKey: string;
76
- }
45
+ export type { ForeignKeyIndexSpec, EnrichmentPlanEntry } from './sync/syncPlan.js';
77
46
  /** Configuration for SyncedStore behavior */
78
47
  export interface SyncedStoreConfig {
79
48
  enableOffline?: boolean;
80
49
  enableCache?: boolean;
81
50
  enableTelemetry?: boolean;
82
- /**
83
- * Initial version vector keys, each seeded to 0. Merged with the
84
- * schema-derived set (if a schema is provided to the constructor) —
85
- * explicit keys here layer on top of derived ones. Replaces the
86
- * subclass pattern of hardcoding `this.versionVector = { tasks: 0, ... }`
87
- * in the constructor.
88
- */
89
- versionVectorKeys?: readonly string[];
90
51
  /**
91
52
  * Declarative enrichment plan consumed by `enrichRelations`. Replaces
92
53
  * the subclass override of `enrichRelations` for per-model parent
@@ -104,17 +65,7 @@ export interface SyncedStoreConfig {
104
65
  */
105
66
  foreignKeyIndexes?: readonly ForeignKeyIndexSpec[];
106
67
  }
107
- /** Sync status for UI binding */
108
- export interface SyncStatus {
109
- state: 'idle' | 'syncing' | 'error' | 'offline' | 'reconnecting';
110
- progress: number;
111
- error?: Error;
112
- /** When true, the error is a session/auth error requiring re-authentication. */
113
- isSessionError: boolean;
114
- lastSyncAt?: Date;
115
- pendingChanges: number;
116
- offlineSince?: Date;
117
- }
68
+ export type { SyncStatus } from './core/storeContract.js';
118
69
  /** User context for initialization */
119
70
  export interface UserContext {
120
71
  userId: string;
@@ -157,15 +108,7 @@ export interface SmartSyncOptions {
157
108
  batchingDelay?: number;
158
109
  maxBatchSize?: number;
159
110
  }
160
- /** Rehydration statistics from bootstrap */
161
- export interface RehydrationStats {
162
- added: number;
163
- updated: number;
164
- removed: number;
165
- skipped: number;
166
- healed: number;
167
- elapsedMs: number;
168
- }
111
+ export type { RehydrationStats } from './sync/bootstrapApply.js';
169
112
  /** Bootstrap timeout configuration */
170
113
  export declare const BOOTSTRAP_CONFIG: {
171
114
  readonly OVERALL_TIMEOUT_MS: 15000;
@@ -173,7 +116,8 @@ export declare const BOOTSTRAP_CONFIG: {
173
116
  readonly RETRY_DELAY_MS: 500;
174
117
  };
175
118
  export { ModelScope };
176
- export type { SyncDelta, SyncGroupChangePayload, GroupAddedPayload, GroupRemovedPayload, VersionVector, BootstrapHint, BootstrapDataEvent, PresenceUpdateEvent, };
119
+ export type { SyncDelta, SyncGroupChangePayload, GroupAddedPayload, GroupRemovedPayload, BootstrapHint, BootstrapDataEvent, PresenceUpdateEvent, };
120
+ export { deriveSyncPlanFromSchema } from './sync/syncPlan.js';
177
121
  /**
178
122
  * BaseSyncedStore — abstract base for app-specific sync stores.
179
123
  *
@@ -202,30 +146,6 @@ export type { SyncDelta, SyncGroupChangePayload, GroupAddedPayload, GroupRemoved
202
146
  * }
203
147
  * }
204
148
  */
205
- /**
206
- * Walk a schema and derive the three sync-plan arrays consumed by
207
- * `BaseSyncedStore`'s constructor: version-vector keys, FK indexes to
208
- * register on the pool, and the enrichment plan.
209
- *
210
- * Version vector keys are derived from each model's `typename` (lowercased
211
- * to match the server's event-type convention — `'Task'` → `'task'`,
212
- * `'SlideLayer'` → `'slidelayer'`). A fallback to the schema key applies
213
- * when `typename` is unset, though `defineSchema()` now always resolves
214
- * it during assembly so the fallback is defensive-only.
215
- *
216
- * FK indexes and enrichment entries are pulled from each `belongsTo`
217
- * relation where `options.index` / `options.enrich` is set. Relations
218
- * without those options are skipped — this is an opt-in mechanism so
219
- * adding a `belongsTo` never silently changes delta or lookup semantics.
220
- *
221
- * Pure function: takes a Schema, returns three arrays. No side effects,
222
- * no class state. Called once at construction time from `BaseSyncedStore`.
223
- */
224
- export declare function deriveSyncPlanFromSchema(schema: Schema): {
225
- versionVectorKeys: string[];
226
- enrichmentPlan: EnrichmentPlanEntry[];
227
- foreignKeyIndexes: ForeignKeyIndexSpec[];
228
- };
229
149
  export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaboration> = DefaultCollaborationEvents, TSchema extends Schema = Schema> {
230
150
  syncStatus: SyncStatus;
231
151
  protected readonly syncClient: SyncClient;
@@ -291,18 +211,16 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
291
211
  unpinScope(scope: ParticipantScope): Promise<void>;
292
212
  protected readonly queryProcessor: QueryProcessor;
293
213
  /**
294
- * Runtime behavior flags only — the three schema/config arrays
295
- * (`versionVectorKeys`, `enrichmentPlan`, `foreignKeyIndexes`) are
296
- * consumed at construction time and stored on the instance as
297
- * `versionVector`, `enrichmentPlan`, and pool-registered indexes.
298
- * They don't need to persist on `this.config`.
214
+ * Runtime behavior flags only — the schema/config arrays
215
+ * (`enrichmentPlan`, `foreignKeyIndexes`) are consumed at construction
216
+ * time and stored on the instance as `enrichmentPlan` and
217
+ * pool-registered indexes. They don't need to persist on `this.config`.
299
218
  */
300
219
  protected readonly config: Required<Pick<SyncedStoreConfig, 'enableOffline' | 'enableCache' | 'enableTelemetry'>>;
301
- protected disposers: Array<() => void>;
220
+ protected disposers: (() => void)[];
302
221
  protected initialized: boolean;
303
222
  protected dataReady: boolean;
304
223
  protected userContext: UserContext | null;
305
- protected versionVector: VersionVector;
306
224
  /**
307
225
  * Declarative enrichment plan: "for model X, when a delta arrives,
308
226
  * read data[foreignKey] and attach the matching parent from the pool
@@ -332,11 +250,11 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
332
250
  modelRegistry: ModelRegistry;
333
251
  /**
334
252
  * Optional schema. When provided, `deriveSyncPlanFromSchema` walks
335
- * the schema's models + relations to auto-populate version vector
336
- * keys, FK indexes, and the enrichment plan from declarative
337
- * annotations. Class-based subclass users (like Ablo's legacy
338
- * SyncedStore) typically pass explicit `config.versionVectorKeys`
339
- * / `config.foreignKeyIndexes` / `config.enrichmentPlan` instead.
253
+ * the schema's models + relations to auto-populate FK indexes and
254
+ * the enrichment plan from declarative annotations. Class-based
255
+ * subclass users (like Ablo's legacy SyncedStore) typically pass
256
+ * explicit `config.foreignKeyIndexes` / `config.enrichmentPlan`
257
+ * instead.
340
258
  */
341
259
  schema?: TSchema;
342
260
  /** Sync server URL for WebSocket connection. Converted to wss:// automatically. */
@@ -391,23 +309,15 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
391
309
  */
392
310
  protected connectionManager: import('./sync/ConnectionManager.js').ConnectionManager | null;
393
311
  /**
394
- * Re-mint hook for the short-lived access credential (the Stripe-style
395
- * `ek_`/`rk_`). Wired by the React provider from its `getToken`/`authEndpoint`
396
- * the engine owns WHEN to refresh (a stale-credential probe / an external
397
- * nudge), the integrator owns HOW to mint. Mirrors the `getToken` contract:
398
- * resolves a token string on success, `null` when the long-lived login is
399
- * gone (terminal), and THROWS on a transient/offline failure. Used by
400
- * {@link performCredentialRefresh}. Absent no silent re-mint (e.g. a static
401
- * `apiKey` deployment whose credential source refreshes out-of-band).
402
- */
403
- private credentialRefresher;
404
- /** Single-flight guard so a wake nudge + an in-flight request + a probe don't
405
- * all mint at once (the classic "token thrash → random logout" bug). */
406
- private inFlightCredentialRefresh;
407
- /** Teardown for the proactive credential lifecycle (refresh timer + wake/
408
- * online/focus listeners) installed by {@link startCredentialLifecycle};
409
- * cleared on {@link disconnect}. Null when no resolver is wired. */
410
- private credentialLifecycleTeardown;
312
+ * Access-credential re-mint + proactive pre-roll extracted to
313
+ * sync/credentialLifecycle.ts. Owns the refresher hook, the single-flight
314
+ * guard, and the browser-only refresh timer / wake listener; talks back
315
+ * through three lazily-resolved callbacks (the ConnectionManager doesn't
316
+ * exist until `setupWebSocketSync`). The `setCredentialRefresher` /
317
+ * `performCredentialRefresh` / `startCredentialLifecycle` methods below
318
+ * are thin delegates so the store's public surface is unchanged.
319
+ */
320
+ private readonly credentialLifecycle;
411
321
  /**
412
322
  * Listeners registered via `subscribeSessionError()`. Fired when the
413
323
  * WebSocket closes with a session-invalid code (1008/4001/4003) or a
@@ -448,7 +358,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
448
358
  waitForConfirmation(modelName: string, modelId: string): Promise<void>;
449
359
  /**
450
360
  * Observe the LOCAL mutation stream for undo recording (see
451
- * {@link import('./react/context.js').LocalMutation}). Taps the
361
+ * {@link import('./core/storeContract.js').LocalMutation}). Taps the
452
362
  * TransactionQueue's `transaction:created` event — fired once per local
453
363
  * create/update/delete/archive with `previousData` already captured.
454
364
  * Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
@@ -469,28 +379,24 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
469
379
  /**
470
380
  * Register the access-credential re-mint hook. Called by the React provider
471
381
  * with a thunk that mints a fresh `ek_`/`rk_` (typically its `getToken`).
472
- * See {@link credentialRefresher}.
382
+ * See {@link CredentialLifecycle.setRefresher}.
473
383
  */
474
- setCredentialRefresher(refresher: (() => Promise<string | null>) | null): void;
384
+ setCredentialRefresher(refresher: CredentialRefresher | null): void;
475
385
  /**
476
386
  * Re-mint the short-lived access credential and push it into the credential
477
387
  * source, reporting a tri-state outcome the {@link ConnectionManager} maps to
478
- * its FSM. The contract mirrors `getToken` (and PowerSync's `fetchCredentials`
479
- * / Liveblocks' `authEndpoint`, but made explicit instead of overloading
480
- * return/throw):
481
- * - token string → `'refreshed'` (fresh key in place; re-probe & reconnect)
482
- * - `null` → `'session_error'` (login itself is gone → terminal, sign out)
483
- * - throw → `'network_error'` (couldn't reach the mint endpoint → transient)
484
- *
485
- * SINGLE-FLIGHT: concurrent callers (a wake nudge, an in-flight request, the
486
- * probe) share one in-flight promise so we never double-mint — the canonical
487
- * fix for the "every 401 mints a token → thrash → spurious logout" anti-pattern.
488
- *
489
- * No refresher wired ⇒ `'refreshed'` (a no-op re-probe): a static-`apiKey`
490
- * deployment has no session to re-mint from; its credential source refreshes
491
- * out-of-band, so we just re-probe with whatever it currently holds.
388
+ * its FSM. Single-flight; no refresher wired ⇒ `'refreshed'` (a no-op
389
+ * re-probe). Full contract on {@link CredentialLifecycle.refresh}.
492
390
  */
493
391
  performCredentialRefresh(): Promise<'refreshed' | 'session_error' | 'network_error'>;
392
+ /**
393
+ * THE auth-recovery backbone for HTTP transports (lazy query lane etc.):
394
+ * classify-driven single-flight re-mint with the same FSM outcome routing
395
+ * the WS probe and proactive pre-roll use. `'retry'` ⇒ a fresh credential
396
+ * is in the credential source, replay the request ONCE. Full contract on
397
+ * {@link CredentialLifecycle.recoverFromAuthRejection}.
398
+ */
399
+ recoverFromAuthRejection(recovery: RecoveryClass): Promise<'retry' | 'stop'>;
494
400
  /**
495
401
  * Nudge the connection FSM to re-probe with the current credential. Idempotent
496
402
  * and safe in any state (ignored while `connected`). Call after pushing a
@@ -500,72 +406,39 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
500
406
  */
501
407
  nudgeReconnect(): void;
502
408
  /**
503
- * Install the access-credential lifecycle the CLIENT owns (this used to live
504
- * in the React provider wrong layer). Two parts:
505
- * 1. REACTIVE register `getToken` as the re-mint hook the FSM calls when a
506
- * probe finds the key stale (`credential_stale`) or on a nudge.
507
- * 2. PROACTIVE keep the short-lived key fresh ahead of trouble: a refresh
508
- * timer inside the TTL, plus re-mint on OS wake. The ENTIRE proactive
509
- * block is browser-gated (`typeof window`): server/SSR has no socket to
510
- * keep warm and the resolver is browser-oriented, so arming it in Node
511
- * would fire a relative-URL fetch and throw. (Agents pass a static
512
- * `apiKey` with no resolver, so this method is never called for them.)
513
- *
514
- * Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
515
- * never call a refresh method. Idempotent (a second call replaces the first);
516
- * torn down on {@link disconnect}.
409
+ * Install the access-credential lifecycle the CLIENT owns: register
410
+ * `getToken` as the reactive re-mint hook AND arm the browser-only
411
+ * proactive pre-roll (refresh timer + OS-wake re-mint). Idempotent
412
+ * (a second call replaces the first); torn down on {@link disconnect}.
413
+ * Full rationale on {@link CredentialLifecycle.start}.
517
414
  */
518
- startCredentialLifecycle(getToken: () => Promise<string | null>): void;
415
+ startCredentialLifecycle(getToken: CredentialRefresher, opts?: {
416
+ proactiveInNode?: boolean;
417
+ }): void;
519
418
  /** Tear down the proactive credential lifecycle (idempotent). */
520
419
  private stopCredentialLifecycle;
420
+ /** Narrow context the group-change leaf talks back through. */
421
+ private groupChangeContext;
521
422
  /**
522
- * Handle an actionType 'G' delta.
523
- *
524
- * The server emits 'G' via two distinct pathways, distinguished by payload
525
- * shape:
526
- *
527
- * Incremental (EmitGroupAdded): { group, userId }
528
- * - The recipient was added to a single sync group.
529
- * - Subsequent 'C' (Covering) deltas deliver each newly-visible entity.
530
- * - No re-bootstrap — entities arrive via the normal insert path.
531
- *
532
- * Legacy (EmitGroupChange): { addedGroups, removedGroups }
533
- * - Single delta carrying the full group membership diff.
534
- * - Forces a full re-bootstrap (disconnect + reconnect + fetch all).
535
- * - Deprecated on the server; kept here for wire-level backward compat.
423
+ * Handle an actionType 'G' delta — incremental `{ group, userId }` or
424
+ * legacy `{ addedGroups, removedGroups }` payloads. Full pathway doc on
425
+ * {@link groupChange.handleSyncGroupChange}.
536
426
  */
537
427
  protected handleSyncGroupChange(delta: SyncDelta): Promise<void>;
538
428
  /**
539
- * Handle an incremental GroupAdded delta.
540
- *
541
- * Adds the new group to the subscription metadata without triggering a
542
- * re-bootstrap. The server will follow up with 'C' (Covering) deltas for
543
- * each newly-visible entity, which flow through the normal insert path.
429
+ * Handle an incremental GroupAdded delta — metadata only, no re-bootstrap
430
+ * (covering deltas bring the entities). See {@link groupChange.handleGroupAdded}.
544
431
  */
545
432
  protected handleGroupAdded(payload: GroupAddedPayload, syncId: number): Promise<void>;
546
433
  /**
547
- * Handle an actionType 'S' (GroupRemoved) delta.
548
- *
549
- * Signals that the recipient has lost access to a sync group. Because
550
- * the client does not track per-entity group membership, we can't
551
- * selectively purge entities belonging to that group. The safe fallback
552
- * is the legacy behavior: clear local state and force a re-bootstrap
553
- * with the updated group list.
554
- *
555
- * Future optimization: track group membership in the ObjectPool so 'S'
556
- * can do a targeted purge instead of a full re-bootstrap.
434
+ * Handle an actionType 'S' (GroupRemoved) delta — SECURITY clear of local
435
+ * state + full re-bootstrap. See {@link groupChange.handleGroupRemoved}.
557
436
  */
558
437
  protected handleGroupRemoved(delta: SyncDelta): Promise<void>;
559
438
  /** Compute new sync groups after applying additions and removals */
560
439
  protected computeUpdatedSyncGroups(payload: SyncGroupChangePayload): string[];
561
- /** Force a full re-bootstrap via connection lifecycle event.
562
- *
563
- * No-op for `bootstrapMode: 'none'` participants — they never pull
564
- * baseline state, so a "force re-bootstrap" trigger (sync-group
565
- * shrink, scope revocation) instead just flushes the local pool and
566
- * relies on covering deltas to repopulate the data they actually
567
- * subscribe to.
568
- */
440
+ /** Force a full re-bootstrap via connection lifecycle event (no-op for
441
+ * `bootstrapMode: 'none'` participants — see {@link groupChange.forceFullRebootstrap}). */
569
442
  protected forceFullRebootstrap(): void;
570
443
  /**
571
444
  * Single source of truth for the sync-group list this session is
@@ -578,6 +451,8 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
578
451
  protected resolveSyncGroups(context: UserContext): readonly string[];
579
452
  /** Check if sync groups shrank since last session — force full bootstrap if so */
580
453
  protected checkSyncGroupShrinkage(): Promise<void>;
454
+ /** Narrow context the bootstrap-apply leaf talks back through. */
455
+ private poolContext;
581
456
  /** Apply bootstrap data to the ObjectPool with ghost removal */
582
457
  /** Apply bootstrap data to the ObjectPool. Delegates pool writes to SyncClient. */
583
458
  protected applyBootstrapToPool(bootstrapResult: BootstrapResult, protectedIds?: ReadonlySet<string>): RehydrationStats;
@@ -595,7 +470,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
595
470
  protected withDeltaQueuing<T>(fn: () => Promise<T>): Promise<T>;
596
471
  /** Collect IDs that must survive ghost removal (added by deltas during bootstrap) */
597
472
  protected collectDeltaProtectedIds(preBootstrapIds: ReadonlySet<string>): Set<string>;
598
- /** Replay deltas queued during bootstrap */
473
+ /** Replay deltas queued during bootstrap (atomically, via `applyDeltaFrame`). */
599
474
  protected replayQueuedDeltas(): void;
600
475
  /**
601
476
  * Factory for the internal `ConnectionManager`. Override to return
@@ -664,11 +539,13 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
664
539
  */
665
540
  protected waitForWebSocketConnected(timeoutMs: number): Promise<boolean>;
666
541
  protected setupWebSocketSync(context: UserContext, lastSyncId: number): void;
667
- /** State signature for delta deduplication */
668
- private extractStateSignature;
542
+ /** Memoized pipeline context — `enqueueDelta` runs once per delta, so the
543
+ * accessor object is built once and reused (the get/set accessors always
544
+ * read the live host fields). */
545
+ private _deltaPipelineContext;
546
+ private get deltaPipelineContext();
669
547
  /** Get fields that represent meaningful state for deduplication. Override for model-specific fields. */
670
548
  protected getStateFields(_modelName: string): string[];
671
- private isSameState;
672
549
  /** Deduplicate deltas to the same entity — keep meaningful state transitions only */
673
550
  protected deduplicateDeltas(deltas: SyncDelta[]): SyncDelta[];
674
551
  /** Process incoming delta with smart batching */