@secondlayer/sdk 6.40.0 → 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 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/dist/index.js CHANGED
@@ -655,7 +655,8 @@ async function consumeStreamsEvents(opts) {
655
655
  const emptyBackoffMs = opts.emptyBackoffMs ?? 500;
656
656
  const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;
657
657
  const maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;
658
- let cursor = opts.fromCursor ?? await opts.sink?.loadCursor() ?? null;
658
+ const committedCursor = await opts.sink?.loadCursor() ?? null;
659
+ let cursor = opts.fromCursor ?? committedCursor;
659
660
  const handledReorgs = new Set;
660
661
  let pages = 0;
661
662
  let emptyPolls = 0;
@@ -820,7 +821,8 @@ async function consumeIndexFeed(opts) {
820
821
  const emptyBackoffMs = opts.emptyBackoffMs ?? 500;
821
822
  const maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;
822
823
  const maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;
823
- let cursor = opts.fromCursor ?? await opts.sink?.loadCursor() ?? null;
824
+ const committedCursor = await opts.sink?.loadCursor() ?? null;
825
+ let cursor = opts.fromCursor ?? committedCursor;
824
826
  const handledReorgs = new Set;
825
827
  let pages = 0;
826
828
  let emptyPolls = 0;
@@ -2875,5 +2877,5 @@ export {
2875
2877
  ApiError
2876
2878
  };
2877
2879
 
2878
- //# debugId=0D258137688545B064756E2164756E21
2880
+ //# debugId=E4A30787B92D2A4C64756E2164756E21
2879
2881
  //# sourceMappingURL=index.js.map