@absolutejs/sync 1.5.0 → 1.7.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.
@@ -33,6 +33,13 @@ export type PollingChangeSourceOptions = {
33
33
  onProcessed?: (uptoSeq: number) => void | Promise<void>;
34
34
  /** Called if a poll throws (the loop keeps running). Defaults to a warning. */
35
35
  onError?: (error: unknown) => void;
36
+ /**
37
+ * Called when an outbox row fails to parse (custom `parse` returned
38
+ * undefined, malformed JSON in `payload`, unknown `op`, etc). Default:
39
+ * silently dropped. Provide this to surface skipped rows so you notice
40
+ * malformed entries in the changelog table.
41
+ */
42
+ onSkip?: (row: OutboxRow, reason: 'parse-failed') => void;
36
43
  };
37
44
  /**
38
45
  * Create a polling {@link ChangeSource} over a changelog table. Connect it with
@@ -12,13 +12,27 @@
12
12
  * - First call per mutation pays a Worker spawn + compile (~30 ms). Every
13
13
  * subsequent call reuses the isolate and only spends ~0.5 ms creating a
14
14
  * fresh context.
15
- * - Timeout terminates the isolate (v1 of isolated-jsc trade-off the
16
- * sandbox runner detects this and lazily re-spawns on the next call).
15
+ * - Timeout terminates the isolate (the sandbox runner detects this and
16
+ * lazily re-spawns on the next call). On the FFI backend timeouts throw
17
+ * a TerminationException without killing the isolate; sync's runner
18
+ * treats both shapes the same.
17
19
  * - Each per-call context retains some JSC metadata until the isolate's
18
- * next GC sweep. Empirically ~2 MB residual per call. For long-lived
19
- * mutations choose `memoryLimit` ≥ 128 (the default 32 trips after a
20
- * few dozen calls without pressure for GC). v2 will let us trigger
21
- * sweep explicitly from the host; for now, size up.
20
+ * next GC sweep. Empirically ~2 MB residual per call (Worker backend).
21
+ * For long-lived mutations choose `memoryLimit` ≥ 128 (the default 32
22
+ * trips after a few dozen calls without pressure for GC).
23
+ *
24
+ * **Backend default: `'worker'`** — required so far because the `actions`
25
+ * machinery (insert/update/delete/change) crosses the host boundary as
26
+ * **async** References (they return Promises that go through the engine's
27
+ * writer + diff path). The isolated-jsc FFI backend only supports SYNC
28
+ * host fns today (per its 0.3 documented limit); calling
29
+ * `actions.insert(...)` from a sandboxed handler on FFI would surface as
30
+ * a Promise-cannot-unwrap error. Pin to Worker.
31
+ *
32
+ * Read-only sandboxed mutations that don't call `actions.*` (e.g. compute
33
+ * a derived value from `args` + `ctx` and `return` it) CAN opt into FFI
34
+ * via `sandbox: { backend: 'ffi' }` — they get the ~300 KB cold heap and
35
+ * interrupt-driven timeouts. Document this clearly when you do.
22
36
  *
23
37
  * The runner is built lazily per-mutation: nothing is spawned until the
24
38
  * mutation actually runs for the first time. No engine teardown hook is
@@ -31,6 +45,22 @@ export type SandboxConfig = {
31
45
  memoryLimit?: number;
32
46
  /** Wall-clock cap per call (ms). Default 5000. */
33
47
  timeout?: number;
48
+ /**
49
+ * isolated-jsc backend. Defaults to `'worker'` because the engine's
50
+ * `actions.insert/update/delete/change` cross the sandbox boundary as
51
+ * async References — and isolated-jsc's FFI backend doesn't pump
52
+ * async host fns (its 0.3 documented limit). The Worker backend
53
+ * supports both sync and async host fns.
54
+ *
55
+ * Opt into `'ffi'` only for **read-only** sandboxed handlers — ones
56
+ * that compute a derived value from `args` + `ctx` and return it
57
+ * without calling any `actions.*`. Those get the FFI cold-heap
58
+ * (~300 KB vs ~46 MB) + interrupt-driven timeout benefits.
59
+ *
60
+ * `'auto'` resolves to FFI when libJSC is reachable and Worker
61
+ * otherwise; same async-actions caveat applies on the FFI path.
62
+ */
63
+ backend?: 'auto' | 'ffi' | 'worker';
34
64
  };
35
65
  /**
36
66
  * Build a lazy runner for one mutation's sandboxed source. The first call
@@ -187,7 +187,81 @@ export type SyncEngine = {
187
187
  * subscribe/unsubscribe). Returns an unsubscribe. Powers the devtools feed.
188
188
  */
