@secondlayer/sdk 6.37.0 → 6.38.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/dist/index.d.ts +184 -24
- package/dist/index.js +265 -78
- package/dist/index.js.map +11 -9
- package/dist/sinks/kysely.d.ts +108 -0
- package/dist/sinks/kysely.js +236 -0
- package/dist/sinks/kysely.js.map +11 -0
- package/dist/streams/index.d.ts +188 -6
- package/dist/streams/index.js +199 -71
- package/dist/streams/index.js.map +8 -7
- package/dist/subgraphs/index.d.ts +124 -21
- package/dist/subgraphs/index.js +222 -16
- package/dist/subgraphs/index.js.map +9 -7
- package/package.json +20 -7
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Kysely, Transaction } from "kysely";
|
|
2
|
+
/**
|
|
3
|
+
* A consumer sink: the destination adapter a consume loop writes through.
|
|
4
|
+
*
|
|
5
|
+
* Without a sink, the user owns the three hard parts of a durable indexer —
|
|
6
|
+
* checkpoint persistence, rows+cursor atomicity, and reorg rollback — and
|
|
7
|
+
* the reasoning that makes them safe lives in doc comments. A sink owns all
|
|
8
|
+
* three: the loop loads its committed cursor, hands the handler a
|
|
9
|
+
* transaction, commits rows AND cursor atomically, and rolls back reorgs
|
|
10
|
+
* without any user code. Crucially, a sink also closes the silent-skip
|
|
11
|
+
* hazard where omitting `onReorg` meant reorgs were ignored forever: with a
|
|
12
|
+
* sink attached, rollback is unconditional.
|
|
13
|
+
*
|
|
14
|
+
* The interface is dependency-free on purpose: implementations with real
|
|
15
|
+
* database drivers live behind subpath exports (`@secondlayer/sdk/sinks/*`)
|
|
16
|
+
* so the root entry ships no DB dependency.
|
|
17
|
+
*/
|
|
18
|
+
interface ConsumerSink<Tx = unknown> {
|
|
19
|
+
/** Phantom marker carrying `Tx` in a directly-inferable position, so the
|
|
20
|
+
* consume loops' `TTx` type parameter resolves from the `sink` option
|
|
21
|
+
* (never set at runtime). */
|
|
22
|
+
readonly _tx?: Tx;
|
|
23
|
+
/** The committed checkpoint, or `null` on first run. Called once, before
|
|
24
|
+
* the first page. Implementations may create their checkpoint storage
|
|
25
|
+
* here and SHOULD validate their rollback preconditions (e.g. that every
|
|
26
|
+
* declared table carries the height column). */
|
|
27
|
+
loadCursor(): Promise<string | null>;
|
|
28
|
+
/**
|
|
29
|
+
* Apply one batch atomically: open a transaction, run `write(tx)` (the
|
|
30
|
+
* user's inserts), and commit the rows AND `cursor` together. A throw
|
|
31
|
+
* from `write` must abort the whole transaction — leaving neither rows
|
|
32
|
+
* nor cursor, so a crashed batch is simply re-read on restart.
|
|
33
|
+
*/
|
|
34
|
+
commitBatch(cursor: string, write: (tx: Tx) => Promise<void> | void): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Roll the projection back to the fork: delete everything AT OR ABOVE
|
|
37
|
+
* `forkPointHeight` (inclusive `>=` — the new chain re-supplies the fork
|
|
38
|
+
* block) and commit `rewindCursor` in the SAME transaction. Deleting
|
|
39
|
+
* without the rewound cursor is the classic silent-gap bug: a crash
|
|
40
|
+
* between the two writes resumes above the fork and the deleted range is
|
|
41
|
+
* never re-read.
|
|
42
|
+
*/
|
|
43
|
+
rollback(forkPointHeight: number, rewindCursor: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Column present on EVERY declared table — `keyof` a union of row types is
|
|
47
|
+
* the intersection of their keys, so a column missing from any one table is
|
|
48
|
+
* a compile error at the `height:` option.
|
|
49
|
+
*/
|
|
50
|
+
type CommonColumn<
|
|
51
|
+
DB,
|
|
52
|
+
T extends keyof DB
|
|
53
|
+
> = keyof DB[T] & string;
|
|
54
|
+
interface KyselySinkOptions<
|
|
55
|
+
DB,
|
|
56
|
+
T extends keyof DB & string
|
|
57
|
+
> {
|
|
58
|
+
/** Checkpoint identity AND concurrency key: the cursor row's primary key,
|
|
59
|
+
* and the input to `pg_try_advisory_xact_lock(hashtext(id))` — a second
|
|
60
|
+
* consumer with the same id fails loudly instead of double-writing. */
|
|
61
|
+
id: string;
|
|
62
|
+
/** Rollback scope. On a reorg, rows AT OR ABOVE the fork point are deleted
|
|
63
|
+
* from exactly these tables (inclusive `>=` — the new chain re-supplies
|
|
64
|
+
* the fork block). A table you write but don't declare keeps orphaned
|
|
65
|
+
* rows forever, so declare everything the handler touches. */
|
|
66
|
+
tables: readonly T[];
|
|
67
|
+
/** The block-height stamp column, present on every declared table (compile
|
|
68
|
+
* and first-use checked). Height-stamp rollback is correct for
|
|
69
|
+
* APPEND-ONLY projections (one row per event/call); aggregates that
|
|
70
|
+
* mutate rows (balances, counters) can't be rolled back this way — use a
|
|
71
|
+
* subgraph for those. */
|
|
72
|
+
height: CommonColumn<DB, T>;
|
|
73
|
+
/** Checkpoint table name. Default `sl_consumer_checkpoints` (created on
|
|
74
|
+
* first use). */
|
|
75
|
+
checkpointTable?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A {@link ConsumerSink} over a Kysely Postgres database — keyed on the
|
|
79
|
+
* QUERY BUILDER (not the driver) so the `DB` schema generic flows through:
|
|
80
|
+
* `tables` is `(keyof DB)[]`, `height` must exist on every declared table,
|
|
81
|
+
* and `ctx.tx` inside `onBatch` is a fully typed `Transaction<DB>`.
|
|
82
|
+
*
|
|
83
|
+
* What it owns (so your indexer doesn't):
|
|
84
|
+
* - the checkpoint table + cursor load on start;
|
|
85
|
+
* - rows AND cursor committed in ONE transaction per batch — a handler
|
|
86
|
+
* throw aborts both, so a crashed batch is re-read on restart with no
|
|
87
|
+
* gaps and no double-writes;
|
|
88
|
+
* - reorg rollback: delete `>= fork_point_height` from every declared table
|
|
89
|
+
* and commit the rewound cursor atomically (the crash-between-the-two-
|
|
90
|
+
* writes gap is unrepresentable);
|
|
91
|
+
* - a per-id advisory lock, so two replicas of the same consumer can't
|
|
92
|
+
* interleave commits.
|
|
93
|
+
*
|
|
94
|
+
* ```ts
|
|
95
|
+
* const sink = kyselySink(db, { id: "sales", tables: ["sales"], height: "block_height" });
|
|
96
|
+
* await index.contractCalls.consume({
|
|
97
|
+
* contractId: MARKETPLACE, functionName: "purchase-asset", fromHeight: 0,
|
|
98
|
+
* sink,
|
|
99
|
+
* onBatch: (calls, _env, ctx) =>
|
|
100
|
+
* ctx.tx.insertInto("sales").values(calls.map(toSale)).execute(),
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare function kyselySink<
|
|
105
|
+
DB,
|
|
106
|
+
T extends keyof DB & string
|
|
107
|
+
>(db: Kysely<DB>, options: KyselySinkOptions<DB, T>): ConsumerSink<Transaction<DB>>;
|
|
108
|
+
export { kyselySink, KyselySinkOptions };
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// src/sinks/kysely.ts
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var DOCS_BASE = "https://www.secondlayer.tools";
|
|
6
|
+
|
|
7
|
+
class SecondLayerError extends Error {
|
|
8
|
+
name = "SecondLayerError";
|
|
9
|
+
shortMessage;
|
|
10
|
+
code;
|
|
11
|
+
docsUrl;
|
|
12
|
+
metaMessages;
|
|
13
|
+
retryable;
|
|
14
|
+
retryAfterSeconds;
|
|
15
|
+
constructor(shortMessage, options = {}) {
|
|
16
|
+
const detailBlocks = [
|
|
17
|
+
...options.metaMessages ?? [],
|
|
18
|
+
...options.details ? [options.details] : [],
|
|
19
|
+
...options.docsUrl ? [`Docs: ${options.docsUrl}`] : []
|
|
20
|
+
];
|
|
21
|
+
const message = [
|
|
22
|
+
shortMessage,
|
|
23
|
+
detailBlocks.length > 0 ? `
|
|
24
|
+
${detailBlocks.join(`
|
|
25
|
+
`)}` : ""
|
|
26
|
+
].join("");
|
|
27
|
+
super(message, options.cause ? { cause: options.cause } : undefined);
|
|
28
|
+
this.shortMessage = shortMessage;
|
|
29
|
+
this.code = options.code;
|
|
30
|
+
this.docsUrl = options.docsUrl;
|
|
31
|
+
this.metaMessages = options.metaMessages;
|
|
32
|
+
this.retryable = options.retryable ?? false;
|
|
33
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
34
|
+
}
|
|
35
|
+
walk(fn) {
|
|
36
|
+
let err = this;
|
|
37
|
+
let last = this;
|
|
38
|
+
while (err != null) {
|
|
39
|
+
if (fn?.(err))
|
|
40
|
+
return err;
|
|
41
|
+
last = err;
|
|
42
|
+
err = err.cause;
|
|
43
|
+
}
|
|
44
|
+
return fn ? null : last;
|
|
45
|
+
}
|
|
46
|
+
toJSON() {
|
|
47
|
+
return {
|
|
48
|
+
name: this.name,
|
|
49
|
+
message: this.message,
|
|
50
|
+
shortMessage: this.shortMessage,
|
|
51
|
+
cause: this.cause instanceof Error ? this.cause.message : undefined,
|
|
52
|
+
code: this.code,
|
|
53
|
+
docsUrl: this.docsUrl,
|
|
54
|
+
retryable: this.retryable,
|
|
55
|
+
retryAfterSeconds: this.retryAfterSeconds
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function statusRetryable(status) {
|
|
60
|
+
return status === 429 || status >= 500;
|
|
61
|
+
}
|
|
62
|
+
function docsFor(status, code) {
|
|
63
|
+
if (code === "UPGRADE_REQUIRED" || code === "PLAN_REQUIRED" || status === 402)
|
|
64
|
+
return `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;
|
|
65
|
+
if (status === 429 || status === 401)
|
|
66
|
+
return `${DOCS_BASE}/docs/authentication`;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class ApiError extends SecondLayerError {
|
|
71
|
+
status;
|
|
72
|
+
body;
|
|
73
|
+
constructor(status, message, body, code, options = {}) {
|
|
74
|
+
super(message, {
|
|
75
|
+
code,
|
|
76
|
+
retryable: options.retryable ?? statusRetryable(status),
|
|
77
|
+
docsUrl: options.docsUrl ?? docsFor(status, code),
|
|
78
|
+
...options.cause ? { cause: options.cause } : {},
|
|
79
|
+
...options.details ? { details: options.details } : {},
|
|
80
|
+
...options.metaMessages ? { metaMessages: options.metaMessages } : {},
|
|
81
|
+
...options.retryAfterSeconds !== undefined ? { retryAfterSeconds: options.retryAfterSeconds } : {}
|
|
82
|
+
});
|
|
83
|
+
this.status = status;
|
|
84
|
+
this.body = body;
|
|
85
|
+
this.name = "ApiError";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class AuthError extends ApiError {
|
|
90
|
+
status = 401;
|
|
91
|
+
constructor(message = "API key invalid or expired.") {
|
|
92
|
+
super(401, message);
|
|
93
|
+
this.name = "AuthError";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class RateLimitError extends ApiError {
|
|
98
|
+
retryAfter;
|
|
99
|
+
status = 429;
|
|
100
|
+
constructor(message = "Rate limited. Try again later.", retryAfter) {
|
|
101
|
+
super(429, message, undefined, undefined, {
|
|
102
|
+
retryable: true,
|
|
103
|
+
retryAfterSeconds: parseRetryAfter(retryAfter)
|
|
104
|
+
});
|
|
105
|
+
this.retryAfter = retryAfter;
|
|
106
|
+
this.name = "RateLimitError";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class ValidationError extends ApiError {
|
|
111
|
+
constructor(message, status, body) {
|
|
112
|
+
super(status, message, body, undefined, { retryable: false });
|
|
113
|
+
this.name = "ValidationError";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function parseRetryAfter(value) {
|
|
117
|
+
if (!value)
|
|
118
|
+
return;
|
|
119
|
+
const seconds = Number(value);
|
|
120
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
121
|
+
return seconds;
|
|
122
|
+
const date = Date.parse(value);
|
|
123
|
+
if (!Number.isNaN(date))
|
|
124
|
+
return Math.max(0, (date - Date.now()) / 1000);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
class VersionConflictError extends ApiError {
|
|
129
|
+
currentVersion;
|
|
130
|
+
expectedVersion;
|
|
131
|
+
constructor(currentVersion, expectedVersion, message = `Version conflict: expected ${expectedVersion}, current ${currentVersion}`) {
|
|
132
|
+
super(409, message, { currentVersion, expectedVersion });
|
|
133
|
+
this.currentVersion = currentVersion;
|
|
134
|
+
this.expectedVersion = expectedVersion;
|
|
135
|
+
this.name = "VersionConflictError";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class ByoBreakingChangeError extends ApiError {
|
|
140
|
+
details;
|
|
141
|
+
constructor(message, details) {
|
|
142
|
+
super(422, message, details, "BYO_BREAKING_CHANGE", {
|
|
143
|
+
metaMessages: [
|
|
144
|
+
"Review and run manually on your database:",
|
|
145
|
+
details.plan.dropStatement,
|
|
146
|
+
...details.plan.statements
|
|
147
|
+
]
|
|
148
|
+
});
|
|
149
|
+
this.name = "ByoBreakingChangeError";
|
|
150
|
+
this.details = details;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function isByoBreakingDetails(x) {
|
|
154
|
+
if (!x || typeof x !== "object")
|
|
155
|
+
return false;
|
|
156
|
+
const d = x;
|
|
157
|
+
const plan = d.plan;
|
|
158
|
+
return Array.isArray(d.reasons) && !!plan && typeof plan === "object" && typeof plan.dropStatement === "string";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/sinks/kysely.ts
|
|
162
|
+
function kyselySink(db, options) {
|
|
163
|
+
const checkpointTable = options.checkpointTable ?? "sl_consumer_checkpoints";
|
|
164
|
+
if (options.tables.length === 0) {
|
|
165
|
+
throw new ValidationError("kyselySink: `tables` is empty — declare every table the handler writes, or reorg rollback is a no-op and orphaned rows persist forever.", 400);
|
|
166
|
+
}
|
|
167
|
+
const ident = (name) => {
|
|
168
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
169
|
+
throw new ValidationError(`kyselySink: invalid identifier "${name}" (letters, digits, underscores only).`, 400);
|
|
170
|
+
}
|
|
171
|
+
return `"${name}"`;
|
|
172
|
+
};
|
|
173
|
+
const cp = ident(checkpointTable);
|
|
174
|
+
const heightCol = ident(options.height);
|
|
175
|
+
for (const table of options.tables)
|
|
176
|
+
ident(table);
|
|
177
|
+
async function acquireLock(tx) {
|
|
178
|
+
const locked = await sql`
|
|
179
|
+
SELECT pg_try_advisory_xact_lock(hashtext(${options.id})) AS "ok"
|
|
180
|
+
`.execute(tx);
|
|
181
|
+
if (!locked.rows[0]?.ok) {
|
|
182
|
+
throw new ValidationError(`kyselySink: another consumer holds the "${options.id}" lock — two replicas of the same consumer would interleave commits. Stop the other instance or use a distinct id.`, 409);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function upsertCursor(tx, cursor) {
|
|
186
|
+
await sql`
|
|
187
|
+
INSERT INTO ${sql.raw(cp)} (id, cursor) VALUES (${options.id}, ${cursor})
|
|
188
|
+
ON CONFLICT (id) DO UPDATE SET cursor = EXCLUDED.cursor
|
|
189
|
+
`.execute(tx);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
async loadCursor() {
|
|
193
|
+
await sql.raw(`CREATE TABLE IF NOT EXISTS ${cp} (id text PRIMARY KEY, cursor text NOT NULL)`).execute(db);
|
|
194
|
+
for (const table of options.tables) {
|
|
195
|
+
const col = await sql`
|
|
196
|
+
SELECT EXISTS (
|
|
197
|
+
SELECT 1 FROM information_schema.columns
|
|
198
|
+
WHERE table_name = ${table} AND column_name = ${options.height}
|
|
199
|
+
) AS "ok"
|
|
200
|
+
`.execute(db);
|
|
201
|
+
if (!col.rows[0]?.ok) {
|
|
202
|
+
throw new ValidationError(`kyselySink: table "${table}" has no "${String(options.height)}" column — the height stamp is what makes reorg rollback possible. Add the column (or fix \`height\`).`, 400);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const row = await sql`
|
|
206
|
+
SELECT cursor FROM ${sql.raw(cp)} WHERE id = ${options.id}
|
|
207
|
+
`.execute(db);
|
|
208
|
+
return row.rows[0]?.cursor ?? null;
|
|
209
|
+
},
|
|
210
|
+
async commitBatch(cursor, write) {
|
|
211
|
+
await db.transaction().execute(async (tx) => {
|
|
212
|
+
await acquireLock(tx);
|
|
213
|
+
await write(tx);
|
|
214
|
+
await upsertCursor(tx, cursor);
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
async rollback(forkPointHeight, rewindCursor) {
|
|
218
|
+
await db.transaction().execute(async (tx) => {
|
|
219
|
+
await acquireLock(tx);
|
|
220
|
+
for (const table of options.tables) {
|
|
221
|
+
await sql`
|
|
222
|
+
DELETE FROM ${sql.raw(ident(table))}
|
|
223
|
+
WHERE ${sql.raw(heightCol)} >= ${forkPointHeight}
|
|
224
|
+
`.execute(tx);
|
|
225
|
+
}
|
|
226
|
+
await upsertCursor(tx, rewindCursor);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
export {
|
|
232
|
+
kyselySink
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
//# debugId=667BFBBF3BF09CCB64756E2164756E21
|
|
236
|
+
//# sourceMappingURL=kysely.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/sinks/kysely.ts", "../src/errors.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { type Kysely, type Transaction, sql } from \"kysely\";\nimport { ValidationError } from \"../errors.ts\";\nimport type { ConsumerSink } from \"./types.ts\";\n\n/**\n * Column present on EVERY declared table — `keyof` a union of row types is\n * the intersection of their keys, so a column missing from any one table is\n * a compile error at the `height:` option.\n */\ntype CommonColumn<DB, T extends keyof DB> = keyof DB[T] & string;\n\nexport interface KyselySinkOptions<DB, T extends keyof DB & string> {\n\t/** Checkpoint identity AND concurrency key: the cursor row's primary key,\n\t * and the input to `pg_try_advisory_xact_lock(hashtext(id))` — a second\n\t * consumer with the same id fails loudly instead of double-writing. */\n\tid: string;\n\t/** Rollback scope. On a reorg, rows AT OR ABOVE the fork point are deleted\n\t * from exactly these tables (inclusive `>=` — the new chain re-supplies\n\t * the fork block). A table you write but don't declare keeps orphaned\n\t * rows forever, so declare everything the handler touches. */\n\ttables: readonly T[];\n\t/** The block-height stamp column, present on every declared table (compile\n\t * and first-use checked). Height-stamp rollback is correct for\n\t * APPEND-ONLY projections (one row per event/call); aggregates that\n\t * mutate rows (balances, counters) can't be rolled back this way — use a\n\t * subgraph for those. */\n\theight: CommonColumn<DB, T>;\n\t/** Checkpoint table name. Default `sl_consumer_checkpoints` (created on\n\t * first use). */\n\tcheckpointTable?: string;\n}\n\n/**\n * A {@link ConsumerSink} over a Kysely Postgres database — keyed on the\n * QUERY BUILDER (not the driver) so the `DB` schema generic flows through:\n * `tables` is `(keyof DB)[]`, `height` must exist on every declared table,\n * and `ctx.tx` inside `onBatch` is a fully typed `Transaction<DB>`.\n *\n * What it owns (so your indexer doesn't):\n * - the checkpoint table + cursor load on start;\n * - rows AND cursor committed in ONE transaction per batch — a handler\n * throw aborts both, so a crashed batch is re-read on restart with no\n * gaps and no double-writes;\n * - reorg rollback: delete `>= fork_point_height` from every declared table\n * and commit the rewound cursor atomically (the crash-between-the-two-\n * writes gap is unrepresentable);\n * - a per-id advisory lock, so two replicas of the same consumer can't\n * interleave commits.\n *\n * ```ts\n * const sink = kyselySink(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 * ctx.tx.insertInto(\"sales\").values(calls.map(toSale)).execute(),\n * });\n * ```\n */\nexport function kyselySink<DB, T extends keyof DB & string>(\n\tdb: Kysely<DB>,\n\toptions: KyselySinkOptions<DB, T>,\n): ConsumerSink<Transaction<DB>> {\n\tconst checkpointTable = options.checkpointTable ?? \"sl_consumer_checkpoints\";\n\tif (options.tables.length === 0) {\n\t\tthrow new ValidationError(\n\t\t\t\"kyselySink: `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\n\t/** `sql.raw`-safe identifier (we interpolate table/column names). */\n\tconst ident = (name: string): string => {\n\t\tif (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n\t\t\tthrow new ValidationError(\n\t\t\t\t`kyselySink: invalid identifier \"${name}\" (letters, digits, underscores only).`,\n\t\t\t\t400,\n\t\t\t);\n\t\t}\n\t\treturn `\"${name}\"`;\n\t};\n\tconst cp = ident(checkpointTable);\n\tconst heightCol = ident(options.height as string);\n\tfor (const table of options.tables) ident(table);\n\n\tasync function acquireLock(tx: Transaction<DB>): Promise<void> {\n\t\tconst locked = await sql<{ ok: boolean }>`\n\t\t\tSELECT pg_try_advisory_xact_lock(hashtext(${options.id})) AS \"ok\"\n\t\t`.execute(tx);\n\t\tif (!locked.rows[0]?.ok) {\n\t\t\tthrow new ValidationError(\n\t\t\t\t`kyselySink: another consumer holds the \"${options.id}\" lock — two replicas of the same consumer would interleave commits. Stop the other instance or use a distinct id.`,\n\t\t\t\t409,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync function upsertCursor(\n\t\ttx: Transaction<DB>,\n\t\tcursor: string,\n\t): Promise<void> {\n\t\t// Identifiers via validated sql.raw; VALUES via bound parameters.\n\t\tawait sql`\n\t\t\tINSERT INTO ${sql.raw(cp)} (id, cursor) VALUES (${options.id}, ${cursor})\n\t\t\tON CONFLICT (id) DO UPDATE SET cursor = EXCLUDED.cursor\n\t\t`.execute(tx);\n\t}\n\n\treturn {\n\t\tasync loadCursor() {\n\t\t\tawait sql\n\t\t\t\t.raw(\n\t\t\t\t\t`CREATE TABLE IF NOT EXISTS ${cp} (id text PRIMARY KEY, cursor text NOT NULL)`,\n\t\t\t\t)\n\t\t\t\t.execute(db);\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\tconst col = await sql<{ ok: boolean }>`\n\t\t\t\t\tSELECT EXISTS (\n\t\t\t\t\t\tSELECT 1 FROM information_schema.columns\n\t\t\t\t\t\tWHERE table_name = ${table} AND column_name = ${options.height as string}\n\t\t\t\t\t) AS \"ok\"\n\t\t\t\t`.execute(db);\n\t\t\t\tif (!col.rows[0]?.ok) {\n\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t`kyselySink: table \"${table}\" has no \"${String(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\tconst row = await sql<{ cursor: string }>`\n\t\t\t\tSELECT cursor FROM ${sql.raw(cp)} WHERE id = ${options.id}\n\t\t\t`.execute(db);\n\t\t\treturn row.rows[0]?.cursor ?? null;\n\t\t},\n\n\t\tasync commitBatch(cursor, write) {\n\t\t\tawait db.transaction().execute(async (tx) => {\n\t\t\t\tawait acquireLock(tx);\n\t\t\t\tawait write(tx);\n\t\t\t\tawait upsertCursor(tx, cursor);\n\t\t\t});\n\t\t},\n\n\t\tasync rollback(forkPointHeight, rewindCursor) {\n\t\t\tawait db.transaction().execute(async (tx) => {\n\t\t\t\tawait acquireLock(tx);\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\t// Identifiers via validated sql.raw; the height bound as a parameter.\n\t\t\t\t\tawait sql`\n\t\t\t\t\t\tDELETE FROM ${sql.raw(ident(table))}\n\t\t\t\t\t\tWHERE ${sql.raw(heightCol)} >= ${forkPointHeight}\n\t\t\t\t\t`.execute(tx);\n\t\t\t\t}\n\t\t\t\tawait upsertCursor(tx, rewindCursor);\n\t\t\t});\n\t\t},\n\t};\n}\n",
|
|
6
|
+
"import type { ByoBreakingChangeDetails } from \"@secondlayer/shared/errors\";\n\nexport type { ByoBreakingChangeDetails };\n\nconst 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`, `BYO_BREAKING_CHANGE`). */\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: `BaseError.details` is a\n * `string`, while {@link ByoBreakingChangeError.details} is the documented\n * structured migration plan — and cross-package `instanceof` is unreliable\n * under per-package bundling anyway (match on `code`/`name` across 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\" || code === \"PLAN_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.get(\"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 platform clients and Streams. */\nexport class AuthError extends ApiError {\n\toverride readonly status = 401 as const;\n\n\tconstructor(message = \"API key invalid or expired.\") {\n\t\tsuper(401, message);\n\t\tthis.name = \"AuthError\";\n\t}\n}\n\n/** Thrown on a 429 — by both the platform 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) {\n\t\tsuper(429, message, undefined, undefined, {\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. */\nexport class ValidationError extends ApiError {\n\tconstructor(message: string, status: number, body?: unknown) {\n\t\tsuper(status, message, body, undefined, { 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\n/**\n * Thrown on optimistic-concurrency conflict when a deploy supplies an\n * `expectedVersion` that no longer matches the server's stored version.\n */\nexport class VersionConflictError extends ApiError {\n\tconstructor(\n\t\tpublic currentVersion: string,\n\t\tpublic expectedVersion: string,\n\t\tmessage = `Version conflict: expected ${expectedVersion}, current ${currentVersion}`,\n\t) {\n\t\tsuper(409, message, { currentVersion, expectedVersion });\n\t\tthis.name = \"VersionConflictError\";\n\t}\n}\n\n/**\n * Thrown when a BYO subgraph deploy is refused for a breaking schema change.\n * The deploy did NOT run — `details.plan` carries the DROP + rebuild DDL to run\n * manually on your own database, plus the breaking `reasons` and the `diff`.\n * The same DDL is surfaced on `metaMessages`, so a bare `console.error(err)`\n * already shows the operator what to run.\n *\n * @example\n * ```ts\n * try {\n * await client.subgraphs.deploy(bundle);\n * } catch (err) {\n * if (err instanceof ByoBreakingChangeError) {\n * console.log(err.details.plan.dropStatement);\n * console.log(err.details.plan.statements.join(\";\\n\"));\n * }\n * }\n * ```\n */\nexport class ByoBreakingChangeError extends ApiError {\n\treadonly details: ByoBreakingChangeDetails;\n\tconstructor(message: string, details: ByoBreakingChangeDetails) {\n\t\tsuper(422, message, details, \"BYO_BREAKING_CHANGE\", {\n\t\t\tmetaMessages: [\n\t\t\t\t\"Review and run manually on your database:\",\n\t\t\t\tdetails.plan.dropStatement,\n\t\t\t\t...details.plan.statements,\n\t\t\t],\n\t\t});\n\t\tthis.name = \"ByoBreakingChangeError\";\n\t\tthis.details = details;\n\t}\n}\n\n/** Narrow an unknown error body's `details` to {@link ByoBreakingChangeDetails}. */\nexport function isByoBreakingDetails(\n\tx: unknown,\n): x is ByoBreakingChangeDetails {\n\tif (!x || typeof x !== \"object\") return false;\n\tconst d = x as Record<string, unknown>;\n\tconst plan = d.plan as Record<string, unknown> | undefined;\n\treturn (\n\t\tArray.isArray(d.reasons) &&\n\t\t!!plan &&\n\t\ttypeof plan === \"object\" &&\n\t\ttypeof plan.dropStatement === \"string\"\n\t);\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "667BFBBF3BF09CCB64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
package/dist/streams/index.d.ts
CHANGED
|
@@ -1,4 +1,145 @@
|
|
|
1
1
|
import { STREAMS_EVENT_TYPES, StreamsEventType } from "@secondlayer/shared";
|
|
2
|
+
/**
|
|
3
|
+
* A consumer sink: the destination adapter a consume loop writes through.
|
|
4
|
+
*
|
|
5
|
+
* Without a sink, the user owns the three hard parts of a durable indexer —
|
|
6
|
+
* checkpoint persistence, rows+cursor atomicity, and reorg rollback — and
|
|
7
|
+
* the reasoning that makes them safe lives in doc comments. A sink owns all
|
|
8
|
+
* three: the loop loads its committed cursor, hands the handler a
|
|
9
|
+
* transaction, commits rows AND cursor atomically, and rolls back reorgs
|
|
10
|
+
* without any user code. Crucially, a sink also closes the silent-skip
|
|
11
|
+
* hazard where omitting `onReorg` meant reorgs were ignored forever: with a
|
|
12
|
+
* sink attached, rollback is unconditional.
|
|
13
|
+
*
|
|
14
|
+
* The interface is dependency-free on purpose: implementations with real
|
|
15
|
+
* database drivers live behind subpath exports (`@secondlayer/sdk/sinks/*`)
|
|
16
|
+
* so the root entry ships no DB dependency.
|
|
17
|
+
*/
|
|
18
|
+
interface ConsumerSink<Tx = unknown> {
|
|
19
|
+
/** Phantom marker carrying `Tx` in a directly-inferable position, so the
|
|
20
|
+
* consume loops' `TTx` type parameter resolves from the `sink` option
|
|
21
|
+
* (never set at runtime). */
|
|
22
|
+
readonly _tx?: Tx;
|
|
23
|
+
/** The committed checkpoint, or `null` on first run. Called once, before
|
|
24
|
+
* the first page. Implementations may create their checkpoint storage
|
|
25
|
+
* here and SHOULD validate their rollback preconditions (e.g. that every
|
|
26
|
+
* declared table carries the height column). */
|
|
27
|
+
loadCursor(): Promise<string | null>;
|
|
28
|
+
/**
|
|
29
|
+
* Apply one batch atomically: open a transaction, run `write(tx)` (the
|
|
30
|
+
* user's inserts), and commit the rows AND `cursor` together. A throw
|
|
31
|
+
* from `write` must abort the whole transaction — leaving neither rows
|
|
32
|
+
* nor cursor, so a crashed batch is simply re-read on restart.
|
|
33
|
+
*/
|
|
34
|
+
commitBatch(cursor: string, write: (tx: Tx) => Promise<void> | void): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Roll the projection back to the fork: delete everything AT OR ABOVE
|
|
37
|
+
* `forkPointHeight` (inclusive `>=` — the new chain re-supplies the fork
|
|
38
|
+
* block) and commit `rewindCursor` in the SAME transaction. Deleting
|
|
39
|
+
* without the rewound cursor is the classic silent-gap bug: a crash
|
|
40
|
+
* between the two writes resumes above the fork and the deleted range is
|
|
41
|
+
* never re-read.
|
|
42
|
+
*/
|
|
43
|
+
rollback(forkPointHeight: number, rewindCursor: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/** `ctx` gains `tx` exactly when a sink is attached; without one the shape
|
|
46
|
+
* is unchanged (an intersection with `unknown` is an identity, so
|
|
47
|
+
* contextual typing of `onBatch` callbacks never degrades to a union). */
|
|
48
|
+
type WithSinkTx<TTx> = [TTx] extends [never] ? unknown : {
|
|
49
|
+
tx: TTx
|
|
50
|
+
};
|
|
51
|
+
type IndexEventBase = {
|
|
52
|
+
cursor: string
|
|
53
|
+
block_height: number
|
|
54
|
+
block_time?: string | null
|
|
55
|
+
tx_id: string
|
|
56
|
+
tx_index: number
|
|
57
|
+
event_index: number
|
|
58
|
+
contract_id: string | null
|
|
59
|
+
/** Submitting-transaction context, present only when `txContext: true` was
|
|
60
|
+
* requested. `tx_sender` is the real tx sender — distinct from a transfer's
|
|
61
|
+
* asset `sender`, and the only place a `print` event's sender is available.
|
|
62
|
+
* Lets a consumer build per-event tx context without a `/v1/index/transactions`
|
|
63
|
+
* call per event. */
|
|
64
|
+
tx_sender?: string | null
|
|
65
|
+
tx_type?: string | null
|
|
66
|
+
tx_status?: string | null
|
|
67
|
+
tx_contract_id?: string | null
|
|
68
|
+
tx_function_name?: string | null
|
|
69
|
+
};
|
|
70
|
+
type IndexFtTransfer = IndexEventBase & {
|
|
71
|
+
event_type: "ft_transfer"
|
|
72
|
+
asset_identifier: string
|
|
73
|
+
sender: string
|
|
74
|
+
recipient: string
|
|
75
|
+
amount: string
|
|
76
|
+
};
|
|
77
|
+
type IndexNftTransfer = IndexEventBase & {
|
|
78
|
+
event_type: "nft_transfer"
|
|
79
|
+
asset_identifier: string
|
|
80
|
+
sender: string
|
|
81
|
+
recipient: string
|
|
82
|
+
value: string
|
|
83
|
+
};
|
|
84
|
+
type IndexStxTransfer = IndexEventBase & {
|
|
85
|
+
event_type: "stx_transfer"
|
|
86
|
+
sender: string
|
|
87
|
+
recipient: string
|
|
88
|
+
amount: string
|
|
89
|
+
memo: string | null
|
|
90
|
+
};
|
|
91
|
+
type IndexStxMint = IndexEventBase & {
|
|
92
|
+
event_type: "stx_mint"
|
|
93
|
+
recipient: string
|
|
94
|
+
amount: string
|
|
95
|
+
};
|
|
96
|
+
type IndexStxBurn = IndexEventBase & {
|
|
97
|
+
event_type: "stx_burn"
|
|
98
|
+
sender: string
|
|
99
|
+
amount: string
|
|
100
|
+
};
|
|
101
|
+
type IndexStxLock = IndexEventBase & {
|
|
102
|
+
event_type: "stx_lock"
|
|
103
|
+
sender: string
|
|
104
|
+
amount: string
|
|
105
|
+
payload: {
|
|
106
|
+
unlock_height: string | null
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
type IndexFtMint = IndexEventBase & {
|
|
110
|
+
event_type: "ft_mint"
|
|
111
|
+
asset_identifier: string
|
|
112
|
+
recipient: string
|
|
113
|
+
amount: string
|
|
114
|
+
};
|
|
115
|
+
type IndexFtBurn = IndexEventBase & {
|
|
116
|
+
event_type: "ft_burn"
|
|
117
|
+
asset_identifier: string
|
|
118
|
+
sender: string
|
|
119
|
+
amount: string
|
|
120
|
+
};
|
|
121
|
+
type IndexNftMint = IndexEventBase & {
|
|
122
|
+
event_type: "nft_mint"
|
|
123
|
+
asset_identifier: string
|
|
124
|
+
recipient: string
|
|
125
|
+
value: string
|
|
126
|
+
};
|
|
127
|
+
type IndexNftBurn = IndexEventBase & {
|
|
128
|
+
event_type: "nft_burn"
|
|
129
|
+
asset_identifier: string
|
|
130
|
+
sender: string
|
|
131
|
+
value: string
|
|
132
|
+
};
|
|
133
|
+
type IndexPrint = IndexEventBase & {
|
|
134
|
+
event_type: "print"
|
|
135
|
+
payload: {
|
|
136
|
+
topic: string | null
|
|
137
|
+
value: unknown
|
|
138
|
+
raw_value: string | null
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
/** Decoded chain event, discriminated by `event_type`. */
|
|
142
|
+
type IndexEvent = IndexFtTransfer | IndexNftTransfer | IndexStxTransfer | IndexStxMint | IndexStxBurn | IndexStxLock | IndexFtMint | IndexFtBurn | IndexNftMint | IndexNftBurn | IndexPrint;
|
|
2
143
|
/** A Clarity value as Streams serves it: the canonical hex string, a typed
|
|
3
144
|
* object carrying that hex (`{ hex }`), or a decoded Clarity-JSON object.
|
|
4
145
|
* Decode helpers (`decodeNftTransfer`, etc.) resolve it to a concrete value. */
|
|
@@ -243,8 +384,28 @@ type StreamsBatchContext = ConsumerBatchContext;
|
|
|
243
384
|
type StreamsReorgContext = {
|
|
244
385
|
cursor: string
|
|
245
386
|
};
|
|
246
|
-
type StreamsEventsConsumeParams
|
|
387
|
+
type StreamsEventsConsumeParams<
|
|
388
|
+
TTx = never,
|
|
389
|
+
D extends boolean = false
|
|
390
|
+
> = {
|
|
247
391
|
fromCursor?: string | null
|
|
392
|
+
/**
|
|
393
|
+
* Deliver events pre-decoded as the flat, `event_type`-discriminated rows
|
|
394
|
+
* Index serves (`IndexEvent`), so Streams consumption reads identically
|
|
395
|
+
* to Index consumption — no guard+decode pairs in your handler.
|
|
396
|
+
*/
|
|
397
|
+
decoded?: D
|
|
398
|
+
/**
|
|
399
|
+
* Destination adapter that owns the checkpoint + rollback transaction
|
|
400
|
+
* (e.g. `kyselySink` from `@secondlayer/sdk/sinks/kysely`). With a sink:
|
|
401
|
+
* the loop resumes from the sink's committed cursor, `onBatch` receives
|
|
402
|
+
* `ctx.tx` and must write ONLY through it (rows and cursor commit in one
|
|
403
|
+
* transaction), reorg rollback is automatic, and `onBatch`'s return value
|
|
404
|
+
* is ignored.
|
|
405
|
+
*/
|
|
406
|
+
sink?: ConsumerSink<TTx>
|
|
407
|
+
/** Fires once per page, before `onBatch` and before any early return. */
|
|
408
|
+
onProgress?: (ctx: ConsumerBatchContext) => void
|
|
248
409
|
mode?: "tail" | "bounded"
|
|
249
410
|
/**
|
|
250
411
|
* Emit only finalized (immutable) events and never surface reorgs. The SDK
|
|
@@ -265,7 +426,7 @@ type StreamsEventsConsumeParams = {
|
|
|
265
426
|
* transaction as your writes. Returning a cursor overrides `ctx.cursor` as
|
|
266
427
|
* the resume point (advanced manual control); returning nothing uses it.
|
|
267
428
|
*/
|
|
268
|
-
onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope, ctx: StreamsBatchContext) => void | string | null | undefined | Promise<void> | Promise<string | null | undefined>
|
|
429
|
+
onBatch: (events: D extends true ? IndexEvent[] : StreamsEvent[], envelope: StreamsEventsEnvelope, ctx: StreamsBatchContext & WithSinkTx<TTx>) => void | string | null | undefined | Promise<void> | Promise<string | null | undefined>
|
|
269
430
|
/**
|
|
270
431
|
* Roll your projection back to `reorg.fork_point_height`, persisting
|
|
271
432
|
* `ctx.cursor` in the same transaction. Called once per *new* reorg
|
|
@@ -351,7 +512,7 @@ type StreamsEventsReplayParams = {
|
|
|
351
512
|
maxEmptyPolls?: number
|
|
352
513
|
signal?: AbortSignal
|
|
353
514
|
};
|
|
354
|
-
type
|
|
515
|
+
type FetchLike2 = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
355
516
|
/** One bulk parquet file in the dumps manifest. `path` is the object key under
|
|
356
517
|
* the dumps base URL. */
|
|
357
518
|
type StreamsDumpFile = {
|
|
@@ -423,7 +584,12 @@ type StreamsClient = {
|
|
|
423
584
|
* The consumer also exits when `maxPages`, `maxEmptyPolls`, or `signal`
|
|
424
585
|
* stops it.
|
|
425
586
|
*/
|
|
426
|
-
consume
|
|
587
|
+
consume<
|
|
588
|
+
TTx = never,
|
|
589
|
+
D extends boolean = false
|
|
590
|
+
>(params: StreamsEventsConsumeParams<TTx, D> & {
|
|
591
|
+
sink?: ConsumerSink<TTx>
|
|
592
|
+
}): Promise<StreamsEventsConsumeResult>
|
|
427
593
|
/**
|
|
428
594
|
* Backfill from bulk dumps, then continue live from the dump→live seam in
|
|
429
595
|
* one call. Iterates finalized dump files (via `onDumpFile`) in block
|
|
@@ -476,7 +642,7 @@ type StreamsUsage = {
|
|
|
476
642
|
type CreateStreamsClientOptions = {
|
|
477
643
|
apiKey: string
|
|
478
644
|
baseUrl?: string
|
|
479
|
-
fetchImpl?:
|
|
645
|
+
fetchImpl?: FetchLike2
|
|
480
646
|
/**
|
|
481
647
|
* Public base URL for bulk parquet dumps (the R2/CDN bucket root). Required
|
|
482
648
|
* to use `client.dumps`. See `GET /public/streams/dumps/manifest`.
|
|
@@ -512,6 +678,22 @@ type CreateStreamsClientOptions = {
|
|
|
512
678
|
verifyDumpsManifest?: boolean
|
|
513
679
|
};
|
|
514
680
|
declare function createStreamsClient(options: CreateStreamsClientOptions): StreamsClient;
|
|
681
|
+
/**
|
|
682
|
+
* Decode a raw Streams event into the SAME flat, `event_type`-discriminated
|
|
683
|
+
* row shape Index serves — so Streams consumption reads identically to Index
|
|
684
|
+
* consumption, with one call instead of eleven guard+decode pairs.
|
|
685
|
+
*
|
|
686
|
+
* ```ts
|
|
687
|
+
* for (const event of envelope.events) {
|
|
688
|
+
* const row = decode(event);
|
|
689
|
+
* if (row.event_type === "ft_transfer") row.amount; // string, narrowed
|
|
690
|
+
* }
|
|
691
|
+
* ```
|
|
692
|
+
*
|
|
693
|
+
* Prefer `decoded: true` on `streams.events.consume` — then decoding never
|
|
694
|
+
* appears in your code at all.
|
|
695
|
+
*/
|
|
696
|
+
declare function decode(event: StreamsEvent): IndexEvent;
|
|
515
697
|
/** Options accepted by every error in the family. */
|
|
516
698
|
interface SecondLayerErrorOptions {
|
|
517
699
|
cause?: Error;
|
|
@@ -893,4 +1075,4 @@ declare const Cursor: {
|
|
|
893
1075
|
}
|
|
894
1076
|
};
|
|
895
1077
|
type DecodedEventRow = DecodedFtTransfer | DecodedNftTransfer | DecodedStxTransfer | DecodedStxMint | DecodedStxBurn | DecodedStxLock | DecodedFtMint | DecodedFtBurn | DecodedNftMint | DecodedNftBurn | DecodedPrint;
|
|
896
|
-
export { isStxTransfer, isStxMint, isStxLock, isStxBurn, isPrint, isNftTransfer, isNftMint, isNftBurn, isFtTransfer, isFtMint, isFtBurn, decodeStxTransfer, decodeStxMint, decodeStxLock, decodeStxBurn, decodePrint, decodeNftTransfer, decodeNftMint, decodeNftBurn, decodeFtTransfer, decodeFtMint, decodeFtBurn, createStreamsClient, ValidationError, StreamsUsage, 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, NftTransferPayload, NftTransferEvent, FtTransferPayload, FtTransferEvent, 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 };
|
|
1078
|
+
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, StreamsUsage, 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, NftTransferPayload, NftTransferEvent, 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 };
|