@artblocks/abx-indexer 0.1.0-alpha.4 → 0.1.0-alpha.41
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/CHANGELOG.md +62 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/indexer.d.ts +81 -10
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +120 -8
- package/dist/indexer.js.map +1 -1
- package/dist/store.d.ts +128 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +314 -31
- package/dist/store.js.map +1 -1
- package/package.json +7 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @artblocks/abx-indexer
|
|
2
|
+
|
|
3
|
+
## 0.1.0-alpha.41
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [b9197bc]
|
|
8
|
+
- @artblocks/abx-sdk@0.1.0-alpha.40
|
|
9
|
+
|
|
10
|
+
## 0.1.0-alpha.40
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [3d7fe3c]
|
|
15
|
+
- Updated dependencies [7b462e2]
|
|
16
|
+
- @artblocks/abx-sdk@0.1.0-alpha.39
|
|
17
|
+
|
|
18
|
+
## 0.1.0-alpha.39
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [c2400a7]
|
|
23
|
+
- @artblocks/abx-sdk@0.1.0-alpha.38
|
|
24
|
+
|
|
25
|
+
## 0.1.0-alpha.38
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [4512962]
|
|
30
|
+
- @artblocks/abx-sdk@0.1.0-alpha.37
|
|
31
|
+
|
|
32
|
+
## 0.1.0-alpha.37
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- Updated dependencies [5522192]
|
|
37
|
+
- @artblocks/abx-sdk@0.1.0-alpha.36
|
|
38
|
+
|
|
39
|
+
## 0.1.0-alpha.36
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- Updated dependencies [21e3d6c]
|
|
44
|
+
- Updated dependencies [a73922c]
|
|
45
|
+
- @artblocks/abx-sdk@0.1.0-alpha.35
|
|
46
|
+
|
|
47
|
+
## 0.1.0-alpha.35
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [3ddba71]
|
|
52
|
+
- Updated dependencies [9065128]
|
|
53
|
+
- @artblocks/abx-sdk@0.1.0-alpha.34
|
|
54
|
+
|
|
55
|
+
## 0.1.0-alpha.34
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- 9242a05: Prepare package metadata, release notes, and public-facing source comments for the initial public
|
|
60
|
+
source release.
|
|
61
|
+
- Updated dependencies [9242a05]
|
|
62
|
+
- @artblocks/abx-sdk@0.1.0-alpha.33
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* Replays the event spine from chain into a disposable, rebuildable projection.
|
|
5
5
|
* The canonical re-index: how every service onboards or exits a project.
|
|
6
6
|
*/
|
|
7
|
-
export { SelfHostIndexer, type IndexResult } from './indexer.js';
|
|
7
|
+
export { SelfHostIndexer, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_VACUUM_INTERVAL_MS, type IndexResult, type ReindexOptions, } from './indexer.js';
|
|
8
8
|
export { SqliteStore, type Store, type ProjectRegistration, type EffectStatusRow, type EffectArtifactRow, } from './store.js';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,eAAe,EACf,gCAAgC,EAChC,0BAA0B,EAC1B,KAAK,WAAW,EAChB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,WAAW,EACX,KAAK,KAAK,EACV,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* Replays the event spine from chain into a disposable, rebuildable projection.
|
|
5
5
|
* The canonical re-index: how every service onboards or exits a project.
|
|
6
6
|
*/
|
|
7
|
-
export { SelfHostIndexer } from './indexer.js';
|
|
7
|
+
export { SelfHostIndexer, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_VACUUM_INTERVAL_MS, } from './indexer.js';
|
|
8
8
|
export { SqliteStore, } from './store.js';
|
|
9
9
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,eAAe,EACf,gCAAgC,EAChC,0BAA0B,GAG3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,WAAW,GAKZ,MAAM,YAAY,CAAC"}
|
package/dist/indexer.d.ts
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
-
import { type Address, type ProjectState, type PublicClient } from '@artblocks/abx-sdk';
|
|
1
|
+
import { type Address, type GetLogsAdaptiveOptions, type ProjectState, type PublicClient, type ReconstructBlockTag } from '@artblocks/abx-sdk';
|
|
2
|
+
import type { Log } from 'viem';
|
|
2
3
|
import { type IndexStatusRow, type Store, type ProjectRegistration } from './store.js';
|
|
4
|
+
export interface ReindexOptions {
|
|
5
|
+
full?: boolean;
|
|
6
|
+
readUriDocuments?: boolean;
|
|
7
|
+
rpcUrl?: string;
|
|
8
|
+
/** Already-fetched logs for this project covering {@link toBlock}. Skips the
|
|
9
|
+
* per-project `eth_getLogs` — the watch loop that scanned the registered set
|
|
10
|
+
* holds the delta and should be the only getLogs. */
|
|
11
|
+
logs?: readonly Log[];
|
|
12
|
+
/** Inclusive scan head `logs` cover. Required with `logs`. */
|
|
13
|
+
toBlock?: bigint;
|
|
14
|
+
/**
|
|
15
|
+
* Stop the scan boundary at this block tag instead of chain head (the default, unchanged
|
|
16
|
+
* behavior). Resolved to a concrete block number BEFORE scanning (`resolveBlockTag` in the SDK),
|
|
17
|
+
* so the stored watermark (`ProjectState.toBlock`) is always a literal number, never the tag —
|
|
18
|
+
* see {@link ReconstructBlockTag}. Ignored when `logs` is supplied: that path's own `toBlock` is
|
|
19
|
+
* the caller's already-resolved scan head, from a watch loop that has nothing to do with a
|
|
20
|
+
* per-project tag choice.
|
|
21
|
+
*/
|
|
22
|
+
blockTag?: ReconstructBlockTag;
|
|
23
|
+
/** See {@link GetLogsAdaptiveOptions.onChunk} — the only progress signal during a large scan. */
|
|
24
|
+
onChunk?: GetLogsAdaptiveOptions['onChunk'];
|
|
25
|
+
}
|
|
3
26
|
export interface IndexResult {
|
|
4
27
|
state: ProjectState;
|
|
5
28
|
elapsedMs: number;
|
|
6
29
|
mode: 'full' | 'incremental';
|
|
7
30
|
}
|
|
31
|
+
/** Bounded per-pass reclaim for {@link SelfHostIndexer.runIncrementalVacuum} — ~100 pages is ~400KB
|
|
32
|
+
* at SQLite's default 4096-byte page size, cheap enough to run synchronously without a request or a
|
|
33
|
+
* watch tick noticing, while a long-running node with a large freelist still drains it over several
|
|
34
|
+
* passes instead of never. */
|
|
35
|
+
export declare const DEFAULT_INCREMENTAL_VACUUM_PAGES = 100;
|
|
36
|
+
/** Default cadence for automatic SQLite maintenance (see
|
|
37
|
+
* {@link SelfHostIndexer.startVacuumMaintenance}). Deliberately far coarser than the chain watcher's
|
|
38
|
+
* poll (`ABX_WATCH_INTERVAL_MS`, 12s by default): freed pages accumulate slowly (a reclaim, a
|
|
39
|
+
* dropped projection, a pruned bound artifact), so there is nothing to gain from touching the file
|
|
40
|
+
* every tick — and this is process-internal scheduling, not an operator-facing knob, so it isn't
|
|
41
|
+
* read from the environment the way the watcher's interval is. */
|
|
42
|
+
export declare const DEFAULT_VACUUM_INTERVAL_MS: number;
|
|
8
43
|
/**
|
|
9
44
|
* The reference indexer (Layer 3). It does one thing well: replay the event
|
|
10
45
|
* spine from chain into the store. Because every state change rides a standard,
|
|
@@ -19,6 +54,16 @@ export declare class SelfHostIndexer {
|
|
|
19
54
|
/** Catch-up runs in flight, per lowercased address — see {@link reindexShared}. */
|
|
20
55
|
private inFlight;
|
|
21
56
|
constructor(store?: Store);
|
|
57
|
+
/**
|
|
58
|
+
* The pooled read client for a chain, or — with `rpcUrl` — one pinned to a SINGLE endpoint.
|
|
59
|
+
*
|
|
60
|
+
* The default (no `rpcUrl`) is a viem `fallback` across every configured endpoint, which fails
|
|
61
|
+
* over on an *error*. It does not fail over on a successful empty answer, which is exactly what an
|
|
62
|
+
* endpoint that has pruned its log history returns for an old block: `[]`, HTTP 200. Pinning one
|
|
63
|
+
* endpoint is how a caller can re-run the same scan against each in turn and find one that holds
|
|
64
|
+
* the history (see the CLI's empty-scan recovery). Pooled per (chain, endpoint) so the pinned
|
|
65
|
+
* clients don't evict the shared one.
|
|
66
|
+
*/
|
|
22
67
|
private client;
|
|
23
68
|
/** The RPC client for a chain — exposed for reads that aren't a reindex (e.g. deploy-block
|
|
24
69
|
* discovery in the admin control plane). Reuses the same pooled client as `reindex`. */
|
|
@@ -34,9 +79,7 @@ export declare class SelfHostIndexer {
|
|
|
34
79
|
* replay. `{full: true}` forces a replay from the deploy block (the durability
|
|
35
80
|
* proof, and what runs automatically when there's no prior state). Idempotent.
|
|
36
81
|
*/
|
|
37
|
-
reindex(address: Address, opts?:
|
|
38
|
-
full?: boolean;
|
|
39
|
-
}): Promise<IndexResult>;
|
|
82
|
+
reindex(address: Address, opts?: ReindexOptions): Promise<IndexResult>;
|
|
40
83
|
/**
|
|
41
84
|
* {@link reindex}, but concurrent calls for the same project COALESCE into one run.
|
|
42
85
|
*
|
|
@@ -46,9 +89,7 @@ export declare class SelfHostIndexer {
|
|
|
46
89
|
* the bottleneck — so the second caller joins the first run instead. A caller that needs a *full*
|
|
47
90
|
* replay while an incremental is in flight gets its replay queued after it, never dropped.
|
|
48
91
|
*/
|
|
49
|
-
reindexShared(address: Address, opts?:
|
|
50
|
-
full?: boolean;
|
|
51
|
-
}): Promise<IndexResult>;
|
|
92
|
+
reindexShared(address: Address, opts?: ReindexOptions): Promise<IndexResult>;
|
|
52
93
|
/** Is a catch-up for this project running right now? (Lets a caller answer "still backfilling"
|
|
53
94
|
* without starting more work.) */
|
|
54
95
|
isCatchingUp(address: Address): boolean;
|
|
@@ -61,14 +102,44 @@ export declare class SelfHostIndexer {
|
|
|
61
102
|
indexStatus(address: Address): IndexStatusRow;
|
|
62
103
|
private track;
|
|
63
104
|
/** Re-index every registered project. */
|
|
64
|
-
reindexAll(opts?:
|
|
65
|
-
full?: boolean;
|
|
66
|
-
}): Promise<IndexResult[]>;
|
|
105
|
+
reindexAll(opts?: ReindexOptions): Promise<IndexResult[]>;
|
|
67
106
|
getProject(address: Address): ProjectState | null;
|
|
68
107
|
/** Throw away a project's reconstructed projection, keeping its registration — the next
|
|
69
108
|
* {@link reindex} then rebuilds it from the deploy block. The projection is a disposable cache of
|
|
70
109
|
* chain state; this is how you prove it (`abx demo` deletes and replays to show identical state). */
|
|
71
110
|
dropProjection(address: Address): void;
|
|
72
111
|
listProjects(): ProjectState[];
|
|
112
|
+
/**
|
|
113
|
+
* Run one bounded `PRAGMA incremental_vacuum` pass, delegating to {@link SqliteStore.runIncrementalVacuum}.
|
|
114
|
+
* A no-op (returns 0) for any `Store` implementation that isn't a `SqliteStore` — vacuuming is a
|
|
115
|
+
* SQLite-specific concern, so a Postgres-backed deploy (which owns its own manual VACUUM story)
|
|
116
|
+
* simply has nothing to do here. Exposed on its own (not just via {@link startVacuumMaintenance})
|
|
117
|
+
* so a caller can also run one pass on demand — e.g. right after `abx vacuum convert`, to reclaim
|
|
118
|
+
* whatever the full VACUUM already didn't (it shouldn't leave anything, but this makes "did it
|
|
119
|
+
* work" independently checkable).
|
|
120
|
+
*/
|
|
121
|
+
runIncrementalVacuum(maxPages?: number): number;
|
|
122
|
+
/**
|
|
123
|
+
* Start automatic SQLite maintenance: {@link runIncrementalVacuum} on its own
|
|
124
|
+
* timer, decoupled from the chain watcher's poll loop (`packages/token-api/src/watcher.ts`) so a
|
|
125
|
+
* reclaim pass can never sit inline with a request or a watch tick — it runs BETWEEN them, on
|
|
126
|
+
* whatever cadence the caller (or the default) picks, and a slow pass only ever delays the next
|
|
127
|
+
* pass, never a request being served concurrently (`node:sqlite`'s `DatabaseSync` is synchronous,
|
|
128
|
+
* but each pass is bounded — see `SqliteStore.runIncrementalVacuum` — precisely so that's cheap).
|
|
129
|
+
*
|
|
130
|
+
* `abx serve` is the intended caller, started once alongside `startChainWatcher`. Safe to call
|
|
131
|
+
* unconditionally: for a non-`SqliteStore` backend this still returns a working `stop()`, it just
|
|
132
|
+
* schedules calls that are themselves no-ops (see {@link runIncrementalVacuum}).
|
|
133
|
+
*
|
|
134
|
+
* A failed pass is logged and swallowed — maintenance must never be the thing that takes a node
|
|
135
|
+
* down; the freelist simply waits for the next pass.
|
|
136
|
+
*/
|
|
137
|
+
startVacuumMaintenance(opts?: {
|
|
138
|
+
intervalMs?: number;
|
|
139
|
+
maxPages?: number;
|
|
140
|
+
log?: (line: string) => void;
|
|
141
|
+
}): {
|
|
142
|
+
stop(): void;
|
|
143
|
+
};
|
|
73
144
|
}
|
|
74
145
|
//# sourceMappingURL=indexer.d.ts.map
|
package/dist/indexer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,OAAO,EACZ,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,MAAM,CAAC;AAC9B,OAAO,EAAc,KAAK,cAAc,EAAE,KAAK,KAAK,EAAE,KAAK,mBAAmB,EAAC,MAAM,YAAY,CAAC;AAElG,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;0DAEsD;IACtD,IAAI,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IACtB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,iGAAiG;IACjG,OAAO,CAAC,EAAE,sBAAsB,CAAC,SAAS,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC;CAC9B;AAED;;;+BAG+B;AAC/B,eAAO,MAAM,gCAAgC,MAAM,CAAC;AAEpD;;;;;mEAKmE;AACnE,eAAO,MAAM,0BAA0B,QAAa,CAAC;AAErD;;;;;;;GAOG;AACH,qBAAa,eAAe;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,OAAO,CAAC,OAAO,CAAmC;IAClD,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAqE;gBAEzE,KAAK,CAAC,EAAE,KAAK;IAIzB;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM;IAOd;6FACyF;IACzF,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY;IAI5C;;6DAEyD;IACzD,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC,GAAG,mBAAmB;IAO7E;;;;;;OAMG;IACG,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,WAAW,CAAC;IA2FhF;;;;;;;;OAQG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,WAAW,CAAC;IAYhF;uCACmC;IACnC,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO;IAIvC;;;;;OAKG;IACH,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,cAAc;IAU7C,OAAO,CAAC,KAAK;IAoBb,yCAAyC;IACnC,UAAU,CAAC,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAQnE,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI;IAIjD;;0GAEsG;IACtG,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAItC,YAAY,IAAI,YAAY,EAAE;IAO9B;;;;;;;;OAQG;IACH,oBAAoB,CAAC,QAAQ,GAAE,MAAyC,GAAG,MAAM;IAIjF;;;;;;;;;;;;;;OAcG;IACH,sBAAsB,CAAC,IAAI,GAAE;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;KAAM,GAAG;QAAC,IAAI,IAAI,IAAI,CAAA;KAAC;CAiB1H"}
|
package/dist/indexer.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import { classifyIndexError, makePublicClient, reconstructIncremental, reconstructProject, } from '@artblocks/abx-sdk';
|
|
1
|
+
import { classifyIndexError, makePublicClient, reconstructFromLogs, reconstructIncremental, reconstructProject, } from '@artblocks/abx-sdk';
|
|
2
2
|
import { SqliteStore } from './store.js';
|
|
3
|
+
/** Bounded per-pass reclaim for {@link SelfHostIndexer.runIncrementalVacuum} — ~100 pages is ~400KB
|
|
4
|
+
* at SQLite's default 4096-byte page size, cheap enough to run synchronously without a request or a
|
|
5
|
+
* watch tick noticing, while a long-running node with a large freelist still drains it over several
|
|
6
|
+
* passes instead of never. */
|
|
7
|
+
export const DEFAULT_INCREMENTAL_VACUUM_PAGES = 100;
|
|
8
|
+
/** Default cadence for automatic SQLite maintenance (see
|
|
9
|
+
* {@link SelfHostIndexer.startVacuumMaintenance}). Deliberately far coarser than the chain watcher's
|
|
10
|
+
* poll (`ABX_WATCH_INTERVAL_MS`, 12s by default): freed pages accumulate slowly (a reclaim, a
|
|
11
|
+
* dropped projection, a pruned bound artifact), so there is nothing to gain from touching the file
|
|
12
|
+
* every tick — and this is process-internal scheduling, not an operator-facing knob, so it isn't
|
|
13
|
+
* read from the environment the way the watcher's interval is. */
|
|
14
|
+
export const DEFAULT_VACUUM_INTERVAL_MS = 5 * 60_000;
|
|
3
15
|
/**
|
|
4
16
|
* The reference indexer (Layer 3). It does one thing well: replay the event
|
|
5
17
|
* spine from chain into the store. Because every state change rides a standard,
|
|
@@ -16,10 +28,21 @@ export class SelfHostIndexer {
|
|
|
16
28
|
constructor(store) {
|
|
17
29
|
this.store = store ?? new SqliteStore();
|
|
18
30
|
}
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The pooled read client for a chain, or — with `rpcUrl` — one pinned to a SINGLE endpoint.
|
|
33
|
+
*
|
|
34
|
+
* The default (no `rpcUrl`) is a viem `fallback` across every configured endpoint, which fails
|
|
35
|
+
* over on an *error*. It does not fail over on a successful empty answer, which is exactly what an
|
|
36
|
+
* endpoint that has pruned its log history returns for an old block: `[]`, HTTP 200. Pinning one
|
|
37
|
+
* endpoint is how a caller can re-run the same scan against each in turn and find one that holds
|
|
38
|
+
* the history (see the CLI's empty-scan recovery). Pooled per (chain, endpoint) so the pinned
|
|
39
|
+
* clients don't evict the shared one.
|
|
40
|
+
*/
|
|
41
|
+
client(chainKey, rpcUrl) {
|
|
42
|
+
const key = rpcUrl ? `${chainKey}|${rpcUrl}` : chainKey;
|
|
43
|
+
let c = this.clients.get(key);
|
|
21
44
|
if (!c)
|
|
22
|
-
this.clients.set(
|
|
45
|
+
this.clients.set(key, (c = makePublicClient({ chainKey, rpcUrl })));
|
|
23
46
|
return c;
|
|
24
47
|
}
|
|
25
48
|
/** The RPC client for a chain — exposed for reads that aren't a reindex (e.g. deploy-block
|
|
@@ -64,15 +87,54 @@ export class SelfHostIndexer {
|
|
|
64
87
|
let state;
|
|
65
88
|
let mode;
|
|
66
89
|
try {
|
|
67
|
-
if (incremental) {
|
|
68
|
-
|
|
90
|
+
if (incremental && opts.logs !== undefined) {
|
|
91
|
+
if (opts.toBlock === undefined) {
|
|
92
|
+
throw new Error('reindex({logs}) requires toBlock — the inclusive scan head those logs cover');
|
|
93
|
+
}
|
|
94
|
+
state = await reconstructFromLogs(this.client(reg.chainKey, opts.rpcUrl), prior, opts.logs, {
|
|
95
|
+
factory,
|
|
96
|
+
readUriDocuments: opts.readUriDocuments,
|
|
97
|
+
toBlock: opts.toBlock,
|
|
98
|
+
});
|
|
99
|
+
mode = 'incremental';
|
|
100
|
+
}
|
|
101
|
+
else if (incremental) {
|
|
102
|
+
state = await reconstructIncremental(this.client(reg.chainKey, opts.rpcUrl), prior, {
|
|
103
|
+
factory,
|
|
104
|
+
readUriDocuments: opts.readUriDocuments,
|
|
105
|
+
onChunk: opts.onChunk,
|
|
106
|
+
toBlock: opts.blockTag,
|
|
107
|
+
});
|
|
69
108
|
mode = 'incremental';
|
|
70
109
|
}
|
|
71
110
|
else {
|
|
72
|
-
state = await reconstructProject(this.client(reg.chainKey), {
|
|
111
|
+
state = await reconstructProject(this.client(reg.chainKey, opts.rpcUrl), {
|
|
73
112
|
address,
|
|
74
113
|
fromBlock: BigInt(reg.fromBlock),
|
|
75
114
|
factory,
|
|
115
|
+
readUriDocuments: opts.readUriDocuments,
|
|
116
|
+
onChunk: opts.onChunk,
|
|
117
|
+
toBlock: opts.blockTag,
|
|
118
|
+
});
|
|
119
|
+
mode = 'full';
|
|
120
|
+
}
|
|
121
|
+
// Self-heal, never loop: an incremental fold that yields FEWER events than what's already
|
|
122
|
+
// stored signals a damaged projection. `eventCount` can only grow with append-only chain
|
|
123
|
+
// history, so a decrease is
|
|
124
|
+
// never legitimate — discard the bad fold before it's written, and repair with exactly ONE
|
|
125
|
+
// full replay from the deploy block. That result is written unconditionally, even in the
|
|
126
|
+
// (pathological) case where it too looks smaller: a full reconstruct from chain is
|
|
127
|
+
// definitionally correct, so there is nothing left to fall back to and no second attempt.
|
|
128
|
+
if (mode === 'incremental' && prior && state.eventCount < prior.eventCount) {
|
|
129
|
+
console.error(`[indexer] ${address}: an incremental fold produced ${state.eventCount} events, fewer than the ` +
|
|
130
|
+
`${prior.eventCount} already stored — discarding it and falling back to one full reconstruct.`);
|
|
131
|
+
state = await reconstructProject(this.client(reg.chainKey, opts.rpcUrl), {
|
|
132
|
+
address,
|
|
133
|
+
fromBlock: BigInt(reg.fromBlock),
|
|
134
|
+
factory,
|
|
135
|
+
readUriDocuments: opts.readUriDocuments,
|
|
136
|
+
onChunk: opts.onChunk,
|
|
137
|
+
toBlock: opts.blockTag,
|
|
76
138
|
});
|
|
77
139
|
mode = 'full';
|
|
78
140
|
}
|
|
@@ -88,7 +150,9 @@ export class SelfHostIndexer {
|
|
|
88
150
|
});
|
|
89
151
|
throw err;
|
|
90
152
|
}
|
|
91
|
-
|
|
153
|
+
// A full replay is the repair path — it purges + rebuilds the append-only event log so a
|
|
154
|
+
// reorg-replaced event can't survive as a stale row (incremental writes never delete).
|
|
155
|
+
this.store.putProject(state, { fullReplay: mode === 'full' });
|
|
92
156
|
const now = new Date().toISOString();
|
|
93
157
|
this.store.setIndexStatus(address, { status: 'live', error: null, attempts: 0, lastAttemptAt: now, lastIndexedAt: now });
|
|
94
158
|
return { state, elapsedMs: Date.now() - started, mode };
|
|
@@ -172,5 +236,53 @@ export class SelfHostIndexer {
|
|
|
172
236
|
listProjects() {
|
|
173
237
|
return this.store.listProjects();
|
|
174
238
|
}
|
|
239
|
+
// ── SQLite maintenance — the bounded-automatic half; the explicit one-time
|
|
240
|
+
// conversion for a pre-existing store lives on `SqliteStore.vacuumConvert` / `abx vacuum convert`.
|
|
241
|
+
/**
|
|
242
|
+
* Run one bounded `PRAGMA incremental_vacuum` pass, delegating to {@link SqliteStore.runIncrementalVacuum}.
|
|
243
|
+
* A no-op (returns 0) for any `Store` implementation that isn't a `SqliteStore` — vacuuming is a
|
|
244
|
+
* SQLite-specific concern, so a Postgres-backed deploy (which owns its own manual VACUUM story)
|
|
245
|
+
* simply has nothing to do here. Exposed on its own (not just via {@link startVacuumMaintenance})
|
|
246
|
+
* so a caller can also run one pass on demand — e.g. right after `abx vacuum convert`, to reclaim
|
|
247
|
+
* whatever the full VACUUM already didn't (it shouldn't leave anything, but this makes "did it
|
|
248
|
+
* work" independently checkable).
|
|
249
|
+
*/
|
|
250
|
+
runIncrementalVacuum(maxPages = DEFAULT_INCREMENTAL_VACUUM_PAGES) {
|
|
251
|
+
return this.store instanceof SqliteStore ? this.store.runIncrementalVacuum(maxPages) : 0;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Start automatic SQLite maintenance: {@link runIncrementalVacuum} on its own
|
|
255
|
+
* timer, decoupled from the chain watcher's poll loop (`packages/token-api/src/watcher.ts`) so a
|
|
256
|
+
* reclaim pass can never sit inline with a request or a watch tick — it runs BETWEEN them, on
|
|
257
|
+
* whatever cadence the caller (or the default) picks, and a slow pass only ever delays the next
|
|
258
|
+
* pass, never a request being served concurrently (`node:sqlite`'s `DatabaseSync` is synchronous,
|
|
259
|
+
* but each pass is bounded — see `SqliteStore.runIncrementalVacuum` — precisely so that's cheap).
|
|
260
|
+
*
|
|
261
|
+
* `abx serve` is the intended caller, started once alongside `startChainWatcher`. Safe to call
|
|
262
|
+
* unconditionally: for a non-`SqliteStore` backend this still returns a working `stop()`, it just
|
|
263
|
+
* schedules calls that are themselves no-ops (see {@link runIncrementalVacuum}).
|
|
264
|
+
*
|
|
265
|
+
* A failed pass is logged and swallowed — maintenance must never be the thing that takes a node
|
|
266
|
+
* down; the freelist simply waits for the next pass.
|
|
267
|
+
*/
|
|
268
|
+
startVacuumMaintenance(opts = {}) {
|
|
269
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_VACUUM_INTERVAL_MS;
|
|
270
|
+
const maxPages = opts.maxPages ?? DEFAULT_INCREMENTAL_VACUUM_PAGES;
|
|
271
|
+
const log = opts.log ?? ((line) => console.error(`[maintenance] ${line}`));
|
|
272
|
+
const timer = setInterval(() => {
|
|
273
|
+
try {
|
|
274
|
+
const reclaimed = this.runIncrementalVacuum(maxPages);
|
|
275
|
+
if (reclaimed > 0)
|
|
276
|
+
log(`incremental_vacuum reclaimed ${reclaimed} page(s)`);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
log(`incremental_vacuum failed (will retry next pass): ${err.message}`);
|
|
280
|
+
}
|
|
281
|
+
}, intervalMs);
|
|
282
|
+
// Never keeps the process alive on its own — a plain background convenience, not a reason `abx
|
|
283
|
+
// serve` (or a test) would hang waiting for it.
|
|
284
|
+
timer.unref?.();
|
|
285
|
+
return { stop: () => clearInterval(timer) };
|
|
286
|
+
}
|
|
175
287
|
}
|
|
176
288
|
//# sourceMappingURL=indexer.js.map
|
package/dist/indexer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexer.js","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"indexer.js","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
CHANGED
|
@@ -22,11 +22,12 @@ export interface ProjectRegistration {
|
|
|
22
22
|
registeredAt: string;
|
|
23
23
|
}
|
|
24
24
|
/** One producer-published effect artifact — a row in the data plane's enumeration registry
|
|
25
|
-
* (`
|
|
25
|
+
* (`site/content/docs/protocol/data-plane.mdx`). The resolver lists a token's rows and keeps only those whose
|
|
26
26
|
* `key` matches the recomputed address at the CURRENT settled inputsHash, so stale rows go
|
|
27
27
|
* silently unlisted (self-invalidation). `locator === null` means the bytes live in custody
|
|
28
|
-
* (the shared backend at `key`, or
|
|
29
|
-
* chain-derived: rows survive a projection wipe and self-heal from a
|
|
28
|
+
* (a co-located producer's bytes in the shared backend at `key`, or a bound output's `bytes` below).
|
|
29
|
+
* Producer-registered, NOT chain-derived: rows survive a projection wipe and self-heal from a
|
|
30
|
+
* runner's next sweep. */
|
|
30
31
|
export interface EffectArtifactRow {
|
|
31
32
|
/** renderArtifactKey (keccak hex; encodes effectKey + settled inputsHash). */
|
|
32
33
|
key: string;
|
|
@@ -38,10 +39,26 @@ export interface EffectArtifactRow {
|
|
|
38
39
|
/** The effect's DECLARED output mimeType — what the manifest and byte route serve. */
|
|
39
40
|
contentType: string | null;
|
|
40
41
|
locator: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* A **bound** output's content (`site/content/docs/protocol/effects.mdx → Bound vs referenced`): the bytes that
|
|
44
|
+
* stitch into the metadata JSON — `render/traits` today. Capped at
|
|
45
|
+
* {@link BOUND_ARTIFACT_MAX_BYTES}; `null` for every **referenced** output, whose bytes stay with
|
|
46
|
+
* the producer and reach us only as `locator`.
|
|
47
|
+
*
|
|
48
|
+
* These live HERE, next to the row, and deliberately not in a `StorageBackend`: byte custody holds
|
|
49
|
+
* *source* bytes an on-chain keccak commits to (switchable, never lapsable), while these are
|
|
50
|
+
* re-creatable projection state whose loss is a re-render. One write instead of two also removes a
|
|
51
|
+
* way for the row and its content to disagree.
|
|
52
|
+
*/
|
|
53
|
+
bytes?: Uint8Array | null;
|
|
41
54
|
updatedAt?: string;
|
|
42
55
|
}
|
|
56
|
+
/** The cap a serving node MUST accept per bound output, and MUST refuse above
|
|
57
|
+
* (`site/content/docs/using-abx/remote-services.mdx → The mode is decided by the binding`). ~100× a real
|
|
58
|
+
* traits payload: generous for what it is for, far too small to become blob storage. */
|
|
59
|
+
export declare const BOUND_ARTIFACT_MAX_BYTES: number;
|
|
43
60
|
/**
|
|
44
|
-
* Where one project sits in the indexing lifecycle (`
|
|
61
|
+
* Where one project sits in the indexing lifecycle (`site/content/docs/using-abx/remote-services.mdx` →
|
|
45
62
|
* The indexing lifecycle) — what the control plane's `status`/list routes report and what
|
|
46
63
|
* `abx status` prints, for a managed provider and for your own node in the same words.
|
|
47
64
|
*
|
|
@@ -119,6 +136,20 @@ export interface Store {
|
|
|
119
136
|
/** Every registered artifact row for (project, token) — the manifest's effect-source listing.
|
|
120
137
|
* Callers filter by recomputing the current-inputsHash key per row (stale rows unlisted). */
|
|
121
138
|
listEffectArtifacts(address: string, tokenId: string): EffectArtifactRow[];
|
|
139
|
+
/**
|
|
140
|
+
* Drop the **bound** content of every superseded row for (project, token, effectKey, outputKey) —
|
|
141
|
+
* everything whose `inputs_hash` isn't `currentHash`.
|
|
142
|
+
*
|
|
143
|
+
* The spec makes this a **MAY**, not a MUST (`site/content/docs/protocol/effects.mdx → Bound vs referenced`):
|
|
144
|
+
* what's normative is that superseded content is never *served* or stitched, which leaves it with
|
|
145
|
+
* no legal reader. Dropping it is therefore free of consequence, and this node drops eagerly — on
|
|
146
|
+
* each bound registration — because that is what turns "held bytes" into
|
|
147
|
+
* `cap × minted × bound outputs` rather than a number that grows with every param change. A node
|
|
148
|
+
* that chose to keep history would be equally conforming and simply carry the cost.
|
|
149
|
+
*
|
|
150
|
+
* The rows themselves survive as provenance; only the unreadable content goes.
|
|
151
|
+
*/
|
|
152
|
+
pruneBoundArtifactBytes(address: string, tokenId: string, effectKey: string, outputKey: string, currentHash: string): void;
|
|
122
153
|
/** Merge a lifecycle patch for one project (preserve-on-omit, clear-on-`null`) and return the
|
|
123
154
|
* stored row. Creates the row on first write. */
|
|
124
155
|
setIndexStatus(address: string, patch: IndexStatusPatch): IndexStatusRow;
|
|
@@ -134,7 +165,12 @@ export interface Store {
|
|
|
134
165
|
/** Clear one artifact key's status — the run finished; artifact presence takes over as truth. */
|
|
135
166
|
clearEffectStatus(key: string): void;
|
|
136
167
|
listEffectStatuses(address: string): EffectStatusRow[];
|
|
137
|
-
|
|
168
|
+
/** Persist a folded projection. `fullReplay: true` (the repair path — `abx index --full`) purges
|
|
169
|
+
* the project's append-only event log first, so a reorg-replaced event can't survive as a stale
|
|
170
|
+
* row; routine incremental writes leave the log append-only. */
|
|
171
|
+
putProject(state: ProjectState, opts?: {
|
|
172
|
+
fullReplay?: boolean;
|
|
173
|
+
}): void;
|
|
138
174
|
getProject(address: string): ProjectState | null;
|
|
139
175
|
listProjects(): ProjectState[];
|
|
140
176
|
/** Drop the rebuildable projection (keeps registrations). Proves replay. */
|
|
@@ -167,12 +203,31 @@ export declare class SqliteStore implements Store {
|
|
|
167
203
|
/** Idempotently add columns introduced after a DB was first created (CREATE IF NOT
|
|
168
204
|
* EXISTS won't backfill them). Keeps an existing projection working across upgrades. */
|
|
169
205
|
private migrate;
|
|
206
|
+
/**
|
|
207
|
+
* One-time data migration, gated on `PRAGMA user_version` (unused before this — starts at 0 on
|
|
208
|
+
* every existing store). `migrate()` above only ever ADDs columns; recomputing `seq` rewrites
|
|
209
|
+
* VALUES in an existing column, which a `PRAGMA table_info` presence check can't gate — hence a
|
|
210
|
+
* separate versioned step.
|
|
211
|
+
*
|
|
212
|
+
* `events.seq` used to be the array index at write time (positional — renumbers on every re-fold).
|
|
213
|
+
* This recomputes every existing row to the chain-derived value `(block_number << 32) | log_index`
|
|
214
|
+
* — the same formula {@link eventSeq} uses going forward — so an upgraded store's history becomes
|
|
215
|
+
* append-only-compatible without re-indexing from chain. No collisions are possible: `(block,
|
|
216
|
+
* logIndex)` is already unique per project (the table's own history proves it — it was a valid
|
|
217
|
+
* event log before this ran), and the shift preserves that uniqueness.
|
|
218
|
+
*
|
|
219
|
+
* A fresh install has no rows yet, so the `UPDATE` is a no-op — but `user_version` still advances,
|
|
220
|
+
* so this never re-scans an empty table on every open. Wrapped in a transaction: a crash mid-way
|
|
221
|
+
* leaves `user_version` at 0 and retries the whole thing next open, never a half-migrated table.
|
|
222
|
+
*/
|
|
223
|
+
private migrateSeq;
|
|
170
224
|
register(reg: ProjectRegistration): void;
|
|
171
225
|
listRegistrations(): ProjectRegistration[];
|
|
172
226
|
getRegistration(address: string): ProjectRegistration | null;
|
|
173
227
|
dropProjection(address: string): void;
|
|
174
228
|
deregister(address: string): void;
|
|
175
229
|
putEffectArtifact(row: EffectArtifactRow): void;
|
|
230
|
+
pruneBoundArtifactBytes(address: string, tokenId: string, effectKey: string, outputKey: string, currentHash: string): void;
|
|
176
231
|
getEffectArtifact(key: string): EffectArtifactRow | null;
|
|
177
232
|
listEffectArtifacts(address: string, tokenId: string): EffectArtifactRow[];
|
|
178
233
|
putMeta(key: string, value: string): void;
|
|
@@ -189,11 +244,78 @@ export declare class SqliteStore implements Store {
|
|
|
189
244
|
putEffectStatus(row: EffectStatusRow): void;
|
|
190
245
|
clearEffectStatus(key: string): void;
|
|
191
246
|
listEffectStatuses(address: string): EffectStatusRow[];
|
|
192
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Persist a reconstructed `ProjectState`. O(delta), not O(history): the routine case — one new
|
|
249
|
+
* mint on an otherwise-unchanged project — writes one project row and one token row, appends
|
|
250
|
+
* whatever events are new, and touches nothing else. A delete-and-reinsert approach would rewrite
|
|
251
|
+
* the entire projection (every token, every event) on every call.
|
|
252
|
+
*
|
|
253
|
+
* `contract_uri`/`token_uri` are bound NULL unconditionally regardless of what `state` carries —
|
|
254
|
+
* see the schema comments on those columns. This is deliberate even when a caller passed populated
|
|
255
|
+
* values (e.g. a reconstruct run with `readUriDocuments: true`): the store's job is never to cache
|
|
256
|
+
* a value with no settled state, only to serve it live when asked.
|
|
257
|
+
*/
|
|
258
|
+
putProject(state: ProjectState, opts?: {
|
|
259
|
+
fullReplay?: boolean;
|
|
260
|
+
}): void;
|
|
193
261
|
getProject(address: string): ProjectState | null;
|
|
194
262
|
listProjects(): ProjectState[];
|
|
195
263
|
wipeProjections(): void;
|
|
196
264
|
destroy(): void;
|
|
265
|
+
/**
|
|
266
|
+
* This store's actual `PRAGMA auto_vacuum` mode, right now. A store opened from a file created
|
|
267
|
+
* before `SCHEMA` started setting `PRAGMA auto_vacuum = INCREMENTAL` BEFORE its first `CREATE
|
|
268
|
+
* TABLE` (see the comment on that pragma above) is permanently stuck at `'none'` — the pragma
|
|
269
|
+
* silently no-ops on a database that already has tables — until {@link vacuumConvert} runs. This
|
|
270
|
+
* is the detection half of the two-part approach: every other read/write path in this class is
|
|
271
|
+
* indifferent to the mode, so nothing breaks for a store still at `'none'`; only reclamation cares.
|
|
272
|
+
*/
|
|
273
|
+
autoVacuumMode(): 'none' | 'full' | 'incremental';
|
|
274
|
+
/** Diagnostics behind `abx vacuum status`: {@link autoVacuumMode} alongside the raw page/freelist
|
|
275
|
+
* counts a caller (or a test) can watch move as maintenance runs. */
|
|
276
|
+
vacuumStats(): {
|
|
277
|
+
mode: 'none' | 'full' | 'incremental';
|
|
278
|
+
freelistPages: number;
|
|
279
|
+
pageCount: number;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* The one-time, EXPLICIT conversion for a store stuck at `auto_vacuum='none'`: sets the pragma —
|
|
283
|
+
* on its own a no-op here, same as it was at construction, since the database already has tables
|
|
284
|
+
* — and then runs a full `VACUUM`, the only operation that actually rebuilds the file under the
|
|
285
|
+
* new mode. This is deliberately not run automatically anywhere in this class or in
|
|
286
|
+
* {@link SelfHostIndexer}: a full `VACUUM` rewrites the ENTIRE database file and can briefly need
|
|
287
|
+
* up to ~2x its on-disk size while it runs, so it must be an operator's explicit choice, made with
|
|
288
|
+
* that cost understood — `abx vacuum convert` (and the docs it points at) is that explicit surface.
|
|
289
|
+
* A no-op, returning the mode unchanged, once the store is already `'full'` or `'incremental'`.
|
|
290
|
+
*/
|
|
291
|
+
vacuumConvert(): 'none' | 'full' | 'incremental';
|
|
292
|
+
/**
|
|
293
|
+
* Bounded `PRAGMA incremental_vacuum(N)`: reclaims at most `maxPages` freed pages THIS call, never
|
|
294
|
+
* the whole freelist at once. `node:sqlite`'s `DatabaseSync` is SYNCHRONOUS and shares this process
|
|
295
|
+
* with token-api reads on a co-located `abx serve` — an unbounded reclaim would block every request
|
|
296
|
+
* in flight for however long a large freelist takes to drain. Bounding it is what makes it safe to
|
|
297
|
+
* call automatically between watch-loop ticks (see {@link SelfHostIndexer.runIncrementalVacuum})
|
|
298
|
+
* rather than only from the explicit `abx vacuum convert` command above.
|
|
299
|
+
*
|
|
300
|
+
* A no-op (returns 0, touches nothing) before the store has been converted to `'incremental'` mode
|
|
301
|
+
* — `incremental_vacuum` only does anything in that mode, and running it blind on a `'none'` store
|
|
302
|
+
* would be silent maintenance with no effect, which is worse than an honest no-op.
|
|
303
|
+
*
|
|
304
|
+
* Returns the number of pages actually REQUESTED of SQLite this call (capped by the freelist's
|
|
305
|
+
* current size), not a guarantee it moved exactly that many — draining an already-small freelist
|
|
306
|
+
* legitimately returns fewer than `maxPages`.
|
|
307
|
+
*/
|
|
308
|
+
runIncrementalVacuum(maxPages: number): number;
|
|
309
|
+
/**
|
|
310
|
+
* Drop one project's projection — `extensions` / `tokens` / `projects` — but deliberately NOT
|
|
311
|
+
* `events`. The event log is append-only and chain-derived (`seq` is `(block << 32) | logIndex`,
|
|
312
|
+
* so a re-fold always lands on the same rows): a re-index after this re-derives the identical
|
|
313
|
+
* event set from chain and re-inserts it as a no-op (`ON CONFLICT DO NOTHING`), so nothing is lost
|
|
314
|
+
* by leaving the rows in place, and `abx demo`'s "drop + replay" proof doesn't depend on the log
|
|
315
|
+
* being gone. `deregister`/forget-the-project-entirely paths delete events explicitly — this
|
|
316
|
+
* helper is for `dropProjection`, which keeps the registration precisely so the next reindex can
|
|
317
|
+
* rebuild from it.
|
|
318
|
+
*/
|
|
197
319
|
private deleteProjection;
|
|
198
320
|
private hydrate;
|
|
199
321
|
}
|