189
189
  onActivity: (listener: (event: EngineActivity) => void) => () => void;
190
+ /**
191
+ * Outbound CDC stream — yield every committed change as a {@link LoggedChange},
192
+ * historical first (entries with `version > since`) then continuously tailing
193
+ * live commits. Use it to feed downstream pipelines (Kafka, search indexers,
194
+ * audit logs, analytics warehouses).
195
+ *
196
+ * The iterator is notify-driven (no polling): it parks on a Promise that
197
+ * resolves the instant a new commit lands.
198
+ *
199
+ * If `since` falls before the oldest entry retained in the bounded change
200
+ * log, the iterator throws {@link MissedChangesError} so the consumer
201
+ * notices the gap instead of silently skipping commits. Resubscribe with
202
+ * `since = engine.inspect().recentChanges[0].version` after re-bootstrapping.
203
+ *
204
+ * If the consumer iterates slower than the engine commits and the in-flight
205
+ * buffer overflows (`maxBuffer`, default 10000), the iterator throws
206
+ * {@link CdcConsumerSlowError} for the same reason.
207
+ *
208
+ * @example
209
+ * for await (const entry of engine.streamChanges({ since: lastCursor })) {
210
+ * await kafka.send('sync.changes', JSON.stringify(entry));
211
+ * lastCursor = entry.version;
212
+ * }
213
+ */
214
+ streamChanges: (options?: StreamChangesOptions) => AsyncIterable<LoggedChange>;
215
+ };
216
+ /**
217
+ * A single committed change as it appears in the engine's change log and on
218
+ * the {@link SyncEngine.streamChanges} CDC stream. Versions are monotonic
219
+ * across the engine: a single mutation that writes N rows emits N entries
220
+ * all sharing the same `version`.
221
+ */
222
+ export type LoggedChange = {
223
+ version: number;
224
+ table: string;
225
+ change: RowChange<unknown>;
190
226
  };
227
+ /** Thrown by {@link SyncEngine.streamChanges} when `since` is older than the
228
+ * oldest entry retained in the bounded change log (i.e. the consumer was
229
+ * disconnected long enough that the engine has lost the diff). The consumer
230
+ * should re-bootstrap from a fresh hydrate and resume from `availableSince`. */
231
+ export declare class MissedChangesError extends Error {
232
+ readonly requestedSince: number;
233
+ readonly availableSince: number;
234
+ constructor(requestedSince: number, availableSince: number);
235
+ }
236
+ /** Options for {@link SyncEngine.streamChanges}. */
237
+ export type StreamChangesOptions = {
238
+ /**
239
+ * Last version the consumer has already processed. The stream yields
240
+ * entries with `version > since`. Defaults to `0` (replay everything in
241
+ * the log, then tail).
242
+ */
243
+ since?: number;
244
+ /**
245
+ * Cancel the stream cleanly. When the signal aborts, the iterator
246
+ * resolves to `done` on its next yield and unregisters its subscriber.
247
+ */
248
+ signal?: AbortSignal;
249
+ /**
250
+ * Hard cap on the in-flight buffer for this consumer. If the engine
251
+ * commits faster than the consumer iterates and the buffer overflows,
252
+ * the stream rejects so the consumer notices instead of silently
253
+ * skipping entries. Defaults to 10000.
254
+ */
255
+ maxBuffer?: number;
256
+ };
257
+ /** Thrown by {@link SyncEngine.streamChanges} when the consumer fell so far
258
+ * behind that the in-flight buffer overflowed. Resubscribe from the last
259
+ * cursor the consumer successfully processed. */
260
+ export declare class CdcConsumerSlowError extends Error {
261
+ readonly maxBuffer: number;
262
+ readonly lastDeliveredVersion: number;
263
+ constructor(maxBuffer: number, lastDeliveredVersion: number);
264
+ }
191
265
  export type SyncEngineOptions = {
192
266
  /**
193
267
  * How many recent changes to retain for resumable reconnects. A client that
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export { sync } from './plugin';
6
6
  export type { SyncPluginOptions, SyncRequestContext } from './plugin';
7
7
  export { syncSocket } from './engine/socket';
8
8
  export type { SyncSocketOptions } from './engine/socket';
9
+ export { syncCdc } from './engine/cdc';
10
+ export type { SyncCdcOptions } from './engine/cdc';
9
11
  export { syncDevtools } from './devtools';
10
12
  export type { SyncDevtoolsOptions } from './devtools';
11
13
  export { createPresenceHub } from './engine/presence';