@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
@@ -0,0 +1,329 @@
1
+ /**
2
+ * credentialLifecycle — reconnect-credential re-mint + proactive pre-roll.
3
+ *
4
+ * Extracted from BaseSyncedStore.ts as a cohesive leaf: owns the re-mint
5
+ * hook, the single-flight guard, and the browser-only proactive refresh
6
+ * (timer + OS-wake listener). The store keeps thin public delegates
7
+ * (`setCredentialRefresher` / `performCredentialRefresh` /
8
+ * `startCredentialLifecycle`) so its published surface is unchanged, and
9
+ * talks back through the minimal {@link CredentialLifecycleContext} —
10
+ * never the store's class type — so no module cycle forms.
11
+ */
12
+ import { getContext } from '../context.js';
13
+ /** Fallback pre-roll cadence AND ceiling — the historical 10-minute value,
14
+ * comfortably inside the server's 15m default `ek_` TTL. Used verbatim when
15
+ * the refresher reports no expiry. */
16
+ export const DEFAULT_PREROLL_INTERVAL_MS = 10 * 60 * 1000;
17
+ /** Floor so a very short (or already-elapsed) TTL can't hot-loop the mint. */
18
+ export const MIN_PREROLL_DELAY_MS = 30 * 1000;
19
+ /**
20
+ * When to pre-roll a credential that expires at `expiresAtMs`: after ~2/3 of
21
+ * the REMAINING lifetime (15m TTL → 10m, exactly the historical cadence),
22
+ * clamped to [{@link MIN_PREROLL_DELAY_MS}, {@link DEFAULT_PREROLL_INTERVAL_MS}].
23
+ * Unknown expiry (`null`) → the 10-minute fallback. Exported for unit tests.
24
+ */
25
+ export function computePrerollDelayMs(expiresAtMs, nowMs) {
26
+ if (expiresAtMs === null || !Number.isFinite(expiresAtMs)) {
27
+ return DEFAULT_PREROLL_INTERVAL_MS;
28
+ }
29
+ const remaining = expiresAtMs - nowMs;
30
+ if (remaining <= 0)
31
+ return MIN_PREROLL_DELAY_MS;
32
+ const twoThirds = Math.floor((remaining * 2) / 3);
33
+ return Math.min(Math.max(twoThirds, MIN_PREROLL_DELAY_MS), DEFAULT_PREROLL_INTERVAL_MS);
34
+ }
35
+ /** Narrow a refresher-supplied `expiresAt` to epoch ms, or `null` when
36
+ * absent/unparseable (→ fallback cadence, never a crash). */
37
+ function normalizeExpiresAtMs(expiresAt) {
38
+ if (expiresAt === undefined)
39
+ return null;
40
+ const ms = expiresAt instanceof Date
41
+ ? expiresAt.getTime()
42
+ : typeof expiresAt === 'number'
43
+ ? expiresAt
44
+ : Date.parse(expiresAt);
45
+ return Number.isFinite(ms) ? ms : null;
46
+ }
47
+ export class CredentialLifecycle {
48
+ ctx;
49
+ /**
50
+ * Re-mint hook for the short-lived access credential (the Stripe-style
51
+ * `ek_`/`rk_`). Wired by the React provider from its `getToken`/`authEndpoint`
52
+ * — the engine owns WHEN to refresh (a stale-credential probe / an external
53
+ * nudge), the integrator owns HOW to mint. Mirrors the `getToken` contract:
54
+ * resolves a token string on success, `null` when the long-lived login is
55
+ * gone (terminal), and THROWS on a transient/offline failure. Used by
56
+ * {@link refresh}. Absent ⇒ no silent re-mint (e.g. a static
57
+ * `apiKey` deployment whose credential source refreshes out-of-band).
58
+ */
59
+ credentialRefresher = null;
60
+ /** Single-flight guard so a wake nudge + an in-flight request + a probe don't
61
+ * all mint at once (the classic "token thrash → random logout" bug). */
62
+ inFlightCredentialRefresh = null;
63
+ /** Teardown for the proactive credential lifecycle (refresh timer + wake/
64
+ * online/focus listeners) installed by {@link start};
65
+ * cleared on `BaseSyncedStore.disconnect`. Null when no resolver is wired. */
66
+ credentialLifecycleTeardown = null;
67
+ /** Epoch ms the CURRENT credential expires, when the refresher reports it
68
+ * (object-form {@link CredentialRefreshResult}); `null` for the classic
69
+ * string-form resolver → the pre-roll falls back to its fixed cadence. */
70
+ credentialExpiresAtMs = null;
71
+ /** Re-arm hook for the proactive pre-roll timer (set by {@link start}).
72
+ * Invoked after every successful mint so a REACTIVE refresh (probe, wake,
73
+ * the initial `ready()` mint) immediately re-anchors the schedule to the
74
+ * fresh credential's real expiry instead of a stale fixed delay. */
75
+ prerollReschedule = null;
76
+ constructor(ctx) {
77
+ this.ctx = ctx;
78
+ }
79
+ /**
80
+ * Register the access-credential re-mint hook. Called by the React provider
81
+ * with a thunk that mints a fresh `ek_`/`rk_` (typically its `getToken`).
82
+ * See {@link credentialRefresher}.
83
+ */
84
+ setRefresher(refresher) {
85
+ this.credentialRefresher = refresher;
86
+ }
87
+ /**
88
+ * Re-mint the short-lived access credential and push it into the credential
89
+ * source, reporting a tri-state outcome the `ConnectionManager` maps to
90
+ * its FSM. The contract mirrors `getToken` (and PowerSync's `fetchCredentials`
91
+ * / Liveblocks' `authEndpoint`, but made explicit instead of overloading
92
+ * return/throw):
93
+ * - token string → `'refreshed'` (fresh key in place; re-probe & reconnect)
94
+ * - `null` → `'session_error'` (login itself is gone → terminal, sign out)
95
+ * - throw → `'network_error'` (couldn't reach the mint endpoint → transient)
96
+ *
97
+ * SINGLE-FLIGHT: concurrent callers (a wake nudge, an in-flight request, the
98
+ * probe) share one in-flight promise so we never double-mint — the canonical
99
+ * fix for the "every 401 mints a token → thrash → spurious logout" anti-pattern.
100
+ *
101
+ * No refresher wired ⇒ `'refreshed'` (a no-op re-probe): a static-`apiKey`
102
+ * deployment has no session to re-mint from; its credential source refreshes
103
+ * out-of-band, so we just re-probe with whatever it currently holds.
104
+ */
105
+ async refresh() {
106
+ const refresher = this.credentialRefresher;
107
+ if (!refresher)
108
+ return 'refreshed';
109
+ if (this.inFlightCredentialRefresh)
110
+ return this.inFlightCredentialRefresh;
111
+ const run = (async () => {
112
+ try {
113
+ const result = await refresher();
114
+ const token = typeof result === 'string' ? result : result?.token;
115
+ if (!token) {
116
+ // null = the long-lived login is gone (mint endpoint answered 401/403).
117
+ // Terminal — the FSM routes this to sign-out.
118
+ return 'session_error';
119
+ }
120
+ // Object-form resolvers carry the mint's actual expiry; remember it so
121
+ // the proactive pre-roll schedules off the real TTL (string-form
122
+ // resolvers leave it null → fixed fallback cadence).
123
+ this.credentialExpiresAtMs =
124
+ typeof result === 'object' && result !== null
125
+ ? normalizeExpiresAtMs(result.expiresAt)
126
+ : null;
127
+ this.ctx.setAuthToken(token);
128
+ // Re-anchor the proactive pre-roll to the fresh credential's expiry.
129
+ this.prerollReschedule?.();
130
+ return 'refreshed';
131
+ }
132
+ catch (error) {
133
+ // A throw = transient (offline / mint endpoint unreachable / 5xx). The
134
+ // login may be perfectly valid; never sign out for this — back off and
135
+ // retry. Mirrors the `getToken` throw-vs-null contract end-to-end.
136
+ const message = error?.message ?? String(error);
137
+ // A relative-URL resolver invoked server-side (Node fetch has no origin
138
+ // to resolve against) emits the opaque "Failed to parse URL" / "Only
139
+ // absolute URLs are supported". Translate it into something actionable
140
+ // instead of a mystery transient blip — the proactive refresh is now
141
+ // browser-only, so hitting this means the resolver fired from SSR/RSC or
142
+ // a server route.
143
+ if (typeof window === 'undefined' && /parse URL|absolute URLs?/i.test(message)) {
144
+ getContext().logger.warn('credential resolver ran on the server with a relative URL — Node fetch needs an absolute URL. ' +
145
+ 'Refresh the Ablo client in the browser, or build an absolute URL server-side ' +
146
+ "(e.g. new URL('/api/ablo-session', process.env.NEXT_PUBLIC_APP_URL)).", { error: message });
147
+ }
148
+ else {
149
+ getContext().logger.debug('access-credential re-mint failed (transient)', { error: message });
150
+ }
151
+ return 'network_error';
152
+ }
153
+ })();
154
+ this.inFlightCredentialRefresh = run;
155
+ try {
156
+ return await run;
157
+ }
158
+ finally {
159
+ this.inFlightCredentialRefresh = null;
160
+ }
161
+ }
162
+ /**
163
+ * Route a refresh outcome into the FSM — the ONE place the tri-state is
164
+ * interpreted, shared by the proactive pre-roll, the wake nudge, and the
165
+ * HTTP auth-recovery path so every trigger converges on identical behavior.
166
+ */
167
+ routeRefreshOutcome(outcome) {
168
+ if (outcome === 'refreshed') {
169
+ // Fresh key already pushed into the credential source by `refresh`;
170
+ // nudge a parked connection to re-probe with it.
171
+ this.ctx.nudgeReconnect();
172
+ return 'retry';
173
+ }
174
+ if (outcome === 'session_error') {
175
+ // The long-lived login is gone (mint answered 401/403). Surface it —
176
+ // a no-op in FSM states that don't accept the event (the probe
177
+ // converges on sign-out there anyway); `session_expired`'s onEnter
178
+ // owns the authoritative log.
179
+ this.ctx.reportSessionExpired();
180
+ return 'stop';
181
+ }
182
+ // 'network_error' → transient (offline / mint hiccup); the next proactive
183
+ // tick or the FSM's own probe retries. Never sign out, never replay now.
184
+ return 'stop';
185
+ }
186
+ /**
187
+ * THE recovery backbone for auth-rejected requests, whatever the transport.
188
+ * The WS probe already routes 401s through the FSM; HTTP consumers (the
189
+ * lazy-query lane, bootstrap helpers) call THIS instead of inventing their
190
+ * own handling — one single-flight mint, one outcome routing, one taxonomy.
191
+ *
192
+ * Mirrors `NetworkProbe`'s classification of the same codes:
193
+ * - `access_credential_expiry` — the routine case (an expired `ek_`/`rk_`):
194
+ * silently re-mint via the single-flight {@link refresh} and tell the
195
+ * caller to replay ONCE on success. Never a sign-out by itself — the
196
+ * only terminal path is the mint itself resolving `null`.
197
+ * - `session_expiry` — the login is gone. Report to the FSM (sign-out
198
+ * path) and stop; replaying is pointless.
199
+ * - `auth_blocked` / `permission` / everything else — re-minting produces
200
+ * the same rejected credential; stop without touching the FSM.
201
+ */
202
+ async recoverFromAuthRejection(recovery) {
203
+ switch (recovery) {
204
+ case 'access_credential_expiry':
205
+ return this.routeRefreshOutcome(await this.refresh());
206
+ case 'session_expiry':
207
+ this.ctx.reportSessionExpired();
208
+ return 'stop';
209
+ default:
210
+ return 'stop';
211
+ }
212
+ }
213
+ /**
214
+ * Install the access-credential lifecycle the CLIENT owns (this used to live
215
+ * in the React provider — wrong layer). Two parts:
216
+ * 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
217
+ * probe finds the key stale (`credential_stale`) or on a nudge.
218
+ * 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
219
+ * timer inside the TTL, plus re-mint on OS wake. The ENTIRE proactive
220
+ * block is browser-gated (`typeof window`): server/SSR has no socket to
221
+ * keep warm and the resolver is browser-oriented, so arming it in Node
222
+ * would fire a relative-URL fetch and throw. (Agents pass a static
223
+ * `apiKey` with no resolver, so this method is never called for them.)
224
+ *
225
+ * Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
226
+ * never call a refresh method. Idempotent (a second call replaces the first);
227
+ * torn down on `BaseSyncedStore.disconnect`.
228
+ *
229
+ * `opts.proactiveInNode` arms the refresh TIMER on a windowless host too —
230
+ * set for agent/system participants (long-lived server sockets whose
231
+ * `rk_`/`ek_` must renew BEFORE the hub's keepalive reaper closes them; the
232
+ * PowerSync/Ably "SDK renews everywhere" model). Node timers are `unref`ed
233
+ * so a finishing script is never kept alive by the pre-roll. The OS-wake
234
+ * listener remains browser-only regardless (no `window` to listen on).
235
+ */
236
+ start(getToken, opts) {
237
+ this.stop();
238
+ this.setRefresher(getToken);
239
+ // Re-mint through the SAME single-flight path the FSM's reactive probe uses
240
+ // (`refresh`) rather than calling `getToken()` directly. Two
241
+ // wins over the old direct call:
242
+ // - SINGLE-FLIGHT: a wake nudge, an in-flight probe, and this proactive
243
+ // roll share one in-flight promise — no double-mint thrash.
244
+ // - The tri-state is HONOURED. The old code did `if (token) {…}` and
245
+ // dropped a `null` on the floor — a zombie session that re-minted on
246
+ // every tab focus and logged "signing out" forever without ever signing
247
+ // out. `session_error` now drives the FSM to actually expire.
248
+ const refresh = async () => {
249
+ // Same outcome routing as every other trigger (see routeRefreshOutcome):
250
+ // refreshed → nudge, session_error → report, network_error → wait for
251
+ // the next tick / the FSM's own probe. Never sign out for a transient.
252
+ this.routeRefreshOutcome(await this.refresh());
253
+ };
254
+ const teardowns = [];
255
+ // The proactive pre-roll arms in the browser, or in Node when the host
256
+ // OPTED IN (`proactiveInNode` — agent/system participants). The default
257
+ // Node posture stays reactive-only because a Next.js SSR/RSC eval of the
258
+ // `providers` module constructs user-kind clients whose scaffolded
259
+ // resolver is browser-oriented (a relative-URL `fetch('/api/ablo-session')`)
260
+ // — arming a timer there fires that resolver in Node, where fetch has no
261
+ // origin to resolve a relative URL against → "Failed to parse URL" on
262
+ // every tick. Long-lived server participants are the opposite case: their
263
+ // socket MUST outlive the credential TTL, so they get the timer (the
264
+ // PowerSync/Ably model — "the SDK invokes fetchCredentials when a token is
265
+ // nearing expiry", on every platform). The reactive re-mint hook
266
+ // (`setRefresher` above) stays UNCONDITIONAL: it only fires on a
267
+ // real connection probe, which can't happen during a bare SSR module eval.
268
+ if (typeof window !== 'undefined' || opts?.proactiveInNode === true) {
269
+ // A missed (background-throttled) tick is recovered by the next, or by
270
+ // the reactive probe. The timer is the sole proactive PRE-ROLL — it keeps
271
+ // the key warm ahead of expiry even while the socket sits healthy-
272
+ // `connected` (a state the FSM never probes). The delay is derived from
273
+ // the minted credential's ACTUAL `expiresAt` when the refresher reports
274
+ // one (~2/3 of remaining TTL), with the historical 10-minute value as
275
+ // ceiling AND fallback — a deployment minting shorter-TTL keys no longer
276
+ // outlives a fixed pre-roll and drops to reactive-only recovery.
277
+ let timer = null;
278
+ const scheduleNext = () => {
279
+ if (timer !== null)
280
+ clearTimeout(timer); // idempotent re-arm
281
+ const delay = computePrerollDelayMs(this.credentialExpiresAtMs, Date.now());
282
+ timer = setTimeout(() => {
283
+ // `.finally` keeps the chain alive on transient failures; a
284
+ // SUCCESSFUL mint already re-armed via `prerollReschedule` (no-op
285
+ // double-arm thanks to the clearTimeout above).
286
+ void refresh().finally(scheduleNext);
287
+ }, delay);
288
+ // Node: never keep a finishing process alive just for the pre-roll
289
+ // (browser timers have no unref — optional call is a no-op there).
290
+ timer.unref?.();
291
+ };
292
+ scheduleNext();
293
+ this.prerollReschedule = scheduleNext;
294
+ teardowns.push(() => {
295
+ if (timer !== null)
296
+ clearTimeout(timer);
297
+ timer = null;
298
+ this.prerollReschedule = null;
299
+ });
300
+ // OS-wake (desktop only): the Electron shell bridges `powerMonitor`
301
+ // 'resume' to this DOM event. This is the ONE event-trigger the lifecycle
302
+ // still owns, because `visibilitychange` does NOT fire on wake-from-sleep
303
+ // and — unlike `online`/`visibilitychange` — the ConnectionManager's own
304
+ // browser listeners (`setupBrowserListeners`) don't cover wake.
305
+ // Browser-gated separately from the timer: a `proactiveInNode` host has
306
+ // no `window` to listen on (and no OS sleep to wake from).
307
+ //
308
+ // The `online` and `visibilitychange` listeners that used to live here
309
+ // were REMOVED: the FSM already re-probes on NETWORK_ONLINE / TAB_VISIBLE
310
+ // through this exact credential path, so registering them here too only
311
+ // fired a second, null-swallowing mint per focus — the "session-key
312
+ // POSTed on every tab focus" spam in the console.
313
+ if (typeof window !== 'undefined') {
314
+ const onWake = () => void refresh();
315
+ window.addEventListener('ablo:wake', onWake);
316
+ teardowns.push(() => { window.removeEventListener('ablo:wake', onWake); });
317
+ }
318
+ }
319
+ this.credentialLifecycleTeardown = () => {
320
+ for (const t of teardowns)
321
+ t();
322
+ };
323
+ }
324
+ /** Tear down the proactive credential lifecycle (idempotent). */
325
+ stop() {
326
+ this.credentialLifecycleTeardown?.();
327
+ this.credentialLifecycleTeardown = null;
328
+ }
329
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * deltaPipeline — the incoming-delta dedup/batch/flush pipeline.
3
+ *
4
+ * Extracted from BaseSyncedStore.ts as a cohesive leaf: state-signature
5
+ * dedup, per-delta bookkeeping + enqueue, the live-traffic debounce, and
6
+ * the IDB→pool flush. The store keeps thin protected delegates with
7
+ * unchanged signatures, and the leaf routes every call to a protected
8
+ * override point (getStateFields, isCustomEntity, deduplicateDeltas,
9
+ * flushPendingDeltas, the G/S handlers, …) back through the minimal
10
+ * {@link DeltaPipelineContext} so subclass dynamic dispatch is preserved.
11
+ * `applyDeltaFrame` — the authoritative-apply correctness seam — stays in
12
+ * BaseSyncedStore and drives this pipeline via `enqueueDelta` +
13
+ * `flushPendingDeltas`.
14
+ */
15
+ import { ModelScope } from '../ObjectPool.js';
16
+ import type { Model } from '../Model.js';
17
+ import type { ModelData } from '../types/modelData.js';
18
+ import type { SyncDelta } from './SyncWebSocket.js';
19
+ /** One applied-delta result out of `Database.processDeltaBatch`, forwarded to the pool. */
20
+ interface DeltaDbResult {
21
+ action: 'add' | 'update' | 'remove' | 'archive' | 'verify';
22
+ modelName: string;
23
+ modelId: string;
24
+ data?: ModelData | null;
25
+ transactionId?: string;
26
+ }
27
+ /**
28
+ * What the pipeline needs back from its host store: the shared mutable
29
+ * pipeline state (backed by host fields via get/set accessors), narrow
30
+ * persistence/pool facades, and the host's own protected hooks so subclass
31
+ * overrides keep taking effect.
32
+ */
33
+ export interface DeltaPipelineContext {
34
+ pendingDeltas: SyncDelta[];
35
+ batchTimer: ReturnType<typeof setTimeout> | null;
36
+ /** Queue for deltas arriving during an active bootstrap; null when none. */
37
+ readonly bootstrapDeltaQueue: SyncDelta[] | null;
38
+ readonly smartSyncOptions: {
39
+ readonly batchingDelay: number;
40
+ readonly maxBatchSize: number;
41
+ };
42
+ /** Pool-applied cursor (`syncClient.position.applied`). */
43
+ readonly highestProcessedSyncId: number;
44
+ /** Resume/ack cursor (`syncClient.position.persisted`). */
45
+ readonly lastAckedId: number;
46
+ onDeltaReceived(syncId: number): void;
47
+ advanceApplied(syncId: number): void;
48
+ advancePersisted(syncId: number): void;
49
+ processDeltaBatch(deltas: {
50
+ syncId?: number;
51
+ actionType: SyncDelta['actionType'];
52
+ modelName: string;
53
+ modelId: string;
54
+ data: ModelData | null;
55
+ transactionId?: string;
56
+ }[]): Promise<{
57
+ results: DeltaDbResult[];
58
+ persistedSyncId: number;
59
+ }>;
60
+ /** `SyncClient.applyDeltaBatchToPool` with the host's `enrichRelations` bound. */
61
+ applyDeltaBatchToPool(results: DeltaDbResult[]): void;
62
+ /** `syncWebSocket?.acknowledge?.(syncId)` — no-op when the socket is down. */
63
+ acknowledge(syncId: number): void;
64
+ readonly objectPool: {
65
+ get(id: string): Model | undefined;
66
+ add(model: Model, scope: ModelScope): void;
67
+ remove(id: string): boolean;
68
+ /** Full in-memory clear — the revocation-failure fallback (see
69
+ * {@link handleGroupHandlerFailure}). */
70
+ clear(): void;
71
+ };
72
+ getStateFields(modelName: string): string[];
73
+ isCustomEntity(modelName: string): boolean;
74
+ createCustomEntity(modelName: string, modelId: string, data: Record<string, unknown>): Model | null;
75
+ deduplicateDeltas(deltas: SyncDelta[]): SyncDelta[];
76
+ flushPendingDeltas(): Promise<void>;
77
+ handleFlushError(error: unknown): void;
78
+ handleSyncGroupChange(delta: SyncDelta): Promise<void>;
79
+ handleGroupRemoved(delta: SyncDelta): Promise<void>;
80
+ /** Host's re-bootstrap trigger — the revocation-failure fallback. */
81
+ forceFullRebootstrap(): void;
82
+ cascadeCancelTransactionsForDeletedParent(parentModelName: string, parentId: string): void;
83
+ }
84
+ /**
85
+ * A 'G'/'S' handler rejected AFTER the applied watermark already advanced —
86
+ * the delta will never be re-delivered, so a failed SECURITY clear (revoked
87
+ * data must not persist) would otherwise be permanently silent. Fall back to
88
+ * the bluntest safe response: drop the whole in-memory pool and force a full
89
+ * re-bootstrap so local state is rebuilt from server truth. Never throws —
90
+ * this runs inside the delta pipeline's fire-and-forget seam.
91
+ */
92
+ export declare function handleGroupHandlerFailure(ctx: DeltaPipelineContext, delta: SyncDelta, error: unknown): void;
93
+ /** Deduplicate deltas to the same entity — keep meaningful state transitions only */
94
+ export declare function deduplicateDeltas(ctx: DeltaPipelineContext, deltas: SyncDelta[]): SyncDelta[];
95
+ /**
96
+ * Per-delta bookkeeping + enqueue. Returns `true` when the delta was
97
+ * pushed onto `pendingDeltas` (a regular batchable I/U/C/D delta that a
98
+ * subsequent flush must drain), `false` when it was skipped (dedup),
99
+ * deferred (bootstrap queue), or handled immediately out-of-band (G/S
100
+ * sync-group mutations). Does NOT schedule a flush — callers decide
101
+ * whether to debounce (live) or flush atomically (catch-up frame).
102
+ */
103
+ export declare function enqueueDelta(ctx: DeltaPipelineContext, delta: SyncDelta, options?: {
104
+ authoritative?: boolean;
105
+ }): boolean;
106
+ /** Debounce a flush for live single-delta traffic. */
107
+ export declare function scheduleDeltaFlush(ctx: DeltaPipelineContext): void;
108
+ /** Flush pending deltas with deduplication and batched ObjectPool mutations */
109
+ /** Flush pending deltas with deduplication. Delegates pool writes to SyncClient. */
110
+ export declare function flushPendingDeltas(ctx: DeltaPipelineContext): Promise<void>;
111
+ export {};