@secondlayer/sdk 6.40.1 → 6.40.2
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 +107 -0
- package/package.json +3 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @secondlayer/sdk — notes for coding agents
|
|
2
|
+
|
|
3
|
+
Five facts that are load-bearing. Everything else you can infer from the types;
|
|
4
|
+
these you cannot, and getting them wrong produces code that looks correct and
|
|
5
|
+
silently loses or corrupts data.
|
|
6
|
+
|
|
7
|
+
## 1. Reads need no key
|
|
8
|
+
|
|
9
|
+
`/v1/index/*` and the public subgraph reads are open. Construct the client with
|
|
10
|
+
no credentials and start reading:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Index } from "@secondlayer/sdk";
|
|
14
|
+
|
|
15
|
+
const sl = new Index();
|
|
16
|
+
const { events } = await sl.events.list({ eventType: "ft_transfer" });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Keys are for Streams, writes, and higher rate limits. Do not invent an
|
|
20
|
+
`apiKey` requirement or send a placeholder — an empty Bearer is worse than none.
|
|
21
|
+
|
|
22
|
+
## 2. Cursors are opaque
|
|
23
|
+
|
|
24
|
+
A cursor is `"<block_height>:<event_index>"`, and that is an implementation
|
|
25
|
+
detail you may read but must never construct, parse into, or compare
|
|
26
|
+
arithmetically. Pass back exactly what you were handed:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
let cursor: string | null = null;
|
|
30
|
+
for (;;) {
|
|
31
|
+
const page = await sl.events.list({ eventType: "ft_transfer", cursor });
|
|
32
|
+
if (page.events.length === 0) break;
|
|
33
|
+
cursor = page.next_cursor; // ✓
|
|
34
|
+
// cursor = `${lastHeight}:0`; // ✗ — skips events, silently
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`Cursor.atHeight(h)` exists for "start at the foot of block h"; use it rather
|
|
39
|
+
than hand-rolling a sentinel.
|
|
40
|
+
|
|
41
|
+
## 3. Reorg rollback is INCLUSIVE
|
|
42
|
+
|
|
43
|
+
`onReorg` hands you `reorg.fork_point_height`. Delete rows at
|
|
44
|
+
`_block_height >= forkHeight`, not `>`. The fork block itself is replaced, so an
|
|
45
|
+
exclusive delete leaves exactly one block of orphaned rows behind — the hardest
|
|
46
|
+
kind of corruption to notice, because everything downstream still looks
|
|
47
|
+
consistent.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
onReorg: async (reorg, ctx) => {
|
|
51
|
+
await tx.deleteFrom("my_rows").where("height", ">=", reorg.fork_point_height).execute();
|
|
52
|
+
await saveCursor(tx, ctx.cursor);
|
|
53
|
+
},
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 4. Rows and the cursor commit in ONE transaction
|
|
57
|
+
|
|
58
|
+
If they commit separately, a crash between them either replays a batch (double
|
|
59
|
+
counts) or skips it (missing rows). There is no ordering that avoids both.
|
|
60
|
+
|
|
61
|
+
Prefer a sink, which owns this by construction — you write rows, it commits them
|
|
62
|
+
with the checkpoint and handles the rollback:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { kyselySink } from "@secondlayer/sdk/sinks/kysely";
|
|
66
|
+
|
|
67
|
+
await sl.streams.events.consume({
|
|
68
|
+
sink: kyselySink(db, { id: "my-indexer", tables: ["transfers"], height: "height" }),
|
|
69
|
+
onBatch: async (events, _envelope, ctx) => {
|
|
70
|
+
for (const e of events) await ctx.tx.insertInto("transfers").values(...).execute();
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Hand-rolling it is allowed, but the checkpoint write must be inside the same
|
|
76
|
+
transaction as the row writes.
|
|
77
|
+
|
|
78
|
+
## 5. `walk()` is not reorg-safe
|
|
79
|
+
|
|
80
|
+
`walk()` iterates a fixed range for backfills and analysis. It does not surface
|
|
81
|
+
reorgs and does not rewind. Anything writing durable state off the tip belongs
|
|
82
|
+
in `consume()` with `onReorg` (or a sink). Use `walk()` for history that is
|
|
83
|
+
already final.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Shapes worth knowing
|
|
88
|
+
|
|
89
|
+
- **`decoded: true`** on `streams.events.consume` delivers the flat,
|
|
90
|
+
`event_type`-discriminated rows Index serves, so Streams and Index handlers
|
|
91
|
+
read identically. Prefer it over the individual `is*`/`decode*` helpers.
|
|
92
|
+
- **Labelled filters**: `filters: { <label>: {...} }` plus `on: { <label>: handler }`
|
|
93
|
+
runs two unrelated concerns through one loop, one cursor, one checkpoint. Each
|
|
94
|
+
label's declared `types` narrows its handler.
|
|
95
|
+
- **Field selection**: `fields: ["recipient", "amount"]` on Index reads projects
|
|
96
|
+
the SELECT server-side; unrequested columns are absent from the row *and* the
|
|
97
|
+
type. `cursor`, `block_height`, and `event_type` always come back.
|
|
98
|
+
- **Errors**: everything derives from `SecondLayerError` — `code`, `retryable`,
|
|
99
|
+
`retryAfterSeconds`, `docsUrl`, and `walk(predicate)` to find a cause. Retries
|
|
100
|
+
are built in (`retryCount`/`retryDelay`); `onError` is a void observer, not a
|
|
101
|
+
retry decision.
|
|
102
|
+
|
|
103
|
+
## More
|
|
104
|
+
|
|
105
|
+
- Docs, full text: <https://secondlayer.tools/llms-full.txt>
|
|
106
|
+
- Any page as markdown: append `.md` (e.g. `/docs/streams.md`)
|
|
107
|
+
- Deeper agent skill: `bunx skills add ryanwaits/secondlayer`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@secondlayer/sdk",
|
|
3
|
-
"version": "6.40.
|
|
3
|
+
"version": "6.40.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
-
"dist"
|
|
37
|
+
"dist",
|
|
38
|
+
"AGENTS.md"
|
|
38
39
|
],
|
|
39
40
|
"scripts": {
|
|
40
41
|
"build": "bunup",
|