@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.
- package/AGENTS.md +85 -17
- package/README.md +98 -31
- package/dist/index.d.ts +234 -113
- package/dist/index.js +768 -436
- package/dist/index.js.map +19 -19
- package/dist/sinks/bun-sqlite.d.ts +18 -4
- package/dist/sinks/bun-sqlite.js +21 -20
- package/dist/sinks/bun-sqlite.js.map +5 -5
- package/dist/sinks/core.d.ts +30 -7
- package/dist/sinks/core.js +16 -19
- package/dist/sinks/core.js.map +4 -4
- package/dist/sinks/drizzle.d.ts +23 -6
- package/dist/sinks/drizzle.js +22 -21
- package/dist/sinks/drizzle.js.map +5 -5
- package/dist/sinks/kysely.d.ts +26 -11
- package/dist/sinks/kysely.js +23 -21
- package/dist/sinks/kysely.js.map +5 -5
- package/dist/sinks/testing.d.ts +4 -1
- package/dist/sinks/testing.js +14 -1
- package/dist/sinks/testing.js.map +3 -3
- package/dist/streams/index.d.ts +92 -23
- package/dist/streams/index.js +395 -183
- package/dist/streams/index.js.map +12 -12
- package/dist/subgraphs/index.d.ts +202 -94
- package/dist/subgraphs/index.js +764 -434
- package/dist/subgraphs/index.js.map +17 -17
- package/package.json +4 -4
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
1
|
/**
|
|
3
2
|
* A consumer sink: the destination adapter a consume loop writes through.
|
|
4
3
|
*
|
|
@@ -115,19 +114,34 @@ interface ConsumerSink<Tx = unknown> {
|
|
|
115
114
|
* same `rewindCursor` (the lowest fork point) but its own
|
|
116
115
|
* `forkPointHeight` — scope the undo by `forkPointHeight` only (contract
|
|
117
116
|
* invariant #11), and expect re-application after a crash (#10).
|
|
117
|
+
*
|
|
118
|
+
* `rewindCursor` is `null` only for a fork at genesis: clear the
|
|
119
|
+
* checkpoint so the next run starts from the first event.
|
|
118
120
|
*/
|
|
119
|
-
rollback(forkPointHeight: number, rewindCursor: string): Promise<void>;
|
|
121
|
+
rollback(forkPointHeight: number, rewindCursor: string | null): Promise<void>;
|
|
120
122
|
}
|
|
123
|
+
/** Context handed to {@link CreateSinkOptions.onRollback}. `forkPointHeight`
|
|
124
|
+
* is the undo range (inclusive `>=`); `rewindCursor` is what the checkpoint
|
|
125
|
+
* will become after this transaction — `null` only for a fork at genesis. */
|
|
126
|
+
type SinkRollbackContext = {
|
|
127
|
+
forkPointHeight: number;
|
|
128
|
+
rewindCursor: string | null;
|
|
129
|
+
};
|
|
130
|
+
import { Database } from "bun:sqlite";
|
|
121
131
|
interface BunSqliteSinkOptions {
|
|
122
132
|
/** Checkpoint identity: the cursor row's primary key. */
|
|
123
133
|
id: string;
|
|
124
|
-
/** Rollback scope —
|
|
134
|
+
/** Rollback scope — fact tables the handler writes (see kyselySink).
|
|
135
|
+
* Invert folds (balances) in `onRollback`, do not declare them here. */
|
|
125
136
|
tables: readonly string[];
|
|
126
137
|
/** The block-height stamp column, present on every declared table
|
|
127
138
|
* (first-use checked). Append-only projections only. */
|
|
128
139
|
height: string;
|
|
129
140
|
/** Checkpoint table name. Default `sl_consumer_checkpoints`. */
|
|
130
141
|
checkpointTable?: string;
|
|
142
|
+
/** Same transaction as the fact-table delete, before it. Doomed rows are
|
|
143
|
+
* still visible; a throw aborts the rewind. */
|
|
144
|
+
onRollback?: (tx: Database, ctx: SinkRollbackContext) => Promise<void> | void;
|
|
131
145
|
}
|
|
132
146
|
/**
|
|
133
147
|
* A {@link ConsumerSink} over a `bun:sqlite` database — the zero-dependency,
|
|
@@ -155,4 +169,4 @@ interface BunSqliteSinkOptions {
|
|
|
155
169
|
* ```
|
|
156
170
|
*/
|
|
157
171
|
declare function bunSqliteSink(db: Database, options: BunSqliteSinkOptions): ConsumerSink<Database>;
|
|
158
|
-
export { bunSqliteSink, BunSqliteSinkOptions };
|
|
172
|
+
export { bunSqliteSink, SinkRollbackContext, BunSqliteSinkOptions };
|
package/dist/sinks/bun-sqlite.js
CHANGED
|
@@ -85,8 +85,8 @@ class ApiError extends SecondLayerError {
|
|
|
85
85
|
|
|
86
86
|
class AuthError extends ApiError {
|
|
87
87
|
status = 401;
|
|
88
|
-
constructor(message = "API key invalid or expired.") {
|
|
89
|
-
super(401, message);
|
|
88
|
+
constructor(message = "API key invalid or expired.", body, code) {
|
|
89
|
+
super(401, message, body, code);
|
|
90
90
|
this.name = "AuthError";
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -94,8 +94,8 @@ class AuthError extends ApiError {
|
|
|
94
94
|
class RateLimitError extends ApiError {
|
|
95
95
|
retryAfter;
|
|
96
96
|
status = 429;
|
|
97
|
-
constructor(message = "Rate limited. Try again later.", retryAfter) {
|
|
98
|
-
super(429, message,
|
|
97
|
+
constructor(message = "Rate limited. Try again later.", retryAfter, body, code) {
|
|
98
|
+
super(429, message, body, code, {
|
|
99
99
|
retryable: true,
|
|
100
100
|
retryAfterSeconds: parseRetryAfter(retryAfter)
|
|
101
101
|
});
|
|
@@ -105,8 +105,8 @@ class RateLimitError extends ApiError {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
class ValidationError extends ApiError {
|
|
108
|
-
constructor(message, status, body) {
|
|
109
|
-
super(status, message, body,
|
|
108
|
+
constructor(message, status, body, code) {
|
|
109
|
+
super(status, message, body, code, { retryable: false });
|
|
110
110
|
this.name = "ValidationError";
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -122,17 +122,6 @@ function parseRetryAfter(value) {
|
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
class VersionConflictError extends ApiError {
|
|
126
|
-
currentVersion;
|
|
127
|
-
expectedVersion;
|
|
128
|
-
constructor(currentVersion, expectedVersion, message = `Version conflict: expected ${expectedVersion}, current ${currentVersion}`) {
|
|
129
|
-
super(409, message, { currentVersion, expectedVersion });
|
|
130
|
-
this.currentVersion = currentVersion;
|
|
131
|
-
this.expectedVersion = expectedVersion;
|
|
132
|
-
this.name = "VersionConflictError";
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
125
|
// src/sinks/core.ts
|
|
137
126
|
var DEFAULT_CHECKPOINT_TABLE = "sl_consumer_checkpoints";
|
|
138
127
|
function assertSqlIdentifier(name, label) {
|
|
@@ -174,10 +163,18 @@ function createSink(driver, options) {
|
|
|
174
163
|
async rollback(forkPointHeight, rewindCursor) {
|
|
175
164
|
await driver.transact(async (tx) => {
|
|
176
165
|
await driver.acquireLock?.(tx);
|
|
166
|
+
await options.onRollback?.(tx, { forkPointHeight, rewindCursor });
|
|
177
167
|
for (const table of options.tables) {
|
|
178
168
|
await driver.deleteAtOrAbove(tx, table, forkPointHeight);
|
|
179
169
|
}
|
|
180
|
-
|
|
170
|
+
if (rewindCursor === null) {
|
|
171
|
+
if (!driver.clearCursor) {
|
|
172
|
+
throw new ValidationError(`${label}: this driver has no clearCursor, so a rewind to pre-genesis cannot be committed. Add clearCursor to the driver, or resume with an explicit fromCursor.`, 400);
|
|
173
|
+
}
|
|
174
|
+
await driver.clearCursor(tx);
|
|
175
|
+
} else {
|
|
176
|
+
await driver.writeCursor(tx, rewindCursor);
|
|
177
|
+
}
|
|
181
178
|
});
|
|
182
179
|
}
|
|
183
180
|
};
|
|
@@ -209,6 +206,9 @@ function bunSqliteSink(db, options) {
|
|
|
209
206
|
async writeCursor(tx, cursor) {
|
|
210
207
|
tx.query(`INSERT INTO ${cp} (id, cursor) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET cursor = excluded.cursor`).run(options.id, cursor);
|
|
211
208
|
},
|
|
209
|
+
async clearCursor(tx) {
|
|
210
|
+
tx.query(`DELETE FROM ${cp} WHERE id = ?`).run(options.id);
|
|
211
|
+
},
|
|
212
212
|
async deleteAtOrAbove(tx, table, height) {
|
|
213
213
|
tx.query(`DELETE FROM ${quoteIdent(table)} WHERE ${quoteIdent(options.height)} >= ?`).run(height);
|
|
214
214
|
},
|
|
@@ -222,12 +222,13 @@ function bunSqliteSink(db, options) {
|
|
|
222
222
|
id: options.id,
|
|
223
223
|
tables: options.tables,
|
|
224
224
|
height: options.height,
|
|
225
|
-
checkpointTable
|
|
225
|
+
checkpointTable,
|
|
226
|
+
onRollback: options.onRollback
|
|
226
227
|
});
|
|
227
228
|
}
|
|
228
229
|
export {
|
|
229
230
|
bunSqliteSink
|
|
230
231
|
};
|
|
231
232
|
|
|
232
|
-
//# debugId=
|
|
233
|
+
//# debugId=98C041CE25A11F8364756E2164756E21
|
|
233
234
|
//# sourceMappingURL=bun-sqlite.js.map
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["src/errors.ts", "src/sinks/core.ts", "src/sinks/bun-sqlite.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"const DOCS_BASE = \"https://www.secondlayer.tools\";\n\n/** Options accepted by every error in the family. */\nexport interface SecondLayerErrorOptions {\n\tcause?: Error;\n\tdetails?: string;\n\t/** Stable machine-readable code (e.g. `UPGRADE_REQUIRED`). */\n\tcode?: string;\n\t/** Docs page that explains this failure and its fix. */\n\tdocsUrl?: string;\n\t/** Extra context lines appended to the message (DDL to run, next steps). */\n\tmetaMessages?: string[];\n\t/** Whether retrying the SAME request can succeed (429/5xx/network). */\n\tretryable?: boolean;\n\t/** Parsed `Retry-After`, in seconds — set on rate limits that carry it. */\n\tretryAfterSeconds?: number;\n}\n\n/**\n * Root of the SDK error family. Implements the same protocol as `BaseError`\n * in `@secondlayer/stacks` (`shortMessage`, `cause`, `toJSON`) so a failed\n * chain read and a failed platform read serialize and introspect identically.\n * It intentionally does NOT extend that class: cross-package `instanceof` is\n * unreliable under per-package bundling anyway (match on `code`/`name` across\n * packages).\n *\n * `retryable` is the signal the consume loops act on: 429/5xx/network are\n * retryable, 4xx and body-serialization failures are not.\n */\nexport class SecondLayerError extends Error {\n\toverride name = \"SecondLayerError\";\n\t/** The one-line failure, without the appended context blocks. */\n\tshortMessage: string;\n\tcode?: string;\n\tdocsUrl?: string;\n\tmetaMessages?: string[];\n\tretryable: boolean;\n\tretryAfterSeconds?: number;\n\n\tconstructor(shortMessage: string, options: SecondLayerErrorOptions = {}) {\n\t\tconst detailBlocks = [\n\t\t\t...(options.metaMessages ?? []),\n\t\t\t...(options.details ? [options.details] : []),\n\t\t\t...(options.docsUrl ? [`Docs: ${options.docsUrl}`] : []),\n\t\t];\n\t\tconst message = [\n\t\t\tshortMessage,\n\t\t\tdetailBlocks.length > 0 ? `\\n${detailBlocks.join(\"\\n\")}` : \"\",\n\t\t].join(\"\");\n\t\tsuper(message, options.cause ? { cause: options.cause } : undefined);\n\t\tthis.shortMessage = shortMessage;\n\t\tthis.code = options.code;\n\t\tthis.docsUrl = options.docsUrl;\n\t\tthis.metaMessages = options.metaMessages;\n\t\tthis.retryable = options.retryable ?? false;\n\t\tthis.retryAfterSeconds = options.retryAfterSeconds;\n\t}\n\n\t/**\n\t * Walk the cause chain. With a predicate, returns the first error matching\n\t * it (or `null`); without one, returns the deepest cause. Mirrors viem's\n\t * `error.walk()` so cross-library error handling reads the same.\n\t */\n\twalk(fn?: (err: unknown) => boolean): unknown {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: cause chains are untyped by nature\n\t\tlet err: any = this;\n\t\tlet last: unknown = this;\n\t\twhile (err != null) {\n\t\t\tif (fn?.(err)) return err;\n\t\t\tlast = err;\n\t\t\terr = err.cause;\n\t\t}\n\t\treturn fn ? null : last;\n\t}\n\n\ttoJSON(): {\n\t\tname: string;\n\t\tmessage: string;\n\t\tshortMessage: string;\n\t\tcause: string | undefined;\n\t\tcode: string | undefined;\n\t\tdocsUrl: string | undefined;\n\t\tretryable: boolean;\n\t\tretryAfterSeconds: number | undefined;\n\t} {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\tmessage: this.message,\n\t\t\tshortMessage: this.shortMessage,\n\t\t\tcause: this.cause instanceof Error ? this.cause.message : undefined,\n\t\t\tcode: this.code,\n\t\t\tdocsUrl: this.docsUrl,\n\t\t\tretryable: this.retryable,\n\t\t\tretryAfterSeconds: this.retryAfterSeconds,\n\t\t};\n\t}\n}\n\n/** Retryability of an HTTP status: 429 and 5xx are transient. */\nfunction statusRetryable(status: number): boolean {\n\treturn status === 429 || status >= 500;\n}\n\n/** Docs page for a status/code pair, when one exists. */\nfunction docsFor(status: number, code?: string): string | undefined {\n\tif (code === \"UPGRADE_REQUIRED\" || status === 402)\n\t\treturn `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;\n\tif (status === 429 || status === 401)\n\t\treturn `${DOCS_BASE}/docs/authentication`;\n\treturn undefined;\n}\n\n/**\n * Error thrown by {@link SecondLayer} when an API request fails.\n * Includes the HTTP status code for programmatic error handling.\n *\n * @example\n * ```ts\n * try {\n * await client.subgraphs.status(\"my-subgraph\");\n * } catch (err) {\n * if (err instanceof ApiError && err.status === 404) {\n * console.log(\"Subgraph not found\");\n * }\n * }\n * ```\n */\nexport class ApiError extends SecondLayerError {\n\tconstructor(\n\t\t/** HTTP status code (0 for network errors). */\n\t\tpublic status: number,\n\t\tmessage: string,\n\t\t/** Raw response body (parsed JSON if possible) — preserved for callers that need error details. */\n\t\tpublic body?: unknown,\n\t\t/** Stable machine-readable code from the API's `{error, code}` error envelope. */\n\t\tcode?: string,\n\t\toptions: SecondLayerErrorOptions = {},\n\t) {\n\t\tsuper(message, {\n\t\t\tcode,\n\t\t\tretryable: options.retryable ?? statusRetryable(status),\n\t\t\tdocsUrl: options.docsUrl ?? docsFor(status, code),\n\t\t\t...(options.cause ? { cause: options.cause } : {}),\n\t\t\t...(options.details ? { details: options.details } : {}),\n\t\t\t...(options.metaMessages ? { metaMessages: options.metaMessages } : {}),\n\t\t\t...(options.retryAfterSeconds !== undefined\n\t\t\t\t? { retryAfterSeconds: options.retryAfterSeconds }\n\t\t\t\t: {}),\n\t\t});\n\t\tthis.name = \"ApiError\";\n\t}\n}\n\n/** Thrown on a 401
|
|
6
|
-
"import { ValidationError } from \"../errors.ts\";\nimport type { ConsumerSink } from \"./types.ts\";\n\n/** Default checkpoint table name, shared by every SQL-backed sink so two\n * sinks on the same database land in one table keyed by `id`. */\nexport const DEFAULT_CHECKPOINT_TABLE = \"sl_consumer_checkpoints\";\n\n/**\n * The driver surface a database adapter implements — the ~7 methods that\n * actually differ between SQL stores. Everything above them (the\n * commit/rollback call sequences, the guards, identifier validation, the\n * checkpoint schema) is policy owned by {@link createSink}, so a driver\n * cannot get the ordering wrong: it never sees a cursor write outside the\n * transaction that carries the rows, and never sees a delete without the\n * rewound cursor riding along.\n *\n * Identifier args (`table`, `column`) arrive pre-validated against\n * `[A-Za-z_][A-Za-z0-9_]*` — safe to interpolate after quoting (see\n * {@link quoteIdent}). Values (cursor, height, id) must still be bound as\n * parameters.\n */\nexport interface SinkDriver<Tx> {\n\t/** Run `fn` inside ONE transaction: begin, run, commit — a throw from\n\t * `fn` must roll the whole transaction back and re-throw. */\n\ttransact<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;\n\t/** Create the checkpoint store if missing: `(id primary key, cursor)`.\n\t * Must be safe under concurrent first start (two consumers racing the\n\t * same CREATE). */\n\tensureCheckpointStore(): Promise<void>;\n\t/** The committed cursor for this sink's `id`, or `null` if none. */\n\treadCursor(): Promise<string | null>;\n\t/** Upsert this sink's cursor row inside `tx`. */\n\twriteCursor(tx: Tx, cursor: string): Promise<void>;\n\t/** Delete rows with `height column >= height` from `table`, inside `tx`. */\n\tdeleteAtOrAbove(tx: Tx, table: string, height: number): Promise<void>;\n\t/** Whether `table` exists AND carries `column` — the first-use check that\n\t * keeps reorg rollback from being a silent no-op. */\n\thasColumn(table: string, column: string): Promise<boolean>;\n\t/** Optional: assert this consumer is the only live writer for its `id`,\n\t * inside `tx`. Must FAIL LOUDLY (throw) when another writer holds it —\n\t * never block or interleave (contract invariant #13). */\n\tacquireLock?(tx: Tx): Promise<void>;\n}\n\n/** Options for {@link createSink} — the policy inputs, driver-agnostic.\n * Concrete sinks re-expose these with schema-typed `tables`/`height`. */\nexport interface CreateSinkOptions {\n\t/** Error-message prefix naming the concrete sink (e.g. `\"kyselySink\"`),\n\t * so a thrown guard points at the thing the user actually constructed. */\n\tlabel: string;\n\t/** Checkpoint identity AND concurrency key. */\n\tid: string;\n\t/** Rollback scope: on a reorg, rows at/above the fork point are deleted\n\t * from exactly these tables. */\n\ttables: readonly string[];\n\t/** The block-height stamp column, present on every declared table. */\n\theight: string;\n\t/** Checkpoint table name. Default {@link DEFAULT_CHECKPOINT_TABLE}. */\n\tcheckpointTable?: string;\n\t/** Forwarded to {@link ConsumerSink.capabilities}. */\n\tcapabilities?: ConsumerSink[\"capabilities\"];\n}\n\n/** Throw unless `name` is a bare SQL identifier (letters, digits,\n * underscores; no leading digit) — the guard that makes interpolating\n * table/column names into DDL and DELETE statements safe. */\nexport function assertSqlIdentifier(name: string, label: string): void {\n\tif (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n\t\tthrow new ValidationError(\n\t\t\t`${label}: invalid identifier \"${name}\" (letters, digits, underscores only).`,\n\t\t\t400,\n\t\t);\n\t}\n}\n\n/** Double-quote a (pre-validated) identifier for interpolation. */\nexport function quoteIdent(name: string): string {\n\treturn `\"${name}\"`;\n}\n\n/**\n * Build a {@link ConsumerSink} from a {@link SinkDriver}: the portable ~90\n * lines every SQL sink otherwise re-implements — and re-risks. Owns the two\n * transaction sequences (begin → lock → write rows → write cursor;\n * begin → lock → delete `>=` fork → write rewound cursor), the empty-`tables
|
|
7
|
-
"import type { Database } from \"bun:sqlite\";\nimport {\n\tDEFAULT_CHECKPOINT_TABLE,\n\ttype SinkDriver,\n\tcreateSink,\n\tquoteIdent,\n} from \"./core.ts\";\nimport type { ConsumerSink } from \"./types.ts\";\n\nexport interface BunSqliteSinkOptions {\n\t/** Checkpoint identity: the cursor row's primary key. */\n\tid: string;\n\t/** Rollback scope —
|
|
5
|
+
"const DOCS_BASE = \"https://www.secondlayer.tools\";\n\n/** Options accepted by every error in the family. */\nexport interface SecondLayerErrorOptions {\n\tcause?: Error;\n\tdetails?: string;\n\t/** Stable machine-readable code (e.g. `UPGRADE_REQUIRED`). */\n\tcode?: string;\n\t/** Docs page that explains this failure and its fix. */\n\tdocsUrl?: string;\n\t/** Extra context lines appended to the message (DDL to run, next steps). */\n\tmetaMessages?: string[];\n\t/** Whether retrying the SAME request can succeed (429/5xx/network). */\n\tretryable?: boolean;\n\t/** Parsed `Retry-After`, in seconds — set on rate limits that carry it. */\n\tretryAfterSeconds?: number;\n}\n\n/**\n * Root of the SDK error family. Implements the same protocol as `BaseError`\n * in `@secondlayer/stacks` (`shortMessage`, `cause`, `toJSON`) so a failed\n * chain read and a failed platform read serialize and introspect identically.\n * It intentionally does NOT extend that class: cross-package `instanceof` is\n * unreliable under per-package bundling anyway (match on `code`/`name` across\n * packages).\n *\n * `retryable` is the signal the consume loops act on: 429/5xx/network are\n * retryable, 4xx and body-serialization failures are not.\n */\nexport class SecondLayerError extends Error {\n\toverride name = \"SecondLayerError\";\n\t/** The one-line failure, without the appended context blocks. */\n\tshortMessage: string;\n\tcode?: string;\n\tdocsUrl?: string;\n\tmetaMessages?: string[];\n\tretryable: boolean;\n\tretryAfterSeconds?: number;\n\n\tconstructor(shortMessage: string, options: SecondLayerErrorOptions = {}) {\n\t\tconst detailBlocks = [\n\t\t\t...(options.metaMessages ?? []),\n\t\t\t...(options.details ? [options.details] : []),\n\t\t\t...(options.docsUrl ? [`Docs: ${options.docsUrl}`] : []),\n\t\t];\n\t\tconst message = [\n\t\t\tshortMessage,\n\t\t\tdetailBlocks.length > 0 ? `\\n${detailBlocks.join(\"\\n\")}` : \"\",\n\t\t].join(\"\");\n\t\tsuper(message, options.cause ? { cause: options.cause } : undefined);\n\t\tthis.shortMessage = shortMessage;\n\t\tthis.code = options.code;\n\t\tthis.docsUrl = options.docsUrl;\n\t\tthis.metaMessages = options.metaMessages;\n\t\tthis.retryable = options.retryable ?? false;\n\t\tthis.retryAfterSeconds = options.retryAfterSeconds;\n\t}\n\n\t/**\n\t * Walk the cause chain. With a predicate, returns the first error matching\n\t * it (or `null`); without one, returns the deepest cause. Mirrors viem's\n\t * `error.walk()` so cross-library error handling reads the same.\n\t */\n\twalk(fn?: (err: unknown) => boolean): unknown {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: cause chains are untyped by nature\n\t\tlet err: any = this;\n\t\tlet last: unknown = this;\n\t\twhile (err != null) {\n\t\t\tif (fn?.(err)) return err;\n\t\t\tlast = err;\n\t\t\terr = err.cause;\n\t\t}\n\t\treturn fn ? null : last;\n\t}\n\n\ttoJSON(): {\n\t\tname: string;\n\t\tmessage: string;\n\t\tshortMessage: string;\n\t\tcause: string | undefined;\n\t\tcode: string | undefined;\n\t\tdocsUrl: string | undefined;\n\t\tretryable: boolean;\n\t\tretryAfterSeconds: number | undefined;\n\t} {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\tmessage: this.message,\n\t\t\tshortMessage: this.shortMessage,\n\t\t\tcause: this.cause instanceof Error ? this.cause.message : undefined,\n\t\t\tcode: this.code,\n\t\t\tdocsUrl: this.docsUrl,\n\t\t\tretryable: this.retryable,\n\t\t\tretryAfterSeconds: this.retryAfterSeconds,\n\t\t};\n\t}\n}\n\n/** Retryability of an HTTP status: 429 and 5xx are transient. */\nfunction statusRetryable(status: number): boolean {\n\treturn status === 429 || status >= 500;\n}\n\n/** Docs page for a status/code pair, when one exists. */\nfunction docsFor(status: number, code?: string): string | undefined {\n\tif (code === \"UPGRADE_REQUIRED\" || status === 402)\n\t\treturn `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;\n\tif (status === 429 || status === 401)\n\t\treturn `${DOCS_BASE}/docs/authentication`;\n\treturn undefined;\n}\n\n/**\n * Error thrown by {@link SecondLayer} when an API request fails.\n * Includes the HTTP status code for programmatic error handling.\n *\n * @example\n * ```ts\n * try {\n * await client.subgraphs.status(\"my-subgraph\");\n * } catch (err) {\n * if (err instanceof ApiError && err.status === 404) {\n * console.log(\"Subgraph not found\");\n * }\n * }\n * ```\n */\nexport class ApiError extends SecondLayerError {\n\tconstructor(\n\t\t/** HTTP status code (0 for network errors). */\n\t\tpublic status: number,\n\t\tmessage: string,\n\t\t/** Raw response body (parsed JSON if possible) — preserved for callers that need error details. */\n\t\tpublic body?: unknown,\n\t\t/** Stable machine-readable code from the API's `{error, code}` error envelope. */\n\t\tcode?: string,\n\t\toptions: SecondLayerErrorOptions = {},\n\t) {\n\t\tsuper(message, {\n\t\t\tcode,\n\t\t\tretryable: options.retryable ?? statusRetryable(status),\n\t\t\tdocsUrl: options.docsUrl ?? docsFor(status, code),\n\t\t\t...(options.cause ? { cause: options.cause } : {}),\n\t\t\t...(options.details ? { details: options.details } : {}),\n\t\t\t...(options.metaMessages ? { metaMessages: options.metaMessages } : {}),\n\t\t\t...(options.retryAfterSeconds !== undefined\n\t\t\t\t? { retryAfterSeconds: options.retryAfterSeconds }\n\t\t\t\t: {}),\n\t\t});\n\t\tthis.name = \"ApiError\";\n\t}\n}\n\n/** Thrown on a 401 by both the instance clients and Streams. Carries the\n * server's `{error, code}` envelope when it sent one, so a revoked token reads\n * differently from a missing one. */\nexport class AuthError extends ApiError {\n\toverride readonly status = 401 as const;\n\n\tconstructor(\n\t\tmessage = \"API key invalid or expired.\",\n\t\tbody?: unknown,\n\t\tcode?: string,\n\t) {\n\t\tsuper(401, message, body, code);\n\t\tthis.name = \"AuthError\";\n\t}\n}\n\n/** Thrown on a 429 by both the instance clients and Streams. `retryable`,\n * with `retryAfterSeconds` parsed from the `Retry-After` header when sent. */\nexport class RateLimitError extends ApiError {\n\toverride readonly status = 429 as const;\n\n\tconstructor(\n\t\tmessage = \"Rate limited. Try again later.\",\n\t\t/** Raw `Retry-After` header value (seconds or HTTP-date). */\n\t\treadonly retryAfter?: string,\n\t\tbody?: unknown,\n\t\tcode?: string,\n\t) {\n\t\tsuper(429, message, body, code, {\n\t\t\tretryable: true,\n\t\t\tretryAfterSeconds: parseRetryAfter(retryAfter),\n\t\t});\n\t\tthis.name = \"RateLimitError\";\n\t}\n}\n\n/** Thrown on a 4xx the caller can fix (bad cursor, bad params). Never retried.\n * `code` is the server's machine-readable code when the envelope carried one. */\nexport class ValidationError extends ApiError {\n\tconstructor(message: string, status: number, body?: unknown, code?: string) {\n\t\tsuper(status, message, body, code, { retryable: false });\n\t\tthis.name = \"ValidationError\";\n\t}\n}\n\n/** Parse a `Retry-After` header: delta-seconds or an HTTP-date. */\nexport function parseRetryAfter(value?: string | null): number | undefined {\n\tif (!value) return undefined;\n\tconst seconds = Number(value);\n\tif (Number.isFinite(seconds) && seconds >= 0) return seconds;\n\tconst date = Date.parse(value);\n\tif (!Number.isNaN(date)) return Math.max(0, (date - Date.now()) / 1000);\n\treturn undefined;\n}\n",
|
|
6
|
+
"import { ValidationError } from \"../errors.ts\";\nimport type { ConsumerSink } from \"./types.ts\";\n\n/** Default checkpoint table name, shared by every SQL-backed sink so two\n * sinks on the same database land in one table keyed by `id`. */\nexport const DEFAULT_CHECKPOINT_TABLE = \"sl_consumer_checkpoints\";\n\n/**\n * The driver surface a database adapter implements — the ~7 methods that\n * actually differ between SQL stores. Everything above them (the\n * commit/rollback call sequences, the guards, identifier validation, the\n * checkpoint schema) is policy owned by {@link createSink}, so a driver\n * cannot get the ordering wrong: it never sees a cursor write outside the\n * transaction that carries the rows, and never sees a delete without the\n * rewound cursor riding along.\n *\n * Identifier args (`table`, `column`) arrive pre-validated against\n * `[A-Za-z_][A-Za-z0-9_]*` — safe to interpolate after quoting (see\n * {@link quoteIdent}). Values (cursor, height, id) must still be bound as\n * parameters.\n */\nexport interface SinkDriver<Tx> {\n\t/** Run `fn` inside ONE transaction: begin, run, commit — a throw from\n\t * `fn` must roll the whole transaction back and re-throw. */\n\ttransact<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;\n\t/** Create the checkpoint store if missing: `(id primary key, cursor)`.\n\t * Must be safe under concurrent first start (two consumers racing the\n\t * same CREATE). */\n\tensureCheckpointStore(): Promise<void>;\n\t/** The committed cursor for this sink's `id`, or `null` if none. */\n\treadCursor(): Promise<string | null>;\n\t/** Upsert this sink's cursor row inside `tx`. */\n\twriteCursor(tx: Tx, cursor: string): Promise<void>;\n\t/** Delete this sink's cursor row inside `tx`: a rewind to pre-genesis\n\t * (fork at block 0). Optional for drivers written before it existed;\n\t * without it a genesis rewind throws instead of writing a bad cursor. */\n\tclearCursor?(tx: Tx): Promise<void>;\n\t/** Delete rows with `height column >= height` from `table`, inside `tx`. */\n\tdeleteAtOrAbove(tx: Tx, table: string, height: number): Promise<void>;\n\t/** Whether `table` exists AND carries `column` — the first-use check that\n\t * keeps reorg rollback from being a silent no-op. */\n\thasColumn(table: string, column: string): Promise<boolean>;\n\t/** Optional: assert this consumer is the only live writer for its `id`,\n\t * inside `tx`. Must FAIL LOUDLY (throw) when another writer holds it —\n\t * never block or interleave (contract invariant #13). */\n\tacquireLock?(tx: Tx): Promise<void>;\n}\n\n/** Context handed to {@link CreateSinkOptions.onRollback}. `forkPointHeight`\n * is the undo range (inclusive `>=`); `rewindCursor` is what the checkpoint\n * will become after this transaction — `null` only for a fork at genesis. */\nexport type SinkRollbackContext = {\n\tforkPointHeight: number;\n\trewindCursor: string | null;\n};\n\n/** Options for {@link createSink} — the policy inputs, driver-agnostic.\n * Concrete sinks re-expose these with schema-typed `tables`/`height`. */\nexport interface CreateSinkOptions<Tx = unknown> {\n\t/** Error-message prefix naming the concrete sink (e.g. `\"kyselySink\"`),\n\t * so a thrown guard points at the thing the user actually constructed. */\n\tlabel: string;\n\t/** Checkpoint identity AND concurrency key. */\n\tid: string;\n\t/** Rollback scope: on a reorg, rows at/above the fork point are deleted\n\t * from exactly these tables. Fact tables only — a fold (balances,\n\t * counters) does not belong here; invert it in `onRollback`. */\n\ttables: readonly string[];\n\t/** The block-height stamp column, present on every declared table. */\n\theight: string;\n\t/** Checkpoint table name. Default {@link DEFAULT_CHECKPOINT_TABLE}. */\n\tcheckpointTable?: string;\n\t/** Forwarded to {@link ConsumerSink.capabilities}. */\n\tcapabilities?: ConsumerSink[\"capabilities\"];\n\t/** Runs inside the rollback transaction after the lock, before any\n\t * `DELETE … height >= fork`. Doomed fact rows are still visible.\n\t * Remaining facts are `height < forkPointHeight`. A throw aborts the\n\t * whole rewind (no delete, no cursor write). Called on every\n\t * `rollback`, including re-application after a crash — derive undo\n\t * from the doomed rows so a second pass is a no-op. */\n\tonRollback?: (tx: Tx, ctx: SinkRollbackContext) => Promise<void> | void;\n}\n\n/** Throw unless `name` is a bare SQL identifier (letters, digits,\n * underscores; no leading digit) — the guard that makes interpolating\n * table/column names into DDL and DELETE statements safe. */\nexport function assertSqlIdentifier(name: string, label: string): void {\n\tif (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n\t\tthrow new ValidationError(\n\t\t\t`${label}: invalid identifier \"${name}\" (letters, digits, underscores only).`,\n\t\t\t400,\n\t\t);\n\t}\n}\n\n/** Double-quote a (pre-validated) identifier for interpolation. */\nexport function quoteIdent(name: string): string {\n\treturn `\"${name}\"`;\n}\n\n/**\n * Build a {@link ConsumerSink} from a {@link SinkDriver}: the portable ~90\n * lines every SQL sink otherwise re-implements — and re-risks. Owns the two\n * transaction sequences (begin → lock → write rows → write cursor;\n * begin → lock → onRollback? → delete `>=` fork → write rewound cursor), the\n * empty-`tables` guard, identifier validation, and the first-use height-column\n * check.\n *\n * A driver implemented against this base cannot violate contract invariants\n * #4, #5, #8, #9, or #11 (see {@link ConsumerSink}) without breaking\n * `transact` itself — which is exactly what the conformance kit probes.\n */\nexport function createSink<Tx>(\n\tdriver: SinkDriver<Tx>,\n\toptions: CreateSinkOptions<Tx>,\n): ConsumerSink<Tx> {\n\tconst { label } = options;\n\tconst checkpointTable = options.checkpointTable ?? DEFAULT_CHECKPOINT_TABLE;\n\tif (options.tables.length === 0) {\n\t\tthrow new ValidationError(\n\t\t\t`${label}: \\`tables\\` is empty — declare every table the handler writes, or reorg rollback is a no-op and orphaned rows persist forever.`,\n\t\t\t400,\n\t\t);\n\t}\n\tassertSqlIdentifier(checkpointTable, label);\n\tassertSqlIdentifier(options.height, label);\n\tfor (const table of options.tables) assertSqlIdentifier(table, label);\n\n\treturn {\n\t\tcapabilities: options.capabilities,\n\n\t\tasync loadCursor() {\n\t\t\tawait driver.ensureCheckpointStore();\n\t\t\t// First-use validation of the rollback precondition: every declared\n\t\t\t// table must exist and carry the height column. Failing here beats a\n\t\t\t// rollback that silently deletes nothing during a reorg.\n\t\t\tfor (const table of options.tables) {\n\t\t\t\tif (!(await driver.hasColumn(table, options.height))) {\n\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t`${label}: table \"${table}\" has no \"${options.height}\" column — the height stamp is what makes reorg rollback possible. Add the column (or fix \\`height\\`).`,\n\t\t\t\t\t\t400,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn driver.readCursor();\n\t\t},\n\n\t\tasync commitBatch(cursor, write) {\n\t\t\tawait driver.transact(async (tx) => {\n\t\t\t\tawait driver.acquireLock?.(tx);\n\t\t\t\tawait write(tx);\n\t\t\t\tawait driver.writeCursor(tx, cursor);\n\t\t\t});\n\t\t},\n\n\t\tasync rollback(forkPointHeight, rewindCursor) {\n\t\t\tawait driver.transact(async (tx) => {\n\t\t\t\tawait driver.acquireLock?.(tx);\n\t\t\t\t// Folds (balances, last-wins) invert/recompute here, while doomed\n\t\t\t\t// fact rows are still visible. A throw aborts the rewind.\n\t\t\t\tawait options.onRollback?.(tx, { forkPointHeight, rewindCursor });\n\t\t\t\t// INCLUSIVE of the fork block: the new canonical chain re-supplies\n\t\t\t\t// it, and the consumer rewinds to re-read from `fork:0`.\n\t\t\t\tfor (const table of options.tables) {\n\t\t\t\t\tawait driver.deleteAtOrAbove(tx, table, forkPointHeight);\n\t\t\t\t}\n\t\t\t\tif (rewindCursor === null) {\n\t\t\t\t\tif (!driver.clearCursor) {\n\t\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t\t`${label}: this driver has no clearCursor, so a rewind to pre-genesis cannot be committed. Add clearCursor to the driver, or resume with an explicit fromCursor.`,\n\t\t\t\t\t\t\t400,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tawait driver.clearCursor(tx);\n\t\t\t\t} else {\n\t\t\t\t\tawait driver.writeCursor(tx, rewindCursor);\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t};\n}\n",
|
|
7
|
+
"import type { Database } from \"bun:sqlite\";\nimport {\n\tDEFAULT_CHECKPOINT_TABLE,\n\ttype SinkDriver,\n\ttype SinkRollbackContext,\n\tcreateSink,\n\tquoteIdent,\n} from \"./core.ts\";\nimport type { ConsumerSink } from \"./types.ts\";\n\nexport type { SinkRollbackContext };\n\nexport interface BunSqliteSinkOptions {\n\t/** Checkpoint identity: the cursor row's primary key. */\n\tid: string;\n\t/** Rollback scope — fact tables the handler writes (see kyselySink).\n\t * Invert folds (balances) in `onRollback`, do not declare them here. */\n\ttables: readonly string[];\n\t/** The block-height stamp column, present on every declared table\n\t * (first-use checked). Append-only projections only. */\n\theight: string;\n\t/** Checkpoint table name. Default `sl_consumer_checkpoints`. */\n\tcheckpointTable?: string;\n\t/** Same transaction as the fact-table delete, before it. Doomed rows are\n\t * still visible; a throw aborts the rewind. */\n\tonRollback?: (tx: Database, ctx: SinkRollbackContext) => Promise<void> | void;\n}\n\n/**\n * A {@link ConsumerSink} over a `bun:sqlite` database — the zero-dependency,\n * zero-docker sink: one file on disk, nothing to install, nothing to run.\n * `ctx.tx` inside `onBatch` is the `Database` itself, inside an open\n * transaction.\n *\n * Batches commit under `BEGIN IMMEDIATE`, which takes SQLite's single write\n * lock up front — so the single-writer requirement (contract invariant #13)\n * is enforced by the file lock itself: a second writer process fails with\n * SQLITE_BUSY instead of interleaving.\n *\n * ```ts\n * import { Database } from \"bun:sqlite\";\n * const db = new Database(\"sales.db\");\n * const sink = bunSqliteSink(db, { id: \"sales\", tables: [\"sales\"], height: \"block_height\" });\n * await index.contractCalls.consume({\n * contractId: MARKETPLACE, functionName: \"purchase-asset\", fromHeight: 0,\n * sink,\n * onBatch: (calls, _env, ctx) => {\n * for (const call of calls)\n * ctx.tx.run(\"INSERT OR IGNORE INTO sales (tx_id, block_height) VALUES (?, ?)\", [call.tx_id, call.block_height]);\n * },\n * });\n * ```\n */\nexport function bunSqliteSink(\n\tdb: Database,\n\toptions: BunSqliteSinkOptions,\n): ConsumerSink<Database> {\n\tconst checkpointTable = options.checkpointTable ?? DEFAULT_CHECKPOINT_TABLE;\n\tconst cp = quoteIdent(checkpointTable);\n\n\tconst driver: SinkDriver<Database> = {\n\t\t// Manual BEGIN/COMMIT rather than db.transaction(): the handler is\n\t\t// async, and a sync transaction wrapper would COMMIT at the first\n\t\t// await — writes after it would land in autocommit, outside the\n\t\t// atomicity the contract requires.\n\t\tasync transact(fn) {\n\t\t\tdb.exec(\"BEGIN IMMEDIATE\");\n\t\t\ttry {\n\t\t\t\tconst result = await fn(db);\n\t\t\t\tdb.exec(\"COMMIT\");\n\t\t\t\treturn result;\n\t\t\t} catch (err) {\n\t\t\t\tdb.exec(\"ROLLBACK\");\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t},\n\n\t\tasync ensureCheckpointStore() {\n\t\t\tdb.exec(\n\t\t\t\t`CREATE TABLE IF NOT EXISTS ${cp} (id TEXT PRIMARY KEY, cursor TEXT NOT NULL)`,\n\t\t\t);\n\t\t},\n\n\t\tasync readCursor() {\n\t\t\tconst row = db\n\t\t\t\t.query<{ cursor: string }, [string]>(\n\t\t\t\t\t`SELECT cursor FROM ${cp} WHERE id = ?`,\n\t\t\t\t)\n\t\t\t\t.get(options.id);\n\t\t\treturn row?.cursor ?? null;\n\t\t},\n\n\t\tasync writeCursor(tx, cursor) {\n\t\t\ttx.query(\n\t\t\t\t`INSERT INTO ${cp} (id, cursor) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET cursor = excluded.cursor`,\n\t\t\t).run(options.id, cursor);\n\t\t},\n\n\t\tasync clearCursor(tx) {\n\t\t\ttx.query(`DELETE FROM ${cp} WHERE id = ?`).run(options.id);\n\t\t},\n\n\t\tasync deleteAtOrAbove(tx, table, height) {\n\t\t\ttx.query(\n\t\t\t\t`DELETE FROM ${quoteIdent(table)} WHERE ${quoteIdent(options.height)} >= ?`,\n\t\t\t).run(height);\n\t\t},\n\n\t\tasync hasColumn(table, column) {\n\t\t\tconst row = db\n\t\t\t\t.query(\"SELECT 1 AS ok FROM pragma_table_info(?) WHERE name = ?\")\n\t\t\t\t.get(table, column);\n\t\t\treturn row !== null;\n\t\t},\n\t};\n\n\treturn createSink(driver, {\n\t\tlabel: \"bunSqliteSink\",\n\t\tid: options.id,\n\t\ttables: options.tables,\n\t\theight: options.height,\n\t\tcheckpointTable,\n\t\tonRollback: options.onRollback,\n\t});\n}\n"
|
|
8
8
|
],
|
|
9
|
-
"mappings": ";AAAA,IAAM,YAAY;AAAA;AA6BX,MAAM,yBAAyB,MAAM;AAAA,EAClC,OAAO;AAAA,EAEhB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,WAAW,CAAC,cAAsB,UAAmC,CAAC,GAAG;AAAA,IACxE,MAAM,eAAe;AAAA,MACpB,GAAI,QAAQ,gBAAgB,CAAC;AAAA,MAC7B,GAAI,QAAQ,UAAU,CAAC,QAAQ,OAAO,IAAI,CAAC;AAAA,MAC3C,GAAI,QAAQ,UAAU,CAAC,SAAS,QAAQ,SAAS,IAAI,CAAC;AAAA,IACvD;AAAA,IACA,MAAM,UAAU;AAAA,MACf;AAAA,MACA,aAAa,SAAS,IAAI;AAAA,EAAK,aAAa,KAAK;AAAA,CAAI,MAAM;AAAA,IAC5D,EAAE,KAAK,EAAE;AAAA,IACT,MAAM,SAAS,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,SAAS;AAAA,IACnE,KAAK,eAAe;AAAA,IACpB,KAAK,OAAO,QAAQ;AAAA,IACpB,KAAK,UAAU,QAAQ;AAAA,IACvB,KAAK,eAAe,QAAQ;AAAA,IAC5B,KAAK,YAAY,QAAQ,aAAa;AAAA,IACtC,KAAK,oBAAoB,QAAQ;AAAA;AAAA,EAQlC,IAAI,CAAC,IAAyC;AAAA,IAE7C,IAAI,MAAW;AAAA,IACf,IAAI,OAAgB;AAAA,IACpB,OAAO,OAAO,MAAM;AAAA,MACnB,IAAI,KAAK,GAAG;AAAA,QAAG,OAAO;AAAA,MACtB,OAAO;AAAA,MACP,MAAM,IAAI;AAAA,IACX;AAAA,IACA,OAAO,KAAK,OAAO;AAAA;AAAA,EAGpB,MAAM,GASJ;AAAA,IACD,OAAO;AAAA,MACN,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,cAAc,KAAK;AAAA,MACnB,OAAO,KAAK,iBAAiB,QAAQ,KAAK,MAAM,UAAU;AAAA,MAC1D,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,WAAW,KAAK;AAAA,MAChB,mBAAmB,KAAK;AAAA,IACzB;AAAA;AAEF;AAGA,SAAS,eAAe,CAAC,QAAyB;AAAA,EACjD,OAAO,WAAW,OAAO,UAAU;AAAA;AAIpC,SAAS,OAAO,CAAC,QAAgB,MAAmC;AAAA,EACnE,IAAI,SAAS,sBAAsB,WAAW;AAAA,IAC7C,OAAO,GAAG;AAAA,EACX,IAAI,WAAW,OAAO,WAAW;AAAA,IAChC,OAAO,GAAG;AAAA,EACX;AAAA;AAAA;AAkBM,MAAM,iBAAiB,iBAAiB;AAAA,EAGtC;AAAA,EAGA;AAAA,EALR,WAAW,CAEH,QACP,SAEO,MAEP,MACA,UAAmC,CAAC,GACnC;AAAA,IACD,MAAM,SAAS;AAAA,MACd;AAAA,MACA,WAAW,QAAQ,aAAa,gBAAgB,MAAM;AAAA,MACtD,SAAS,QAAQ,WAAW,QAAQ,QAAQ,IAAI;AAAA,SAC5C,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,SAC5C,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,SAClD,QAAQ,eAAe,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;AAAA,SACjE,QAAQ,sBAAsB,YAC/B,EAAE,mBAAmB,QAAQ,kBAAkB,IAC/C,CAAC;AAAA,IACL,CAAC;AAAA,IAlBM;AAAA,IAGA;AAAA,IAgBP,KAAK,OAAO;AAAA;AAEd;AAAA;
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "98C041CE25A11F8364756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|
package/dist/sinks/core.d.ts
CHANGED
|
@@ -114,8 +114,11 @@ interface ConsumerSink<Tx = unknown> {
|
|
|
114
114
|
* same `rewindCursor` (the lowest fork point) but its own
|
|
115
115
|
* `forkPointHeight` — scope the undo by `forkPointHeight` only (contract
|
|
116
116
|
* invariant #11), and expect re-application after a crash (#10).
|
|
117
|
+
*
|
|
118
|
+
* `rewindCursor` is `null` only for a fork at genesis: clear the
|
|
119
|
+
* checkpoint so the next run starts from the first event.
|
|
117
120
|
*/
|
|
118
|
-
rollback(forkPointHeight: number, rewindCursor: string): Promise<void>;
|
|
121
|
+
rollback(forkPointHeight: number, rewindCursor: string | null): Promise<void>;
|
|
119
122
|
}
|
|
120
123
|
/** Default checkpoint table name, shared by every SQL-backed sink so two
|
|
121
124
|
* sinks on the same database land in one table keyed by `id`. */
|
|
@@ -146,6 +149,10 @@ interface SinkDriver<Tx> {
|
|
|
146
149
|
readCursor(): Promise<string | null>;
|
|
147
150
|
/** Upsert this sink's cursor row inside `tx`. */
|
|
148
151
|
writeCursor(tx: Tx, cursor: string): Promise<void>;
|
|
152
|
+
/** Delete this sink's cursor row inside `tx`: a rewind to pre-genesis
|
|
153
|
+
* (fork at block 0). Optional for drivers written before it existed;
|
|
154
|
+
* without it a genesis rewind throws instead of writing a bad cursor. */
|
|
155
|
+
clearCursor?(tx: Tx): Promise<void>;
|
|
149
156
|
/** Delete rows with `height column >= height` from `table`, inside `tx`. */
|
|
150
157
|
deleteAtOrAbove(tx: Tx, table: string, height: number): Promise<void>;
|
|
151
158
|
/** Whether `table` exists AND carries `column` — the first-use check that
|
|
@@ -156,16 +163,24 @@ interface SinkDriver<Tx> {
|
|
|
156
163
|
* never block or interleave (contract invariant #13). */
|
|
157
164
|
acquireLock?(tx: Tx): Promise<void>;
|
|
158
165
|
}
|
|
166
|
+
/** Context handed to {@link CreateSinkOptions.onRollback}. `forkPointHeight`
|
|
167
|
+
* is the undo range (inclusive `>=`); `rewindCursor` is what the checkpoint
|
|
168
|
+
* will become after this transaction — `null` only for a fork at genesis. */
|
|
169
|
+
type SinkRollbackContext = {
|
|
170
|
+
forkPointHeight: number;
|
|
171
|
+
rewindCursor: string | null;
|
|
172
|
+
};
|
|
159
173
|
/** Options for {@link createSink} — the policy inputs, driver-agnostic.
|
|
160
174
|
* Concrete sinks re-expose these with schema-typed `tables`/`height`. */
|
|
161
|
-
interface CreateSinkOptions {
|
|
175
|
+
interface CreateSinkOptions<Tx = unknown> {
|
|
162
176
|
/** Error-message prefix naming the concrete sink (e.g. `"kyselySink"`),
|
|
163
177
|
* so a thrown guard points at the thing the user actually constructed. */
|
|
164
178
|
label: string;
|
|
165
179
|
/** Checkpoint identity AND concurrency key. */
|
|
166
180
|
id: string;
|
|
167
181
|
/** Rollback scope: on a reorg, rows at/above the fork point are deleted
|
|
168
|
-
* from exactly these tables.
|
|
182
|
+
* from exactly these tables. Fact tables only — a fold (balances,
|
|
183
|
+
* counters) does not belong here; invert it in `onRollback`. */
|
|
169
184
|
tables: readonly string[];
|
|
170
185
|
/** The block-height stamp column, present on every declared table. */
|
|
171
186
|
height: string;
|
|
@@ -173,6 +188,13 @@ interface CreateSinkOptions {
|
|
|
173
188
|
checkpointTable?: string;
|
|
174
189
|
/** Forwarded to {@link ConsumerSink.capabilities}. */
|
|
175
190
|
capabilities?: ConsumerSink["capabilities"];
|
|
191
|
+
/** Runs inside the rollback transaction after the lock, before any
|
|
192
|
+
* `DELETE … height >= fork`. Doomed fact rows are still visible.
|
|
193
|
+
* Remaining facts are `height < forkPointHeight`. A throw aborts the
|
|
194
|
+
* whole rewind (no delete, no cursor write). Called on every
|
|
195
|
+
* `rollback`, including re-application after a crash — derive undo
|
|
196
|
+
* from the doomed rows so a second pass is a no-op. */
|
|
197
|
+
onRollback?: (tx: Tx, ctx: SinkRollbackContext) => Promise<void> | void;
|
|
176
198
|
}
|
|
177
199
|
/** Throw unless `name` is a bare SQL identifier (letters, digits,
|
|
178
200
|
* underscores; no leading digit) — the guard that makes interpolating
|
|
@@ -184,12 +206,13 @@ declare function quoteIdent(name: string): string;
|
|
|
184
206
|
* Build a {@link ConsumerSink} from a {@link SinkDriver}: the portable ~90
|
|
185
207
|
* lines every SQL sink otherwise re-implements — and re-risks. Owns the two
|
|
186
208
|
* transaction sequences (begin → lock → write rows → write cursor;
|
|
187
|
-
* begin → lock → delete `>=` fork → write rewound cursor), the
|
|
188
|
-
* guard, identifier validation, and the first-use height-column
|
|
209
|
+
* begin → lock → onRollback? → delete `>=` fork → write rewound cursor), the
|
|
210
|
+
* empty-`tables` guard, identifier validation, and the first-use height-column
|
|
211
|
+
* check.
|
|
189
212
|
*
|
|
190
213
|
* A driver implemented against this base cannot violate contract invariants
|
|
191
214
|
* #4, #5, #8, #9, or #11 (see {@link ConsumerSink}) without breaking
|
|
192
215
|
* `transact` itself — which is exactly what the conformance kit probes.
|
|
193
216
|
*/
|
|
194
|
-
declare function createSink<Tx>(driver: SinkDriver<Tx>, options: CreateSinkOptions): ConsumerSink<Tx>;
|
|
195
|
-
export { quoteIdent, createSink, assertSqlIdentifier, SinkDriver, DEFAULT_CHECKPOINT_TABLE, CreateSinkOptions };
|
|
217
|
+
declare function createSink<Tx>(driver: SinkDriver<Tx>, options: CreateSinkOptions<Tx>): ConsumerSink<Tx>;
|
|
218
|
+
export { quoteIdent, createSink, assertSqlIdentifier, SinkRollbackContext, SinkDriver, DEFAULT_CHECKPOINT_TABLE, CreateSinkOptions };
|
package/dist/sinks/core.js
CHANGED
|
@@ -85,8 +85,8 @@ class ApiError extends SecondLayerError {
|
|
|
85
85
|
|
|
86
86
|
class AuthError extends ApiError {
|
|
87
87
|
status = 401;
|
|
88
|
-
constructor(message = "API key invalid or expired.") {
|
|
89
|
-
super(401, message);
|
|
88
|
+
constructor(message = "API key invalid or expired.", body, code) {
|
|
89
|
+
super(401, message, body, code);
|
|
90
90
|
this.name = "AuthError";
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -94,8 +94,8 @@ class AuthError extends ApiError {
|
|
|
94
94
|
class RateLimitError extends ApiError {
|
|
95
95
|
retryAfter;
|
|
96
96
|
status = 429;
|
|
97
|
-
constructor(message = "Rate limited. Try again later.", retryAfter) {
|
|
98
|
-
super(429, message,
|
|
97
|
+
constructor(message = "Rate limited. Try again later.", retryAfter, body, code) {
|
|
98
|
+
super(429, message, body, code, {
|
|
99
99
|
retryable: true,
|
|
100
100
|
retryAfterSeconds: parseRetryAfter(retryAfter)
|
|
101
101
|
});
|
|
@@ -105,8 +105,8 @@ class RateLimitError extends ApiError {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
class ValidationError extends ApiError {
|
|
108
|
-
constructor(message, status, body) {
|
|
109
|
-
super(status, message, body,
|
|
108
|
+
constructor(message, status, body, code) {
|
|
109
|
+
super(status, message, body, code, { retryable: false });
|
|
110
110
|
this.name = "ValidationError";
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -122,17 +122,6 @@ function parseRetryAfter(value) {
|
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
class VersionConflictError extends ApiError {
|
|
126
|
-
currentVersion;
|
|
127
|
-
expectedVersion;
|
|
128
|
-
constructor(currentVersion, expectedVersion, message = `Version conflict: expected ${expectedVersion}, current ${currentVersion}`) {
|
|
129
|
-
super(409, message, { currentVersion, expectedVersion });
|
|
130
|
-
this.currentVersion = currentVersion;
|
|
131
|
-
this.expectedVersion = expectedVersion;
|
|
132
|
-
this.name = "VersionConflictError";
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
125
|
// src/sinks/core.ts
|
|
137
126
|
var DEFAULT_CHECKPOINT_TABLE = "sl_consumer_checkpoints";
|
|
138
127
|
function assertSqlIdentifier(name, label) {
|
|
@@ -174,10 +163,18 @@ function createSink(driver, options) {
|
|
|
174
163
|
async rollback(forkPointHeight, rewindCursor) {
|
|
175
164
|
await driver.transact(async (tx) => {
|
|
176
165
|
await driver.acquireLock?.(tx);
|
|
166
|
+
await options.onRollback?.(tx, { forkPointHeight, rewindCursor });
|
|
177
167
|
for (const table of options.tables) {
|
|
178
168
|
await driver.deleteAtOrAbove(tx, table, forkPointHeight);
|
|
179
169
|
}
|
|
180
|
-
|
|
170
|
+
if (rewindCursor === null) {
|
|
171
|
+
if (!driver.clearCursor) {
|
|
172
|
+
throw new ValidationError(`${label}: this driver has no clearCursor, so a rewind to pre-genesis cannot be committed. Add clearCursor to the driver, or resume with an explicit fromCursor.`, 400);
|
|
173
|
+
}
|
|
174
|
+
await driver.clearCursor(tx);
|
|
175
|
+
} else {
|
|
176
|
+
await driver.writeCursor(tx, rewindCursor);
|
|
177
|
+
}
|
|
181
178
|
});
|
|
182
179
|
}
|
|
183
180
|
};
|
|
@@ -189,5 +186,5 @@ export {
|
|
|
189
186
|
DEFAULT_CHECKPOINT_TABLE
|
|
190
187
|
};
|
|
191
188
|
|
|
192
|
-
//# debugId=
|
|
189
|
+
//# debugId=849F49AD7AB025DA64756E2164756E21
|
|
193
190
|
//# sourceMappingURL=core.js.map
|
package/dist/sinks/core.js.map
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["src/errors.ts", "src/sinks/core.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"const DOCS_BASE = \"https://www.secondlayer.tools\";\n\n/** Options accepted by every error in the family. */\nexport interface SecondLayerErrorOptions {\n\tcause?: Error;\n\tdetails?: string;\n\t/** Stable machine-readable code (e.g. `UPGRADE_REQUIRED`). */\n\tcode?: string;\n\t/** Docs page that explains this failure and its fix. */\n\tdocsUrl?: string;\n\t/** Extra context lines appended to the message (DDL to run, next steps). */\n\tmetaMessages?: string[];\n\t/** Whether retrying the SAME request can succeed (429/5xx/network). */\n\tretryable?: boolean;\n\t/** Parsed `Retry-After`, in seconds — set on rate limits that carry it. */\n\tretryAfterSeconds?: number;\n}\n\n/**\n * Root of the SDK error family. Implements the same protocol as `BaseError`\n * in `@secondlayer/stacks` (`shortMessage`, `cause`, `toJSON`) so a failed\n * chain read and a failed platform read serialize and introspect identically.\n * It intentionally does NOT extend that class: cross-package `instanceof` is\n * unreliable under per-package bundling anyway (match on `code`/`name` across\n * packages).\n *\n * `retryable` is the signal the consume loops act on: 429/5xx/network are\n * retryable, 4xx and body-serialization failures are not.\n */\nexport class SecondLayerError extends Error {\n\toverride name = \"SecondLayerError\";\n\t/** The one-line failure, without the appended context blocks. */\n\tshortMessage: string;\n\tcode?: string;\n\tdocsUrl?: string;\n\tmetaMessages?: string[];\n\tretryable: boolean;\n\tretryAfterSeconds?: number;\n\n\tconstructor(shortMessage: string, options: SecondLayerErrorOptions = {}) {\n\t\tconst detailBlocks = [\n\t\t\t...(options.metaMessages ?? []),\n\t\t\t...(options.details ? [options.details] : []),\n\t\t\t...(options.docsUrl ? [`Docs: ${options.docsUrl}`] : []),\n\t\t];\n\t\tconst message = [\n\t\t\tshortMessage,\n\t\t\tdetailBlocks.length > 0 ? `\\n${detailBlocks.join(\"\\n\")}` : \"\",\n\t\t].join(\"\");\n\t\tsuper(message, options.cause ? { cause: options.cause } : undefined);\n\t\tthis.shortMessage = shortMessage;\n\t\tthis.code = options.code;\n\t\tthis.docsUrl = options.docsUrl;\n\t\tthis.metaMessages = options.metaMessages;\n\t\tthis.retryable = options.retryable ?? false;\n\t\tthis.retryAfterSeconds = options.retryAfterSeconds;\n\t}\n\n\t/**\n\t * Walk the cause chain. With a predicate, returns the first error matching\n\t * it (or `null`); without one, returns the deepest cause. Mirrors viem's\n\t * `error.walk()` so cross-library error handling reads the same.\n\t */\n\twalk(fn?: (err: unknown) => boolean): unknown {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: cause chains are untyped by nature\n\t\tlet err: any = this;\n\t\tlet last: unknown = this;\n\t\twhile (err != null) {\n\t\t\tif (fn?.(err)) return err;\n\t\t\tlast = err;\n\t\t\terr = err.cause;\n\t\t}\n\t\treturn fn ? null : last;\n\t}\n\n\ttoJSON(): {\n\t\tname: string;\n\t\tmessage: string;\n\t\tshortMessage: string;\n\t\tcause: string | undefined;\n\t\tcode: string | undefined;\n\t\tdocsUrl: string | undefined;\n\t\tretryable: boolean;\n\t\tretryAfterSeconds: number | undefined;\n\t} {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\tmessage: this.message,\n\t\t\tshortMessage: this.shortMessage,\n\t\t\tcause: this.cause instanceof Error ? this.cause.message : undefined,\n\t\t\tcode: this.code,\n\t\t\tdocsUrl: this.docsUrl,\n\t\t\tretryable: this.retryable,\n\t\t\tretryAfterSeconds: this.retryAfterSeconds,\n\t\t};\n\t}\n}\n\n/** Retryability of an HTTP status: 429 and 5xx are transient. */\nfunction statusRetryable(status: number): boolean {\n\treturn status === 429 || status >= 500;\n}\n\n/** Docs page for a status/code pair, when one exists. */\nfunction docsFor(status: number, code?: string): string | undefined {\n\tif (code === \"UPGRADE_REQUIRED\" || status === 402)\n\t\treturn `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;\n\tif (status === 429 || status === 401)\n\t\treturn `${DOCS_BASE}/docs/authentication`;\n\treturn undefined;\n}\n\n/**\n * Error thrown by {@link SecondLayer} when an API request fails.\n * Includes the HTTP status code for programmatic error handling.\n *\n * @example\n * ```ts\n * try {\n * await client.subgraphs.status(\"my-subgraph\");\n * } catch (err) {\n * if (err instanceof ApiError && err.status === 404) {\n * console.log(\"Subgraph not found\");\n * }\n * }\n * ```\n */\nexport class ApiError extends SecondLayerError {\n\tconstructor(\n\t\t/** HTTP status code (0 for network errors). */\n\t\tpublic status: number,\n\t\tmessage: string,\n\t\t/** Raw response body (parsed JSON if possible) — preserved for callers that need error details. */\n\t\tpublic body?: unknown,\n\t\t/** Stable machine-readable code from the API's `{error, code}` error envelope. */\n\t\tcode?: string,\n\t\toptions: SecondLayerErrorOptions = {},\n\t) {\n\t\tsuper(message, {\n\t\t\tcode,\n\t\t\tretryable: options.retryable ?? statusRetryable(status),\n\t\t\tdocsUrl: options.docsUrl ?? docsFor(status, code),\n\t\t\t...(options.cause ? { cause: options.cause } : {}),\n\t\t\t...(options.details ? { details: options.details } : {}),\n\t\t\t...(options.metaMessages ? { metaMessages: options.metaMessages } : {}),\n\t\t\t...(options.retryAfterSeconds !== undefined\n\t\t\t\t? { retryAfterSeconds: options.retryAfterSeconds }\n\t\t\t\t: {}),\n\t\t});\n\t\tthis.name = \"ApiError\";\n\t}\n}\n\n/** Thrown on a 401
|
|
6
|
-
"import { ValidationError } from \"../errors.ts\";\nimport type { ConsumerSink } from \"./types.ts\";\n\n/** Default checkpoint table name, shared by every SQL-backed sink so two\n * sinks on the same database land in one table keyed by `id`. */\nexport const DEFAULT_CHECKPOINT_TABLE = \"sl_consumer_checkpoints\";\n\n/**\n * The driver surface a database adapter implements — the ~7 methods that\n * actually differ between SQL stores. Everything above them (the\n * commit/rollback call sequences, the guards, identifier validation, the\n * checkpoint schema) is policy owned by {@link createSink}, so a driver\n * cannot get the ordering wrong: it never sees a cursor write outside the\n * transaction that carries the rows, and never sees a delete without the\n * rewound cursor riding along.\n *\n * Identifier args (`table`, `column`) arrive pre-validated against\n * `[A-Za-z_][A-Za-z0-9_]*` — safe to interpolate after quoting (see\n * {@link quoteIdent}). Values (cursor, height, id) must still be bound as\n * parameters.\n */\nexport interface SinkDriver<Tx> {\n\t/** Run `fn` inside ONE transaction: begin, run, commit — a throw from\n\t * `fn` must roll the whole transaction back and re-throw. */\n\ttransact<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;\n\t/** Create the checkpoint store if missing: `(id primary key, cursor)`.\n\t * Must be safe under concurrent first start (two consumers racing the\n\t * same CREATE). */\n\tensureCheckpointStore(): Promise<void>;\n\t/** The committed cursor for this sink's `id`, or `null` if none. */\n\treadCursor(): Promise<string | null>;\n\t/** Upsert this sink's cursor row inside `tx`. */\n\twriteCursor(tx: Tx, cursor: string): Promise<void>;\n\t/** Delete rows with `height column >= height` from `table`, inside `tx`. */\n\tdeleteAtOrAbove(tx: Tx, table: string, height: number): Promise<void>;\n\t/** Whether `table` exists AND carries `column` — the first-use check that\n\t * keeps reorg rollback from being a silent no-op. */\n\thasColumn(table: string, column: string): Promise<boolean>;\n\t/** Optional: assert this consumer is the only live writer for its `id`,\n\t * inside `tx`. Must FAIL LOUDLY (throw) when another writer holds it —\n\t * never block or interleave (contract invariant #13). */\n\tacquireLock?(tx: Tx): Promise<void>;\n}\n\n/** Options for {@link createSink} — the policy inputs, driver-agnostic.\n * Concrete sinks re-expose these with schema-typed `tables`/`height`. */\nexport interface CreateSinkOptions {\n\t/** Error-message prefix naming the concrete sink (e.g. `\"kyselySink\"`),\n\t * so a thrown guard points at the thing the user actually constructed. */\n\tlabel: string;\n\t/** Checkpoint identity AND concurrency key. */\n\tid: string;\n\t/** Rollback scope: on a reorg, rows at/above the fork point are deleted\n\t * from exactly these tables. */\n\ttables: readonly string[];\n\t/** The block-height stamp column, present on every declared table. */\n\theight: string;\n\t/** Checkpoint table name. Default {@link DEFAULT_CHECKPOINT_TABLE}. */\n\tcheckpointTable?: string;\n\t/** Forwarded to {@link ConsumerSink.capabilities}. */\n\tcapabilities?: ConsumerSink[\"capabilities\"];\n}\n\n/** Throw unless `name` is a bare SQL identifier (letters, digits,\n * underscores; no leading digit) — the guard that makes interpolating\n * table/column names into DDL and DELETE statements safe. */\nexport function assertSqlIdentifier(name: string, label: string): void {\n\tif (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n\t\tthrow new ValidationError(\n\t\t\t`${label}: invalid identifier \"${name}\" (letters, digits, underscores only).`,\n\t\t\t400,\n\t\t);\n\t}\n}\n\n/** Double-quote a (pre-validated) identifier for interpolation. */\nexport function quoteIdent(name: string): string {\n\treturn `\"${name}\"`;\n}\n\n/**\n * Build a {@link ConsumerSink} from a {@link SinkDriver}: the portable ~90\n * lines every SQL sink otherwise re-implements — and re-risks. Owns the two\n * transaction sequences (begin → lock → write rows → write cursor;\n * begin → lock → delete `>=` fork → write rewound cursor), the empty-`tables
|
|
5
|
+
"const DOCS_BASE = \"https://www.secondlayer.tools\";\n\n/** Options accepted by every error in the family. */\nexport interface SecondLayerErrorOptions {\n\tcause?: Error;\n\tdetails?: string;\n\t/** Stable machine-readable code (e.g. `UPGRADE_REQUIRED`). */\n\tcode?: string;\n\t/** Docs page that explains this failure and its fix. */\n\tdocsUrl?: string;\n\t/** Extra context lines appended to the message (DDL to run, next steps). */\n\tmetaMessages?: string[];\n\t/** Whether retrying the SAME request can succeed (429/5xx/network). */\n\tretryable?: boolean;\n\t/** Parsed `Retry-After`, in seconds — set on rate limits that carry it. */\n\tretryAfterSeconds?: number;\n}\n\n/**\n * Root of the SDK error family. Implements the same protocol as `BaseError`\n * in `@secondlayer/stacks` (`shortMessage`, `cause`, `toJSON`) so a failed\n * chain read and a failed platform read serialize and introspect identically.\n * It intentionally does NOT extend that class: cross-package `instanceof` is\n * unreliable under per-package bundling anyway (match on `code`/`name` across\n * packages).\n *\n * `retryable` is the signal the consume loops act on: 429/5xx/network are\n * retryable, 4xx and body-serialization failures are not.\n */\nexport class SecondLayerError extends Error {\n\toverride name = \"SecondLayerError\";\n\t/** The one-line failure, without the appended context blocks. */\n\tshortMessage: string;\n\tcode?: string;\n\tdocsUrl?: string;\n\tmetaMessages?: string[];\n\tretryable: boolean;\n\tretryAfterSeconds?: number;\n\n\tconstructor(shortMessage: string, options: SecondLayerErrorOptions = {}) {\n\t\tconst detailBlocks = [\n\t\t\t...(options.metaMessages ?? []),\n\t\t\t...(options.details ? [options.details] : []),\n\t\t\t...(options.docsUrl ? [`Docs: ${options.docsUrl}`] : []),\n\t\t];\n\t\tconst message = [\n\t\t\tshortMessage,\n\t\t\tdetailBlocks.length > 0 ? `\\n${detailBlocks.join(\"\\n\")}` : \"\",\n\t\t].join(\"\");\n\t\tsuper(message, options.cause ? { cause: options.cause } : undefined);\n\t\tthis.shortMessage = shortMessage;\n\t\tthis.code = options.code;\n\t\tthis.docsUrl = options.docsUrl;\n\t\tthis.metaMessages = options.metaMessages;\n\t\tthis.retryable = options.retryable ?? false;\n\t\tthis.retryAfterSeconds = options.retryAfterSeconds;\n\t}\n\n\t/**\n\t * Walk the cause chain. With a predicate, returns the first error matching\n\t * it (or `null`); without one, returns the deepest cause. Mirrors viem's\n\t * `error.walk()` so cross-library error handling reads the same.\n\t */\n\twalk(fn?: (err: unknown) => boolean): unknown {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: cause chains are untyped by nature\n\t\tlet err: any = this;\n\t\tlet last: unknown = this;\n\t\twhile (err != null) {\n\t\t\tif (fn?.(err)) return err;\n\t\t\tlast = err;\n\t\t\terr = err.cause;\n\t\t}\n\t\treturn fn ? null : last;\n\t}\n\n\ttoJSON(): {\n\t\tname: string;\n\t\tmessage: string;\n\t\tshortMessage: string;\n\t\tcause: string | undefined;\n\t\tcode: string | undefined;\n\t\tdocsUrl: string | undefined;\n\t\tretryable: boolean;\n\t\tretryAfterSeconds: number | undefined;\n\t} {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\tmessage: this.message,\n\t\t\tshortMessage: this.shortMessage,\n\t\t\tcause: this.cause instanceof Error ? this.cause.message : undefined,\n\t\t\tcode: this.code,\n\t\t\tdocsUrl: this.docsUrl,\n\t\t\tretryable: this.retryable,\n\t\t\tretryAfterSeconds: this.retryAfterSeconds,\n\t\t};\n\t}\n}\n\n/** Retryability of an HTTP status: 429 and 5xx are transient. */\nfunction statusRetryable(status: number): boolean {\n\treturn status === 429 || status >= 500;\n}\n\n/** Docs page for a status/code pair, when one exists. */\nfunction docsFor(status: number, code?: string): string | undefined {\n\tif (code === \"UPGRADE_REQUIRED\" || status === 402)\n\t\treturn `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;\n\tif (status === 429 || status === 401)\n\t\treturn `${DOCS_BASE}/docs/authentication`;\n\treturn undefined;\n}\n\n/**\n * Error thrown by {@link SecondLayer} when an API request fails.\n * Includes the HTTP status code for programmatic error handling.\n *\n * @example\n * ```ts\n * try {\n * await client.subgraphs.status(\"my-subgraph\");\n * } catch (err) {\n * if (err instanceof ApiError && err.status === 404) {\n * console.log(\"Subgraph not found\");\n * }\n * }\n * ```\n */\nexport class ApiError extends SecondLayerError {\n\tconstructor(\n\t\t/** HTTP status code (0 for network errors). */\n\t\tpublic status: number,\n\t\tmessage: string,\n\t\t/** Raw response body (parsed JSON if possible) — preserved for callers that need error details. */\n\t\tpublic body?: unknown,\n\t\t/** Stable machine-readable code from the API's `{error, code}` error envelope. */\n\t\tcode?: string,\n\t\toptions: SecondLayerErrorOptions = {},\n\t) {\n\t\tsuper(message, {\n\t\t\tcode,\n\t\t\tretryable: options.retryable ?? statusRetryable(status),\n\t\t\tdocsUrl: options.docsUrl ?? docsFor(status, code),\n\t\t\t...(options.cause ? { cause: options.cause } : {}),\n\t\t\t...(options.details ? { details: options.details } : {}),\n\t\t\t...(options.metaMessages ? { metaMessages: options.metaMessages } : {}),\n\t\t\t...(options.retryAfterSeconds !== undefined\n\t\t\t\t? { retryAfterSeconds: options.retryAfterSeconds }\n\t\t\t\t: {}),\n\t\t});\n\t\tthis.name = \"ApiError\";\n\t}\n}\n\n/** Thrown on a 401 by both the instance clients and Streams. Carries the\n * server's `{error, code}` envelope when it sent one, so a revoked token reads\n * differently from a missing one. */\nexport class AuthError extends ApiError {\n\toverride readonly status = 401 as const;\n\n\tconstructor(\n\t\tmessage = \"API key invalid or expired.\",\n\t\tbody?: unknown,\n\t\tcode?: string,\n\t) {\n\t\tsuper(401, message, body, code);\n\t\tthis.name = \"AuthError\";\n\t}\n}\n\n/** Thrown on a 429 by both the instance clients and Streams. `retryable`,\n * with `retryAfterSeconds` parsed from the `Retry-After` header when sent. */\nexport class RateLimitError extends ApiError {\n\toverride readonly status = 429 as const;\n\n\tconstructor(\n\t\tmessage = \"Rate limited. Try again later.\",\n\t\t/** Raw `Retry-After` header value (seconds or HTTP-date). */\n\t\treadonly retryAfter?: string,\n\t\tbody?: unknown,\n\t\tcode?: string,\n\t) {\n\t\tsuper(429, message, body, code, {\n\t\t\tretryable: true,\n\t\t\tretryAfterSeconds: parseRetryAfter(retryAfter),\n\t\t});\n\t\tthis.name = \"RateLimitError\";\n\t}\n}\n\n/** Thrown on a 4xx the caller can fix (bad cursor, bad params). Never retried.\n * `code` is the server's machine-readable code when the envelope carried one. */\nexport class ValidationError extends ApiError {\n\tconstructor(message: string, status: number, body?: unknown, code?: string) {\n\t\tsuper(status, message, body, code, { retryable: false });\n\t\tthis.name = \"ValidationError\";\n\t}\n}\n\n/** Parse a `Retry-After` header: delta-seconds or an HTTP-date. */\nexport function parseRetryAfter(value?: string | null): number | undefined {\n\tif (!value) return undefined;\n\tconst seconds = Number(value);\n\tif (Number.isFinite(seconds) && seconds >= 0) return seconds;\n\tconst date = Date.parse(value);\n\tif (!Number.isNaN(date)) return Math.max(0, (date - Date.now()) / 1000);\n\treturn undefined;\n}\n",
|
|
6
|
+
"import { ValidationError } from \"../errors.ts\";\nimport type { ConsumerSink } from \"./types.ts\";\n\n/** Default checkpoint table name, shared by every SQL-backed sink so two\n * sinks on the same database land in one table keyed by `id`. */\nexport const DEFAULT_CHECKPOINT_TABLE = \"sl_consumer_checkpoints\";\n\n/**\n * The driver surface a database adapter implements — the ~7 methods that\n * actually differ between SQL stores. Everything above them (the\n * commit/rollback call sequences, the guards, identifier validation, the\n * checkpoint schema) is policy owned by {@link createSink}, so a driver\n * cannot get the ordering wrong: it never sees a cursor write outside the\n * transaction that carries the rows, and never sees a delete without the\n * rewound cursor riding along.\n *\n * Identifier args (`table`, `column`) arrive pre-validated against\n * `[A-Za-z_][A-Za-z0-9_]*` — safe to interpolate after quoting (see\n * {@link quoteIdent}). Values (cursor, height, id) must still be bound as\n * parameters.\n */\nexport interface SinkDriver<Tx> {\n\t/** Run `fn` inside ONE transaction: begin, run, commit — a throw from\n\t * `fn` must roll the whole transaction back and re-throw. */\n\ttransact<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;\n\t/** Create the checkpoint store if missing: `(id primary key, cursor)`.\n\t * Must be safe under concurrent first start (two consumers racing the\n\t * same CREATE). */\n\tensureCheckpointStore(): Promise<void>;\n\t/** The committed cursor for this sink's `id`, or `null` if none. */\n\treadCursor(): Promise<string | null>;\n\t/** Upsert this sink's cursor row inside `tx`. */\n\twriteCursor(tx: Tx, cursor: string): Promise<void>;\n\t/** Delete this sink's cursor row inside `tx`: a rewind to pre-genesis\n\t * (fork at block 0). Optional for drivers written before it existed;\n\t * without it a genesis rewind throws instead of writing a bad cursor. */\n\tclearCursor?(tx: Tx): Promise<void>;\n\t/** Delete rows with `height column >= height` from `table`, inside `tx`. */\n\tdeleteAtOrAbove(tx: Tx, table: string, height: number): Promise<void>;\n\t/** Whether `table` exists AND carries `column` — the first-use check that\n\t * keeps reorg rollback from being a silent no-op. */\n\thasColumn(table: string, column: string): Promise<boolean>;\n\t/** Optional: assert this consumer is the only live writer for its `id`,\n\t * inside `tx`. Must FAIL LOUDLY (throw) when another writer holds it —\n\t * never block or interleave (contract invariant #13). */\n\tacquireLock?(tx: Tx): Promise<void>;\n}\n\n/** Context handed to {@link CreateSinkOptions.onRollback}. `forkPointHeight`\n * is the undo range (inclusive `>=`); `rewindCursor` is what the checkpoint\n * will become after this transaction — `null` only for a fork at genesis. */\nexport type SinkRollbackContext = {\n\tforkPointHeight: number;\n\trewindCursor: string | null;\n};\n\n/** Options for {@link createSink} — the policy inputs, driver-agnostic.\n * Concrete sinks re-expose these with schema-typed `tables`/`height`. */\nexport interface CreateSinkOptions<Tx = unknown> {\n\t/** Error-message prefix naming the concrete sink (e.g. `\"kyselySink\"`),\n\t * so a thrown guard points at the thing the user actually constructed. */\n\tlabel: string;\n\t/** Checkpoint identity AND concurrency key. */\n\tid: string;\n\t/** Rollback scope: on a reorg, rows at/above the fork point are deleted\n\t * from exactly these tables. Fact tables only — a fold (balances,\n\t * counters) does not belong here; invert it in `onRollback`. */\n\ttables: readonly string[];\n\t/** The block-height stamp column, present on every declared table. */\n\theight: string;\n\t/** Checkpoint table name. Default {@link DEFAULT_CHECKPOINT_TABLE}. */\n\tcheckpointTable?: string;\n\t/** Forwarded to {@link ConsumerSink.capabilities}. */\n\tcapabilities?: ConsumerSink[\"capabilities\"];\n\t/** Runs inside the rollback transaction after the lock, before any\n\t * `DELETE … height >= fork`. Doomed fact rows are still visible.\n\t * Remaining facts are `height < forkPointHeight`. A throw aborts the\n\t * whole rewind (no delete, no cursor write). Called on every\n\t * `rollback`, including re-application after a crash — derive undo\n\t * from the doomed rows so a second pass is a no-op. */\n\tonRollback?: (tx: Tx, ctx: SinkRollbackContext) => Promise<void> | void;\n}\n\n/** Throw unless `name` is a bare SQL identifier (letters, digits,\n * underscores; no leading digit) — the guard that makes interpolating\n * table/column names into DDL and DELETE statements safe. */\nexport function assertSqlIdentifier(name: string, label: string): void {\n\tif (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n\t\tthrow new ValidationError(\n\t\t\t`${label}: invalid identifier \"${name}\" (letters, digits, underscores only).`,\n\t\t\t400,\n\t\t);\n\t}\n}\n\n/** Double-quote a (pre-validated) identifier for interpolation. */\nexport function quoteIdent(name: string): string {\n\treturn `\"${name}\"`;\n}\n\n/**\n * Build a {@link ConsumerSink} from a {@link SinkDriver}: the portable ~90\n * lines every SQL sink otherwise re-implements — and re-risks. Owns the two\n * transaction sequences (begin → lock → write rows → write cursor;\n * begin → lock → onRollback? → delete `>=` fork → write rewound cursor), the\n * empty-`tables` guard, identifier validation, and the first-use height-column\n * check.\n *\n * A driver implemented against this base cannot violate contract invariants\n * #4, #5, #8, #9, or #11 (see {@link ConsumerSink}) without breaking\n * `transact` itself — which is exactly what the conformance kit probes.\n */\nexport function createSink<Tx>(\n\tdriver: SinkDriver<Tx>,\n\toptions: CreateSinkOptions<Tx>,\n): ConsumerSink<Tx> {\n\tconst { label } = options;\n\tconst checkpointTable = options.checkpointTable ?? DEFAULT_CHECKPOINT_TABLE;\n\tif (options.tables.length === 0) {\n\t\tthrow new ValidationError(\n\t\t\t`${label}: \\`tables\\` is empty — declare every table the handler writes, or reorg rollback is a no-op and orphaned rows persist forever.`,\n\t\t\t400,\n\t\t);\n\t}\n\tassertSqlIdentifier(checkpointTable, label);\n\tassertSqlIdentifier(options.height, label);\n\tfor (const table of options.tables) assertSqlIdentifier(table, label);\n\n\treturn {\n\t\tcapabilities: options.capabilities,\n\n\t\tasync loadCursor() {\n\t\t\tawait driver.ensureCheckpointStore();\n\t\t\t// First-use validation of the rollback precondition: every declared\n\t\t\t// table must exist and carry the height column. Failing here beats a\n\t\t\t// rollback that silently deletes nothing during a reorg.\n\t\t\tfor (const table of options.tables) {\n\t\t\t\tif (!(await driver.hasColumn(table, options.height))) {\n\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t`${label}: table \"${table}\" has no \"${options.height}\" column — the height stamp is what makes reorg rollback possible. Add the column (or fix \\`height\\`).`,\n\t\t\t\t\t\t400,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn driver.readCursor();\n\t\t},\n\n\t\tasync commitBatch(cursor, write) {\n\t\t\tawait driver.transact(async (tx) => {\n\t\t\t\tawait driver.acquireLock?.(tx);\n\t\t\t\tawait write(tx);\n\t\t\t\tawait driver.writeCursor(tx, cursor);\n\t\t\t});\n\t\t},\n\n\t\tasync rollback(forkPointHeight, rewindCursor) {\n\t\t\tawait driver.transact(async (tx) => {\n\t\t\t\tawait driver.acquireLock?.(tx);\n\t\t\t\t// Folds (balances, last-wins) invert/recompute here, while doomed\n\t\t\t\t// fact rows are still visible. A throw aborts the rewind.\n\t\t\t\tawait options.onRollback?.(tx, { forkPointHeight, rewindCursor });\n\t\t\t\t// INCLUSIVE of the fork block: the new canonical chain re-supplies\n\t\t\t\t// it, and the consumer rewinds to re-read from `fork:0`.\n\t\t\t\tfor (const table of options.tables) {\n\t\t\t\t\tawait driver.deleteAtOrAbove(tx, table, forkPointHeight);\n\t\t\t\t}\n\t\t\t\tif (rewindCursor === null) {\n\t\t\t\t\tif (!driver.clearCursor) {\n\t\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t\t`${label}: this driver has no clearCursor, so a rewind to pre-genesis cannot be committed. Add clearCursor to the driver, or resume with an explicit fromCursor.`,\n\t\t\t\t\t\t\t400,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tawait driver.clearCursor(tx);\n\t\t\t\t} else {\n\t\t\t\t\tawait driver.writeCursor(tx, rewindCursor);\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t};\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";AAAA,IAAM,YAAY;AAAA;AA6BX,MAAM,yBAAyB,MAAM;AAAA,EAClC,OAAO;AAAA,EAEhB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,WAAW,CAAC,cAAsB,UAAmC,CAAC,GAAG;AAAA,IACxE,MAAM,eAAe;AAAA,MACpB,GAAI,QAAQ,gBAAgB,CAAC;AAAA,MAC7B,GAAI,QAAQ,UAAU,CAAC,QAAQ,OAAO,IAAI,CAAC;AAAA,MAC3C,GAAI,QAAQ,UAAU,CAAC,SAAS,QAAQ,SAAS,IAAI,CAAC;AAAA,IACvD;AAAA,IACA,MAAM,UAAU;AAAA,MACf;AAAA,MACA,aAAa,SAAS,IAAI;AAAA,EAAK,aAAa,KAAK;AAAA,CAAI,MAAM;AAAA,IAC5D,EAAE,KAAK,EAAE;AAAA,IACT,MAAM,SAAS,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,SAAS;AAAA,IACnE,KAAK,eAAe;AAAA,IACpB,KAAK,OAAO,QAAQ;AAAA,IACpB,KAAK,UAAU,QAAQ;AAAA,IACvB,KAAK,eAAe,QAAQ;AAAA,IAC5B,KAAK,YAAY,QAAQ,aAAa;AAAA,IACtC,KAAK,oBAAoB,QAAQ;AAAA;AAAA,EAQlC,IAAI,CAAC,IAAyC;AAAA,IAE7C,IAAI,MAAW;AAAA,IACf,IAAI,OAAgB;AAAA,IACpB,OAAO,OAAO,MAAM;AAAA,MACnB,IAAI,KAAK,GAAG;AAAA,QAAG,OAAO;AAAA,MACtB,OAAO;AAAA,MACP,MAAM,IAAI;AAAA,IACX;AAAA,IACA,OAAO,KAAK,OAAO;AAAA;AAAA,EAGpB,MAAM,GASJ;AAAA,IACD,OAAO;AAAA,MACN,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,cAAc,KAAK;AAAA,MACnB,OAAO,KAAK,iBAAiB,QAAQ,KAAK,MAAM,UAAU;AAAA,MAC1D,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,WAAW,KAAK;AAAA,MAChB,mBAAmB,KAAK;AAAA,IACzB;AAAA;AAEF;AAGA,SAAS,eAAe,CAAC,QAAyB;AAAA,EACjD,OAAO,WAAW,OAAO,UAAU;AAAA;AAIpC,SAAS,OAAO,CAAC,QAAgB,MAAmC;AAAA,EACnE,IAAI,SAAS,sBAAsB,WAAW;AAAA,IAC7C,OAAO,GAAG;AAAA,EACX,IAAI,WAAW,OAAO,WAAW;AAAA,IAChC,OAAO,GAAG;AAAA,EACX;AAAA;AAAA;AAkBM,MAAM,iBAAiB,iBAAiB;AAAA,EAGtC;AAAA,EAGA;AAAA,EALR,WAAW,CAEH,QACP,SAEO,MAEP,MACA,UAAmC,CAAC,GACnC;AAAA,IACD,MAAM,SAAS;AAAA,MACd;AAAA,MACA,WAAW,QAAQ,aAAa,gBAAgB,MAAM;AAAA,MACtD,SAAS,QAAQ,WAAW,QAAQ,QAAQ,IAAI;AAAA,SAC5C,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,SAC5C,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,SAClD,QAAQ,eAAe,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;AAAA,SACjE,QAAQ,sBAAsB,YAC/B,EAAE,mBAAmB,QAAQ,kBAAkB,IAC/C,CAAC;AAAA,IACL,CAAC;AAAA,IAlBM;AAAA,IAGA;AAAA,IAgBP,KAAK,OAAO;AAAA;AAEd;AAAA;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "849F49AD7AB025DA64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/sinks/drizzle.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Table } from "drizzle-orm";
|
|
2
1
|
/**
|
|
3
2
|
* A consumer sink: the destination adapter a consume loop writes through.
|
|
4
3
|
*
|
|
@@ -115,9 +114,20 @@ interface ConsumerSink<Tx = unknown> {
|
|
|
115
114
|
* same `rewindCursor` (the lowest fork point) but its own
|
|
116
115
|
* `forkPointHeight` — scope the undo by `forkPointHeight` only (contract
|
|
117
116
|
* invariant #11), and expect re-application after a crash (#10).
|
|
117
|
+
*
|
|
118
|
+
* `rewindCursor` is `null` only for a fork at genesis: clear the
|
|
119
|
+
* checkpoint so the next run starts from the first event.
|
|
118
120
|
*/
|
|
119
|
-
rollback(forkPointHeight: number, rewindCursor: string): Promise<void>;
|
|
121
|
+
rollback(forkPointHeight: number, rewindCursor: string | null): Promise<void>;
|
|
120
122
|
}
|
|
123
|
+
/** Context handed to {@link CreateSinkOptions.onRollback}. `forkPointHeight`
|
|
124
|
+
* is the undo range (inclusive `>=`); `rewindCursor` is what the checkpoint
|
|
125
|
+
* will become after this transaction — `null` only for a fork at genesis. */
|
|
126
|
+
type SinkRollbackContext = {
|
|
127
|
+
forkPointHeight: number;
|
|
128
|
+
rewindCursor: string | null;
|
|
129
|
+
};
|
|
130
|
+
import { Table } from "drizzle-orm";
|
|
121
131
|
/**
|
|
122
132
|
* Structural slice of a drizzle database. Postgres databases expose
|
|
123
133
|
* `execute`; sqlite databases expose `run`/`all` (sync drivers return
|
|
@@ -138,10 +148,14 @@ type DrizzleTx<TDb> = TDb extends {
|
|
|
138
148
|
* maps is the intersection, so a key missing from any one table is a
|
|
139
149
|
* compile error at the `height:` option (same trick as kyselySink). */
|
|
140
150
|
type CommonColumnKey<T extends Table> = keyof T["_"]["columns"] & string;
|
|
141
|
-
interface DrizzleSinkOptions<
|
|
151
|
+
interface DrizzleSinkOptions<
|
|
152
|
+
T extends Table,
|
|
153
|
+
TTx = unknown
|
|
154
|
+
> {
|
|
142
155
|
/** Checkpoint identity AND (on Postgres) advisory-lock key. */
|
|
143
156
|
id: string;
|
|
144
|
-
/** Rollback scope: the drizzle table objects the handler writes.
|
|
157
|
+
/** Rollback scope: the drizzle table objects the handler writes. Fact
|
|
158
|
+
* tables only — invert folds (balances) in `onRollback`. */
|
|
145
159
|
tables: readonly T[];
|
|
146
160
|
/** The block-height stamp column KEY (the TypeScript property, e.g.
|
|
147
161
|
* `blockHeight` for `blockHeight: integer("block_height")`) — resolved
|
|
@@ -150,6 +164,9 @@ interface DrizzleSinkOptions<T extends Table> {
|
|
|
150
164
|
height: CommonColumnKey<T>;
|
|
151
165
|
/** Checkpoint table name. Default `sl_consumer_checkpoints`. */
|
|
152
166
|
checkpointTable?: string;
|
|
167
|
+
/** Same transaction as the fact-table delete, before it. Doomed rows are
|
|
168
|
+
* still visible; a throw aborts the rewind. */
|
|
169
|
+
onRollback?: (tx: TTx, ctx: SinkRollbackContext) => Promise<void> | void;
|
|
153
170
|
}
|
|
154
171
|
/**
|
|
155
172
|
* A {@link ConsumerSink} over a drizzle database — Postgres or SQLite
|
|
@@ -180,5 +197,5 @@ interface DrizzleSinkOptions<T extends Table> {
|
|
|
180
197
|
declare function drizzleSink<
|
|
181
198
|
TDb extends DrizzleDatabaseLike,
|
|
182
199
|
T extends Table
|
|
183
|
-
>(db: TDb, options: DrizzleSinkOptions<T
|
|
184
|
-
export { drizzleSink, DrizzleTx, DrizzleSinkOptions };
|
|
200
|
+
>(db: TDb, options: DrizzleSinkOptions<T, DrizzleTx<TDb>>): ConsumerSink<DrizzleTx<TDb>>;
|
|
201
|
+
export { drizzleSink, SinkRollbackContext, DrizzleTx, DrizzleSinkOptions };
|