@secondlayer/sdk 9.0.2 → 10.1.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.
@@ -96,6 +96,19 @@ function sinkConformanceProbes(harness) {
96
96
  expectCursor("inclusive >=", await harness.readCursor(), "10:2147483647");
97
97
  }
98
98
  },
99
+ {
100
+ name: "rollback to genesis (null rewind) deletes every row and clears the checkpoint",
101
+ async run() {
102
+ const sink = await start();
103
+ await commit(sink, "11:0", [
104
+ [10, "a"],
105
+ [11, "b"]
106
+ ]);
107
+ await sink.rollback(0, null);
108
+ expectRows("genesis rewind", await harness.readRows(), []);
109
+ expectCursor("genesis rewind", await harness.readCursor(), null);
110
+ }
111
+ },
99
112
  {
100
113
  name: "rollback then re-read: canonical rows land cleanly after the rewind",
101
114
  async run() {
@@ -190,5 +203,5 @@ export {
190
203
  attachSinkConformance
191
204
  };
192
205
 
193
- //# debugId=5D137D9410DE790C64756E2164756E21
206
+ //# debugId=2A425478905AA1C864756E2164756E21
194
207
  //# sourceMappingURL=testing.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["src/sinks/testing.ts"],
4
4
  "sourcesContent": [
5
- "import type { ConsumerSink } from \"./types.ts\";\n\n/**\n * The sink conformance kit: executable probes for the {@link ConsumerSink}\n * contract invariants — the ones that are SILENT when violated in\n * production (torn batches, silent gaps, under-deleted forks) and only\n * surface weeks later as missing or duplicated rows.\n *\n * Run every sink you author through it, in CI, against the real store:\n *\n * ```ts\n * import { test } from \"bun:test\";\n * import { attachSinkConformance } from \"@secondlayer/sdk/sinks/testing\";\n *\n * attachSinkConformance(test, {\n * makeSink: () => mySink(db, { id: \"conformance\", tables: [\"rows\"], height: \"height\" }),\n * reset: () => db.deleteFrom(...)...,\n * insertRow: (tx, height, key) => ..., // MUST be replay-safe (upsert on key)\n * readRows: () => ...,\n * readCursor: () => ...,\n * });\n * ```\n *\n * Framework-agnostic: probes throw plain `Error`s, so they run under\n * bun:test, vitest, jest, or a bare loop.\n */\nexport interface SinkConformanceHarness<Tx = unknown> {\n\t/** A fresh sink instance bound to the SAME store and the SAME checkpoint\n\t * id each call — \"new instance\" is how the kit simulates a restart. */\n\tmakeSink(): ConsumerSink<Tx> | Promise<ConsumerSink<Tx>>;\n\t/** Wipe the store back to empty: projection rows AND this id's\n\t * checkpoint. Called before every probe. */\n\treset(): Promise<void> | void;\n\t/** Insert one projection row through the lent transaction, exactly as a\n\t * real `onBatch` handler would. MUST be replay-safe (upsert / insert-\n\t * on-conflict-do-nothing keyed on `key`) — the same requirement the\n\t * contract's at-least-once delivery puts on real handlers. */\n\tinsertRow(tx: Tx, height: number, key: string): Promise<void> | void;\n\t/** Every committed projection row, read OUTSIDE any transaction. */\n\treadRows(): Promise<Array<{ height: number; key: string }>>;\n\t/** The committed checkpoint cursor as visible to a restarted process, or\n\t * `null` when none was ever committed. */\n\treadCursor(): Promise<string | null>;\n\t/** Optional (sinks with a writer lock): hold the lock for this sink's id\n\t * — as a second live consumer would — while `during` runs. Enables the\n\t * lock-contention probe (invariant #13: fail loudly, never interleave). */\n\twithLockHeld?(during: () => Promise<void>): Promise<void>;\n}\n\n/** One executable contract probe; `run` throws on violation. */\nexport interface ConformanceProbe {\n\tname: string;\n\trun(): Promise<void>;\n}\n\nfunction fail(probe: string, detail: string): never {\n\tthrow new Error(`sink conformance — ${probe}: ${detail}`);\n}\n\nfunction sortRows(rows: Array<{ height: number; key: string }>) {\n\treturn [...rows].sort(\n\t\t(a, b) => a.height - b.height || a.key.localeCompare(b.key),\n\t);\n}\n\nfunction expectRows(\n\tprobe: string,\n\tactual: Array<{ height: number; key: string }>,\n\texpected: Array<{ height: number; key: string }>,\n): void {\n\tconst got = JSON.stringify(sortRows(actual));\n\tconst want = JSON.stringify(sortRows(expected));\n\tif (got !== want) fail(probe, `rows ${got}, expected ${want}`);\n}\n\nfunction expectCursor(\n\tprobe: string,\n\tactual: string | null,\n\texpected: string | null,\n): void {\n\tif (actual !== expected) {\n\t\tfail(\n\t\t\tprobe,\n\t\t\t`cursor ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`,\n\t\t);\n\t}\n}\n\nasync function expectRejects(\n\tprobe: string,\n\tpromise: Promise<unknown>,\n\twhy: string,\n): Promise<void> {\n\tlet rejected = false;\n\ttry {\n\t\tawait promise;\n\t} catch {\n\t\trejected = true;\n\t}\n\tif (!rejected) fail(probe, why);\n}\n\n/** Build the probe list for a harness. Prefer {@link attachSinkConformance}\n * inside a test framework; use this directly for a bare runner. */\nexport function sinkConformanceProbes<Tx>(\n\tharness: SinkConformanceHarness<Tx>,\n): ConformanceProbe[] {\n\t// Every probe starts from a wiped store and an initialized fresh sink.\n\tasync function start(): Promise<ConsumerSink<Tx>> {\n\t\tawait harness.reset();\n\t\tconst sink = await harness.makeSink();\n\t\tawait sink.loadCursor();\n\t\treturn sink;\n\t}\n\tconst commit = (\n\t\tsink: ConsumerSink<Tx>,\n\t\tcursor: string,\n\t\trows: Array<[number, string]>,\n\t) =>\n\t\tsink.commitBatch(cursor, async (tx) => {\n\t\t\tfor (const [height, key] of rows)\n\t\t\t\tawait harness.insertRow(tx, height, key);\n\t\t});\n\n\tconst probes: ConformanceProbe[] = [\n\t\t{\n\t\t\tname: \"fresh store: loadCursor initializes and returns null\",\n\t\t\tasync run() {\n\t\t\t\tawait harness.reset();\n\t\t\t\tconst sink = await harness.makeSink();\n\t\t\t\tconst cursor = await sink.loadCursor();\n\t\t\t\texpectCursor(\"fresh loadCursor\", cursor, null);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"checkpoint persists: a restarted sink resumes from the committed cursor\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"10:0\", [[10, \"a\"]]);\n\t\t\t\tconst restarted = await harness.makeSink();\n\t\t\t\tconst resumed = await restarted.loadCursor();\n\t\t\t\texpectCursor(\"restart resume\", resumed, \"10:0\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"crash between rows and cursor: a throw aborts both (invariants #4/#5)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait expectRejects(\n\t\t\t\t\t\"torn batch\",\n\t\t\t\t\tsink.commitBatch(\"10:0\", async (tx) => {\n\t\t\t\t\t\tawait harness.insertRow(tx, 10, \"a\");\n\t\t\t\t\t\tthrow new Error(\"kill -9\");\n\t\t\t\t\t}),\n\t\t\t\t\t\"commitBatch swallowed the handler throw — it must propagate (invariant #12)\",\n\t\t\t\t);\n\t\t\t\texpectRows(\"torn batch\", await harness.readRows(), []);\n\t\t\t\texpectCursor(\"torn batch\", await harness.readCursor(), null);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replaying the same cursor is safe: cursor works as an idempotency key (#7)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"10:0\", [[10, \"a\"]]);\n\t\t\t\t// At-least-once redelivery: same cursor, same rows (deterministic\n\t\t\t\t// replay). Neither duplicate rows nor a corrupted checkpoint.\n\t\t\t\tawait commit(sink, \"10:0\", [[10, \"a\"]]);\n\t\t\t\texpectRows(\"replay\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\"replay\", await harness.readCursor(), \"10:0\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback deletes at-or-above the fork — inclusive >= (#8/#9)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"11:0\", [\n\t\t\t\t\t[10, \"a\"],\n\t\t\t\t\t[11, \"b\"],\n\t\t\t\t]);\n\t\t\t\tawait sink.rollback(11, \"10:2147483647\");\n\t\t\t\t// The fork block itself (height 11) is deleted: the new canonical\n\t\t\t\t// chain re-supplies it.\n\t\t\t\texpectRows(\"inclusive >=\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\n\t\t\t\t\t\"inclusive >=\",\n\t\t\t\t\tawait harness.readCursor(),\n\t\t\t\t\t\"10:2147483647\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback then re-read: canonical rows land cleanly after the rewind\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"12:0\", [\n\t\t\t\t\t[10, \"a\"],\n\t\t\t\t\t[11, \"b\"],\n\t\t\t\t\t[12, \"c\"],\n\t\t\t\t]);\n\t\t\t\tawait sink.rollback(11, \"10:2147483647\");\n\t\t\t\texpectRows(\"rollback\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\t// The re-read: the new canonical chain replaces the orphaned rows.\n\t\t\t\tawait commit(sink, \"12:0\", [\n\t\t\t\t\t[11, \"b2\"],\n\t\t\t\t\t[12, \"c2\"],\n\t\t\t\t]);\n\t\t\t\texpectRows(\"re-read\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t\t{ height: 11, key: \"b2\" },\n\t\t\t\t\t{ height: 12, key: \"c2\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\"re-read\", await harness.readCursor(), \"12:0\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback is idempotent: re-application after a crash is a no-op (#10)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"11:0\", [\n\t\t\t\t\t[10, \"a\"],\n\t\t\t\t\t[11, \"b\"],\n\t\t\t\t]);\n\t\t\t\tawait sink.rollback(11, \"10:2147483647\");\n\t\t\t\t// The loop dedups reorgs in memory only — a restart re-detects the\n\t\t\t\t// same fork and re-applies the same rollback.\n\t\t\t\tawait sink.rollback(11, \"10:2147483647\");\n\t\t\t\texpectRows(\"idempotent rollback\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\n\t\t\t\t\t\"idempotent rollback\",\n\t\t\t\t\tawait harness.readCursor(),\n\t\t\t\t\t\"10:2147483647\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback scopes deletion by forkPointHeight, never rewindCursor (#11)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"13:0\", [\n\t\t\t\t\t[9, \"a\"],\n\t\t\t\t\t[10, \"b\"],\n\t\t\t\t\t[11, \"c\"],\n\t\t\t\t\t[12, \"d\"],\n\t\t\t\t\t[13, \"e\"],\n\t\t\t\t]);\n\t\t\t\t// A multi-fork page: every call carries the SAME rewindCursor (the\n\t\t\t\t// lowest fork point) with its OWN forkPointHeight. This call's pair\n\t\t\t\t// is deliberately mismatched — fork at 12, rewind below 10. A sink\n\t\t\t\t// deriving the undo range from the cursor deletes 10-11 here.\n\t\t\t\tawait sink.rollback(12, \"9:2147483647\");\n\t\t\t\texpectRows(\"scope by height\", await harness.readRows(), [\n\t\t\t\t\t{ height: 9, key: \"a\" },\n\t\t\t\t\t{ height: 10, key: \"b\" },\n\t\t\t\t\t{ height: 11, key: \"c\" },\n\t\t\t\t]);\n\t\t\t\tawait sink.rollback(10, \"9:2147483647\");\n\t\t\t\texpectRows(\"scope by height (second fork)\", await harness.readRows(), [\n\t\t\t\t\t{ height: 9, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\n\t\t\t\t\t\"scope by height\",\n\t\t\t\t\tawait harness.readCursor(),\n\t\t\t\t\t\"9:2147483647\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t];\n\n\tif (harness.withLockHeld) {\n\t\tconst withLockHeld = harness.withLockHeld.bind(harness);\n\t\tprobes.push({\n\t\t\tname: \"lock contention: a second writer fails loudly, never interleaves (#13)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait withLockHeld(async () => {\n\t\t\t\t\tawait expectRejects(\n\t\t\t\t\t\t\"lock contention\",\n\t\t\t\t\t\tsink.commitBatch(\"10:0\", async (tx) => {\n\t\t\t\t\t\t\tawait harness.insertRow(tx, 10, \"a\");\n\t\t\t\t\t\t}),\n\t\t\t\t\t\t\"commitBatch succeeded while another writer held the lock — two replicas would interleave commits\",\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t\texpectRows(\"lock contention\", await harness.readRows(), []);\n\t\t\t\texpectCursor(\"lock contention\", await harness.readCursor(), null);\n\t\t\t},\n\t\t});\n\t}\n\n\treturn probes;\n}\n\n/**\n * Register every conformance probe with a test framework: pass your\n * framework's `test` function (bun:test, vitest, jest — anything with a\n * `(name, fn)` signature). One probe becomes one test case.\n */\nexport function attachSinkConformance<Tx>(\n\ttest: (name: string, fn: () => Promise<void>) => void,\n\tharness: SinkConformanceHarness<Tx>,\n): void {\n\tfor (const probe of sinkConformanceProbes(harness)) {\n\t\ttest(probe.name, () => probe.run());\n\t}\n}\n"
5
+ "import type { ConsumerSink } from \"./types.ts\";\n\n/**\n * The sink conformance kit: executable probes for the {@link ConsumerSink}\n * contract invariants — the ones that are SILENT when violated in\n * production (torn batches, silent gaps, under-deleted forks) and only\n * surface weeks later as missing or duplicated rows.\n *\n * Run every sink you author through it, in CI, against the real store:\n *\n * ```ts\n * import { test } from \"bun:test\";\n * import { attachSinkConformance } from \"@secondlayer/sdk/sinks/testing\";\n *\n * attachSinkConformance(test, {\n * makeSink: () => mySink(db, { id: \"conformance\", tables: [\"rows\"], height: \"height\" }),\n * reset: () => db.deleteFrom(...)...,\n * insertRow: (tx, height, key) => ..., // MUST be replay-safe (upsert on key)\n * readRows: () => ...,\n * readCursor: () => ...,\n * });\n * ```\n *\n * Framework-agnostic: probes throw plain `Error`s, so they run under\n * bun:test, vitest, jest, or a bare loop.\n */\nexport interface SinkConformanceHarness<Tx = unknown> {\n\t/** A fresh sink instance bound to the SAME store and the SAME checkpoint\n\t * id each call — \"new instance\" is how the kit simulates a restart. */\n\tmakeSink(): ConsumerSink<Tx> | Promise<ConsumerSink<Tx>>;\n\t/** Wipe the store back to empty: projection rows AND this id's\n\t * checkpoint. Called before every probe. */\n\treset(): Promise<void> | void;\n\t/** Insert one projection row through the lent transaction, exactly as a\n\t * real `onBatch` handler would. MUST be replay-safe (upsert / insert-\n\t * on-conflict-do-nothing keyed on `key`) — the same requirement the\n\t * contract's at-least-once delivery puts on real handlers. */\n\tinsertRow(tx: Tx, height: number, key: string): Promise<void> | void;\n\t/** Every committed projection row, read OUTSIDE any transaction. */\n\treadRows(): Promise<Array<{ height: number; key: string }>>;\n\t/** The committed checkpoint cursor as visible to a restarted process, or\n\t * `null` when none was ever committed. */\n\treadCursor(): Promise<string | null>;\n\t/** Optional (sinks with a writer lock): hold the lock for this sink's id\n\t * — as a second live consumer would — while `during` runs. Enables the\n\t * lock-contention probe (invariant #13: fail loudly, never interleave). */\n\twithLockHeld?(during: () => Promise<void>): Promise<void>;\n}\n\n/** One executable contract probe; `run` throws on violation. */\nexport interface ConformanceProbe {\n\tname: string;\n\trun(): Promise<void>;\n}\n\nfunction fail(probe: string, detail: string): never {\n\tthrow new Error(`sink conformance — ${probe}: ${detail}`);\n}\n\nfunction sortRows(rows: Array<{ height: number; key: string }>) {\n\treturn [...rows].sort(\n\t\t(a, b) => a.height - b.height || a.key.localeCompare(b.key),\n\t);\n}\n\nfunction expectRows(\n\tprobe: string,\n\tactual: Array<{ height: number; key: string }>,\n\texpected: Array<{ height: number; key: string }>,\n): void {\n\tconst got = JSON.stringify(sortRows(actual));\n\tconst want = JSON.stringify(sortRows(expected));\n\tif (got !== want) fail(probe, `rows ${got}, expected ${want}`);\n}\n\nfunction expectCursor(\n\tprobe: string,\n\tactual: string | null,\n\texpected: string | null,\n): void {\n\tif (actual !== expected) {\n\t\tfail(\n\t\t\tprobe,\n\t\t\t`cursor ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`,\n\t\t);\n\t}\n}\n\nasync function expectRejects(\n\tprobe: string,\n\tpromise: Promise<unknown>,\n\twhy: string,\n): Promise<void> {\n\tlet rejected = false;\n\ttry {\n\t\tawait promise;\n\t} catch {\n\t\trejected = true;\n\t}\n\tif (!rejected) fail(probe, why);\n}\n\n/** Build the probe list for a harness. Prefer {@link attachSinkConformance}\n * inside a test framework; use this directly for a bare runner. */\nexport function sinkConformanceProbes<Tx>(\n\tharness: SinkConformanceHarness<Tx>,\n): ConformanceProbe[] {\n\t// Every probe starts from a wiped store and an initialized fresh sink.\n\tasync function start(): Promise<ConsumerSink<Tx>> {\n\t\tawait harness.reset();\n\t\tconst sink = await harness.makeSink();\n\t\tawait sink.loadCursor();\n\t\treturn sink;\n\t}\n\tconst commit = (\n\t\tsink: ConsumerSink<Tx>,\n\t\tcursor: string,\n\t\trows: Array<[number, string]>,\n\t) =>\n\t\tsink.commitBatch(cursor, async (tx) => {\n\t\t\tfor (const [height, key] of rows)\n\t\t\t\tawait harness.insertRow(tx, height, key);\n\t\t});\n\n\tconst probes: ConformanceProbe[] = [\n\t\t{\n\t\t\tname: \"fresh store: loadCursor initializes and returns null\",\n\t\t\tasync run() {\n\t\t\t\tawait harness.reset();\n\t\t\t\tconst sink = await harness.makeSink();\n\t\t\t\tconst cursor = await sink.loadCursor();\n\t\t\t\texpectCursor(\"fresh loadCursor\", cursor, null);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"checkpoint persists: a restarted sink resumes from the committed cursor\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"10:0\", [[10, \"a\"]]);\n\t\t\t\tconst restarted = await harness.makeSink();\n\t\t\t\tconst resumed = await restarted.loadCursor();\n\t\t\t\texpectCursor(\"restart resume\", resumed, \"10:0\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"crash between rows and cursor: a throw aborts both (invariants #4/#5)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait expectRejects(\n\t\t\t\t\t\"torn batch\",\n\t\t\t\t\tsink.commitBatch(\"10:0\", async (tx) => {\n\t\t\t\t\t\tawait harness.insertRow(tx, 10, \"a\");\n\t\t\t\t\t\tthrow new Error(\"kill -9\");\n\t\t\t\t\t}),\n\t\t\t\t\t\"commitBatch swallowed the handler throw — it must propagate (invariant #12)\",\n\t\t\t\t);\n\t\t\t\texpectRows(\"torn batch\", await harness.readRows(), []);\n\t\t\t\texpectCursor(\"torn batch\", await harness.readCursor(), null);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replaying the same cursor is safe: cursor works as an idempotency key (#7)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"10:0\", [[10, \"a\"]]);\n\t\t\t\t// At-least-once redelivery: same cursor, same rows (deterministic\n\t\t\t\t// replay). Neither duplicate rows nor a corrupted checkpoint.\n\t\t\t\tawait commit(sink, \"10:0\", [[10, \"a\"]]);\n\t\t\t\texpectRows(\"replay\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\"replay\", await harness.readCursor(), \"10:0\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback deletes at-or-above the fork — inclusive >= (#8/#9)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"11:0\", [\n\t\t\t\t\t[10, \"a\"],\n\t\t\t\t\t[11, \"b\"],\n\t\t\t\t]);\n\t\t\t\tawait sink.rollback(11, \"10:2147483647\");\n\t\t\t\t// The fork block itself (height 11) is deleted: the new canonical\n\t\t\t\t// chain re-supplies it.\n\t\t\t\texpectRows(\"inclusive >=\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\n\t\t\t\t\t\"inclusive >=\",\n\t\t\t\t\tawait harness.readCursor(),\n\t\t\t\t\t\"10:2147483647\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback to genesis (null rewind) deletes every row and clears the checkpoint\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"11:0\", [\n\t\t\t\t\t[10, \"a\"],\n\t\t\t\t\t[11, \"b\"],\n\t\t\t\t]);\n\t\t\t\t// A fork at block 0 has no cursor to rewind to: the checkpoint row\n\t\t\t\t// goes away, so a restart resumes from the very beginning.\n\t\t\t\tawait sink.rollback(0, null);\n\t\t\t\texpectRows(\"genesis rewind\", await harness.readRows(), []);\n\t\t\t\texpectCursor(\"genesis rewind\", await harness.readCursor(), null);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback then re-read: canonical rows land cleanly after the rewind\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"12:0\", [\n\t\t\t\t\t[10, \"a\"],\n\t\t\t\t\t[11, \"b\"],\n\t\t\t\t\t[12, \"c\"],\n\t\t\t\t]);\n\t\t\t\tawait sink.rollback(11, \"10:2147483647\");\n\t\t\t\texpectRows(\"rollback\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\t// The re-read: the new canonical chain replaces the orphaned rows.\n\t\t\t\tawait commit(sink, \"12:0\", [\n\t\t\t\t\t[11, \"b2\"],\n\t\t\t\t\t[12, \"c2\"],\n\t\t\t\t]);\n\t\t\t\texpectRows(\"re-read\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t\t{ height: 11, key: \"b2\" },\n\t\t\t\t\t{ height: 12, key: \"c2\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\"re-read\", await harness.readCursor(), \"12:0\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback is idempotent: re-application after a crash is a no-op (#10)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"11:0\", [\n\t\t\t\t\t[10, \"a\"],\n\t\t\t\t\t[11, \"b\"],\n\t\t\t\t]);\n\t\t\t\tawait sink.rollback(11, \"10:2147483647\");\n\t\t\t\t// The loop dedups reorgs in memory only — a restart re-detects the\n\t\t\t\t// same fork and re-applies the same rollback.\n\t\t\t\tawait sink.rollback(11, \"10:2147483647\");\n\t\t\t\texpectRows(\"idempotent rollback\", await harness.readRows(), [\n\t\t\t\t\t{ height: 10, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\n\t\t\t\t\t\"idempotent rollback\",\n\t\t\t\t\tawait harness.readCursor(),\n\t\t\t\t\t\"10:2147483647\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rollback scopes deletion by forkPointHeight, never rewindCursor (#11)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait commit(sink, \"13:0\", [\n\t\t\t\t\t[9, \"a\"],\n\t\t\t\t\t[10, \"b\"],\n\t\t\t\t\t[11, \"c\"],\n\t\t\t\t\t[12, \"d\"],\n\t\t\t\t\t[13, \"e\"],\n\t\t\t\t]);\n\t\t\t\t// A multi-fork page: every call carries the SAME rewindCursor (the\n\t\t\t\t// lowest fork point) with its OWN forkPointHeight. This call's pair\n\t\t\t\t// is deliberately mismatched — fork at 12, rewind below 10. A sink\n\t\t\t\t// deriving the undo range from the cursor deletes 10-11 here.\n\t\t\t\tawait sink.rollback(12, \"9:2147483647\");\n\t\t\t\texpectRows(\"scope by height\", await harness.readRows(), [\n\t\t\t\t\t{ height: 9, key: \"a\" },\n\t\t\t\t\t{ height: 10, key: \"b\" },\n\t\t\t\t\t{ height: 11, key: \"c\" },\n\t\t\t\t]);\n\t\t\t\tawait sink.rollback(10, \"9:2147483647\");\n\t\t\t\texpectRows(\"scope by height (second fork)\", await harness.readRows(), [\n\t\t\t\t\t{ height: 9, key: \"a\" },\n\t\t\t\t]);\n\t\t\t\texpectCursor(\n\t\t\t\t\t\"scope by height\",\n\t\t\t\t\tawait harness.readCursor(),\n\t\t\t\t\t\"9:2147483647\",\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t];\n\n\tif (harness.withLockHeld) {\n\t\tconst withLockHeld = harness.withLockHeld.bind(harness);\n\t\tprobes.push({\n\t\t\tname: \"lock contention: a second writer fails loudly, never interleaves (#13)\",\n\t\t\tasync run() {\n\t\t\t\tconst sink = await start();\n\t\t\t\tawait withLockHeld(async () => {\n\t\t\t\t\tawait expectRejects(\n\t\t\t\t\t\t\"lock contention\",\n\t\t\t\t\t\tsink.commitBatch(\"10:0\", async (tx) => {\n\t\t\t\t\t\t\tawait harness.insertRow(tx, 10, \"a\");\n\t\t\t\t\t\t}),\n\t\t\t\t\t\t\"commitBatch succeeded while another writer held the lock — two replicas would interleave commits\",\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t\texpectRows(\"lock contention\", await harness.readRows(), []);\n\t\t\t\texpectCursor(\"lock contention\", await harness.readCursor(), null);\n\t\t\t},\n\t\t});\n\t}\n\n\treturn probes;\n}\n\n/**\n * Register every conformance probe with a test framework: pass your\n * framework's `test` function (bun:test, vitest, jest — anything with a\n * `(name, fn)` signature). One probe becomes one test case.\n */\nexport function attachSinkConformance<Tx>(\n\ttest: (name: string, fn: () => Promise<void>) => void,\n\tharness: SinkConformanceHarness<Tx>,\n): void {\n\tfor (const probe of sinkConformanceProbes(harness)) {\n\t\ttest(probe.name, () => probe.run());\n\t}\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "5D137D9410DE790C64756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "2A425478905AA1C864756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -115,8 +115,11 @@ interface ConsumerSink<Tx = unknown> {
115
115
  * same `rewindCursor` (the lowest fork point) but its own
116
116
  * `forkPointHeight` — scope the undo by `forkPointHeight` only (contract
117
117
  * invariant #11), and expect re-application after a crash (#10).
118
+ *
119
+ * `rewindCursor` is `null` only for a fork at genesis: clear the
120
+ * checkpoint so the next run starts from the first event.
118
121
  */
119
- rollback(forkPointHeight: number, rewindCursor: string): Promise<void>;
122
+ rollback(forkPointHeight: number, rewindCursor: string | null): Promise<void>;
120
123
  }
121
124
  /** `ctx` gains `tx` exactly when a sink is attached; without one the shape
122
125
  * is unchanged (an intersection with `unknown` is an identity, so
@@ -356,11 +359,41 @@ type StreamsEventsSubscribeParams = {
356
359
  filters?: StreamsFilterMap;
357
360
  /** Abort to unsubscribe (the returned function does the same). */
358
361
  signal?: AbortSignal;
359
- /** Called for each pushed event, in order. */
362
+ /**
363
+ * Called for each pushed event, in order. The resume cursor advances only
364
+ * after this resolves, so a handler that throws sees the same event again
365
+ * on reconnect (at-least-once). Key durable writes by `cursor`.
366
+ */
360
367
  onEvent: (event: StreamsEvent) => void | Promise<void>;
361
- /** Called on a connection error; the subscription auto-reconnects from the
362
- * last delivered cursor unless the signal has aborted. */
368
+ /**
369
+ * Called on every failure. Transport errors (dropped socket, 5xx, 429, a
370
+ * stale connection, a throwing handler) reconnect from the last handled
371
+ * cursor with exponential backoff. Errors that a retry cannot fix (401,
372
+ * 4xx, a bad signature) end the subscription and reject its `done`.
373
+ */
363
374
  onError?: (err: unknown) => void;
375
+ /**
376
+ * First reconnect pause in ms (default 1000). Doubles per consecutive
377
+ * failure up to 30 s with jitter, resets once a frame arrives, and never
378
+ * undercuts a `Retry-After`.
379
+ */
380
+ reconnectDelayMs?: number;
381
+ /**
382
+ * Reconnect when no frame (pings included) arrives for this long (default
383
+ * 60000, three server heartbeats). Catches a half-open socket that never
384
+ * errors; set it above your instance's heartbeat interval.
385
+ */
386
+ staleAfterMs?: number;
387
+ };
388
+ /**
389
+ * Handle for a live subscription: call it to unsubscribe. `done` resolves
390
+ * after unsubscribe (or the signal aborting) and rejects with the error that
391
+ * ended the loop when a retry could not fix it, so a worker can `await`
392
+ * it instead of polling for silence.
393
+ */
394
+ type StreamsSubscription = {
395
+ (): void;
396
+ done: Promise<void>;
364
397
  };
365
398
  /**
366
399
  * The checkpoint the SDK computes for a batch. Persist `cursor` inside the same
@@ -425,8 +458,11 @@ type StreamsBatchContext = ConsumerBatchContext;
425
458
  * The checkpoint for a reorg rollback. Persist `cursor` (the rewind position)
426
459
  * inside the same transaction as your rollback so the two commit atomically.
427
460
  */
461
+ /** `cursor` is the rewind position the loop resumes from after `onReorg`:
462
+ * the foot of the fork point, or `null` for a fork at genesis. Persist it
463
+ * in the same transaction as the rollback. */
428
464
  type StreamsReorgContext = {
429
- cursor: string;
465
+ cursor: string | null;
430
466
  };
431
467
  type StreamsEventsConsumeParams<
432
468
  TTx = never,
@@ -446,7 +482,8 @@ type StreamsEventsConsumeParams<
446
482
  * the loop resumes from the sink's committed cursor, `onBatch` receives
447
483
  * `ctx.tx` and must write ONLY through it (rows and cursor commit in one
448
484
  * transaction), reorg rollback is automatic, and `onBatch`'s return value
449
- * is ignored.
485
+ * is ignored. Folds (balances) invert in the sink's `onRollback`, not in
486
+ * `onReorg`.
450
487
  */
451
488
  sink?: ConsumerSink<TTx>;
452
489
  /** Fires once per page, before `onBatch` and before any early return. */
@@ -494,12 +531,23 @@ type StreamsEventsConsumeParams<
494
531
  onBatch?: (events: D extends true ? IndexEvent[] : StreamsEvent[], envelope: StreamsEventsEnvelope, ctx: StreamsBatchContext & WithSinkTx<TTx>) => void | string | null | undefined | Promise<void> | Promise<string | null | undefined>;
495
532
  /**
496
533
  * Roll your projection back to `reorg.fork_point_height`, persisting
497
- * `ctx.cursor` in the same transaction. Called once per *new* reorg
498
- * (deduped in-memory, fork-ascending) before the SDK rewinds and re-reads the
499
- * now-canonical events. Omit it to ignore reorgs (events stay canonical, but
500
- * stale rows from an orphaned fork are left in place).
534
+ * `ctx.cursor` in the same transaction. Called once per *new* reorg at or
535
+ * below the checkpoint (deduped in-memory, fork-ascending) before the SDK
536
+ * rewinds and re-reads the now-canonical events. A reorg whose fork sits
537
+ * above the checkpoint is not a rollback (nothing past it was written) and
538
+ * is skipped. Omit it to ignore reorgs (events stay canonical, but stale
539
+ * rows from an orphaned fork are left in place).
540
+ *
541
+ * With `onReorg` or a `sink` attached, every empty page also polls
542
+ * `reorgs.list` (one extra request per idle poll) so a fork that lands
543
+ * while the consumer idles at the tip is rolled back too.
501
544
  */
502
545
  onReorg?: (reorg: StreamsReorg, ctx: StreamsReorgContext) => Promise<void> | void;
546
+ /** Deepest rewind one reorg may make below the checkpoint before the loop
547
+ * refuses with `ValidationError`. Default 1000 blocks. The fork point is
548
+ * server supplied and drives a delete on every declared table, so raise
549
+ * this only for a source you trust. */
550
+ maxRollbackDepth?: number;
503
551
  /** Page-fetch retries after the first failure (429/5xx/network only —
504
552
  * 4xx and handler throws always propagate). Default 3; `0` disables. */
505
553
  retryCount?: number;
@@ -562,8 +610,16 @@ type StreamsEventsReplayParams = {
562
610
  * Called once per finalized dump file, in block order, before live tailing.
563
611
  * Process the parquet with your own tooling (e.g. DuckDB) — the SDK does not
564
612
  * decode parquet. Use `client.dumps.download(file)` to fetch + verify bytes.
613
+ *
614
+ * Delivery is file-granular and at-least-once: a file whose range straddles
615
+ * `from` is handed over whole. `ctx.from` is that cursor (`null` from
616
+ * genesis); skip rows at or below it, or key rows by `cursor` so a re-run
617
+ * is an idempotent no-op. Files ending at or below `from` are not handed
618
+ * over at all.
565
619
  */
566
- onDumpFile: (file: StreamsDumpFile) => Promise<void> | void;
620
+ onDumpFile: (file: StreamsDumpFile, ctx: {
621
+ from: string | null;
622
+ }) => Promise<void> | void;
567
623
  /** Called per live page after the dump phase, like `consume`. */
568
624
  onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope) => Promise<string | null | undefined> | string | null | undefined;
569
625
  /**
@@ -699,9 +755,11 @@ type StreamsClient = {
699
755
  * Subscribe to the real-time SSE push surface. Calls `onEvent` for each new
700
756
  * canonical event as the server pushes it (chain cadence, not poll-bounded),
701
757
  * and verifies each frame's inline ed25519 signature when the client was
702
- * created with `verify`. Returns an unsubscribe function.
758
+ * created with `verify`. Returns an unsubscribe function whose `done`
759
+ * settles when the loop ends. Not reorg-aware: an event delivered then
760
+ * orphaned is never retracted. Durable writers use `consume()`.
703
761
  */
704
- subscribe(params: StreamsEventsSubscribeParams): () => void;
762
+ subscribe(params: StreamsEventsSubscribeParams): StreamsSubscription;
705
763
  };
706
764
  blocks: {
707
765
  events(heightOrHash: number | string): Promise<StreamsEventsListEnvelope>;
@@ -717,6 +775,8 @@ type StreamsClient = {
717
775
  type CreateStreamsClientOptions = {
718
776
  apiKey?: string;
719
777
  baseUrl?: string;
778
+ /** Deploy origin label sent as `x-sl-origin` (telemetry). Defaults to `cli`. */
779
+ origin?: "cli" | "mcp" | "session";
720
780
  fetchImpl?: FetchLike2;
721
781
  /**
722
782
  * Public base URL for bulk parquet dumps (the R2/CDN bucket root). Required
@@ -844,26 +904,30 @@ declare class ApiError extends SecondLayerError {
844
904
  body?: unknown;
845
905
  constructor(status: number, message: string, body?: unknown, code?: string, options?: SecondLayerErrorOptions);
846
906
  }
847
- /** Thrown on a 401 — by both the platform clients and Streams. */
907
+ /** Thrown on a 401 by both the instance clients and Streams. Carries the
908
+ * server's `{error, code}` envelope when it sent one, so a revoked token reads
909
+ * differently from a missing one. */
848
910
  declare class AuthError extends ApiError {
849
911
  readonly status: 401;
850
- constructor(message?: string);
912
+ constructor(message?: string, body?: unknown, code?: string);
851
913
  }
852
- /** Thrown on a 429 — by both the platform clients and Streams. `retryable`,
914
+ /** Thrown on a 429 by both the instance clients and Streams. `retryable`,
853
915
  * with `retryAfterSeconds` parsed from the `Retry-After` header when sent. */
854
916
  declare class RateLimitError extends ApiError {
855
917
  /** Raw `Retry-After` header value (seconds or HTTP-date). */
856
918
  readonly retryAfter?: string | undefined;
857
919
  readonly status: 429;
858
- constructor(message?: string, retryAfter?: string | undefined);
920
+ constructor(message?: string, retryAfter?: string | undefined, body?: unknown, code?: string);
859
921
  }
860
- /** Thrown on a 4xx the caller can fix (bad cursor, bad params). Never retried. */
922
+ /** Thrown on a 4xx the caller can fix (bad cursor, bad params). Never retried.
923
+ * `code` is the server's machine-readable code when the envelope carried one. */
861
924
  declare class ValidationError extends ApiError {
862
- constructor(message: string, status: number, body?: unknown);
925
+ constructor(message: string, status: number, body?: unknown, code?: string);
863
926
  }
864
- /** Thrown on a 5xx from the Streams API. `retryable`. */
927
+ /** Thrown on a 5xx from the Streams API, and on a failed signing-key fetch.
928
+ * `retryable`: the page retry policy tries again. */
865
929
  declare class StreamsServerError extends ApiError {
866
- constructor(message: string, status: number, body?: unknown);
930
+ constructor(message: string, status: number, body?: unknown, code?: string);
867
931
  }
868
932
  /** Thrown when response signature verification is enabled and fails. */
869
933
  declare class StreamsSignatureError extends SecondLayerError {
@@ -895,12 +959,17 @@ declare const Cursor: {
895
959
  * every reorg. The sentinel is int4 max (the `event_index`/`tx_index` column
896
960
  * type), larger than any real index, so nothing at `height - 1` survives the
897
961
  * keyset and the next returned row is exactly `(height, 0)`.
962
+ *
963
+ * `atHeight(0)` is `null`: the pre-genesis position both consume loops
964
+ * accept as "read everything from the first event". There is no string
965
+ * cursor below `0:0`, and `0:0` itself is exclusive, so returning it here
966
+ * skipped the genesis event.
898
967
  */
899
- atHeight(height: number): string;
968
+ atHeight(height: number): string | null;
900
969
  /** Parse a `<block>:<index>` cursor. Throws `ValidationError` if malformed. */
901
970
  parse(cursor: string): {
902
971
  blockHeight: number;
903
972
  eventIndex: number;
904
973
  };
905
974
  };
906
- export { isStxTransfer, isStxMint, isStxLock, isStxBurn, isPrint, isNftTransfer, isNftMint, isNftBurn, isFtTransfer, isFtMint, isFtBurn, decodeStxTransfer, decodeStxMint, decodeStxLock, decodeStxBurn, decodePrint, decodeNftTransfer, decodeNftMint, decodeNftBurn, decodeFtTransfer, decodeFtMint, decodeFtBurn, decode, createStreamsClient, ValidationError, StreamsTip, StreamsSignatureError, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorgContext, StreamsReorg, StreamsEventsSubscribeParams, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsDumpsManifest, StreamsDumps, StreamsDumpFile, StreamsConsumeParams, StreamsClient, StreamsCanonicalBlock, StreamsBatchContext, StreamsBatch, STREAMS_EVENT_TYPES, RateLimitError, NftTransferPayload2 as NftTransferPayload, NftTransferEvent, FtTransferPayload2 as FtTransferPayload, FtTransferEvent, FetchLike2 as FetchLike, DecodedStxTransferPayload, DecodedStxTransfer, DecodedStxMintPayload, DecodedStxMint, DecodedStxLockPayload, DecodedStxLock, DecodedStxBurnPayload, DecodedStxBurn, DecodedPrintValue, DecodedPrintPayload, DecodedPrint, DecodedNftTransferPayload, DecodedNftTransfer, DecodedNftMintPayload, DecodedNftMint, DecodedNftBurnPayload, DecodedNftBurn, DecodedFtTransferPayload, DecodedFtTransfer, DecodedFtMintPayload, DecodedFtMint, DecodedFtBurnPayload, DecodedFtBurn, DecodedEventRow, DecodedEventColumns, Cursor, ConsumerBatchContext, AuthError };
975
+ export { isStxTransfer, isStxMint, isStxLock, isStxBurn, isPrint, isNftTransfer, isNftMint, isNftBurn, isFtTransfer, isFtMint, isFtBurn, decodeStxTransfer, decodeStxMint, decodeStxLock, decodeStxBurn, decodePrint, decodeNftTransfer, decodeNftMint, decodeNftBurn, decodeFtTransfer, decodeFtMint, decodeFtBurn, decode, createStreamsClient, ValidationError, StreamsTip, StreamsSubscription, StreamsSignatureError, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorgContext, StreamsReorg, StreamsEventsSubscribeParams, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsDumpsManifest, StreamsDumps, StreamsDumpFile, StreamsConsumeParams, StreamsClient, StreamsCanonicalBlock, StreamsBatchContext, StreamsBatch, STREAMS_EVENT_TYPES, RateLimitError, NftTransferPayload2 as NftTransferPayload, NftTransferEvent, FtTransferPayload2 as FtTransferPayload, FtTransferEvent, FetchLike2 as FetchLike, DecodedStxTransferPayload, DecodedStxTransfer, DecodedStxMintPayload, DecodedStxMint, DecodedStxLockPayload, DecodedStxLock, DecodedStxBurnPayload, DecodedStxBurn, DecodedPrintValue, DecodedPrintPayload, DecodedPrint, DecodedNftTransferPayload, DecodedNftTransfer, DecodedNftMintPayload, DecodedNftMint, DecodedNftBurnPayload, DecodedNftBurn, DecodedFtTransferPayload, DecodedFtTransfer, DecodedFtMintPayload, DecodedFtMint, DecodedFtBurnPayload, DecodedFtBurn, DecodedEventRow, DecodedEventColumns, Cursor, ConsumerBatchContext, AuthError };