@artblocks/abx-effects 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Art Blocks, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,177 @@
1
+ import { type Server } from 'node:http';
2
+ import { type Hex, type ProjectState, type PublicClient, type TokenState, type TokenDataResult } from '@artblocks/abx-sdk';
3
+ import type { StorageBackend } from '@artblocks/abx-storage';
4
+ /**
5
+ * The reference effect runner (`specs/protocol/effects.md → The reference runner`).
6
+ *
7
+ * An effect is a spine subscriber, so its natural form is a deployed, long-running
8
+ * service. This harness is effect-agnostic: an effect registers `{key, outputs, run}`;
9
+ * the harness supplies triggers, the queue, idempotency, and storage. It is
10
+ * **stateless by construction** — "is there work?" reduces to "does custody lack the
11
+ * artifact at the CURRENT inputsHash address?" — so there is no job database, a
12
+ * crash-restart re-derives everything, and duplicate runners are harmless (same
13
+ * inputs → same address → idempotent writes).
14
+ *
15
+ * Trigger lanes (both converge on the same sweep): a periodic sweep (the floor), and
16
+ * an HTTP ping (`POST /run {address, tokenIds?}`) the co-deployed indexer/resolver —
17
+ * or the `abx render` repair command — hits for immediacy.
18
+ */
19
+ export interface EffectOutput {
20
+ bytes: Uint8Array;
21
+ contentType: string;
22
+ }
23
+ export interface EffectContext {
24
+ client: PublicClient | null;
25
+ state: ProjectState;
26
+ token: TokenState;
27
+ tokenData: TokenDataResult;
28
+ /** The live-view URL — the same document a collector sees; the render input. */
29
+ liveViewUrl: string;
30
+ }
31
+ /** One declared output: the key naming the artifact + its MIME type, declared where the effect is
32
+ * declared (`specs/protocol/data-plane.md → Declared type, never sniffed`). The declared type is
33
+ * what the manifest and the serving surface speak; the per-run `EffectOutput.contentType` still
34
+ * travels with the bytes (a mismatch is logged, the declaration wins). */
35
+ export interface EffectOutputDecl {
36
+ key: string;
37
+ mimeType: string;
38
+ }
39
+ export interface EffectModule {
40
+ /** Lowercase dot-namespaced effect key (`render`, `inference.caption`, …). */
41
+ key: string;
42
+ /** Typed output declarations; `outputs[0].key` is the presence probe (idempotency). */
43
+ outputs: EffectOutputDecl[];
44
+ run(ctx: EffectContext): Promise<Record<string, EffectOutput | null>>;
45
+ }
46
+ /** One artifact registration — the row a runner records (co-located, via `recordArtifact`) or
47
+ * publishes (`/admin/effect-artifacts`) per declared output, so the resolver can enumerate the
48
+ * token's data plane without learning any effect's internals. `locator === null` means the bytes
49
+ * live in custody at `key` (shared disk, or published bytes held by the resolver). */
50
+ export interface EffectArtifactRecord {
51
+ key: Hex;
52
+ address: string;
53
+ tokenId: string;
54
+ effectKey: string;
55
+ outputKey: string;
56
+ inputsHash: Hex;
57
+ /** The DECLARED mimeType (`EffectOutputDecl.mimeType`) — what the manifest serves. */
58
+ contentType: string;
59
+ locator: string | null;
60
+ }
61
+ export interface RunnerOptions {
62
+ resolverUrl: string;
63
+ client: PublicClient | null;
64
+ storage: StorageBackend;
65
+ effects: EffectModule[];
66
+ environmentId?: string;
67
+ /** When set, publish each stored artifact to the resolver's admin control plane
68
+ * (POST /admin/effect-artifacts) so a resolver that does NOT share this backend's disk can serve it
69
+ * — a durable locator (ipfs/ar) when the backend exposes one, else the raw bytes. Bearer only;
70
+ * never signs on-chain. Omit for the co-located topology (runner + resolver share one backend). */
71
+ adminToken?: string;
72
+ /** Also (re)publish artifacts that already exist in storage (not just freshly-rendered ones) — the
73
+ * idempotent republish that restores a resolver which lost its volume. Requires `adminToken`. */
74
+ republish?: boolean;
75
+ /** Co-located topology: record each declared output into the resolver's artifact registry
76
+ * directly (the shared SQLite store) — the enumeration surface the manifest lists. Recorded on
77
+ * every run AND on every skip (cheap local upsert), so a wiped registry self-heals on the next
78
+ * sweep without re-rendering. The hosted twin is `adminToken` (publish over HTTP). */
79
+ recordArtifact?: (row: EffectArtifactRecord) => void | Promise<void>;
80
+ /** Test seam: state source override (defaults to the resolver's public state API). */
81
+ fetchState?: (address: string) => Promise<ProjectState | null>;
82
+ log?: (line: string) => void;
83
+ /** Parallel renders while draining the queue. Default 1 — deterministic order, and Chromium is
84
+ * heavy on small VMs; raise deliberately (ABX_EFFECTS_CONCURRENCY). */
85
+ concurrency?: number;
86
+ /** When set, `/run` and `/notify` require `Authorization: Bearer <token>` — REQUIRED for a
87
+ * publicly-reachable runner (an open /run with force would let anyone burn Chromium/storage).
88
+ * `/health` stays open. (ABX_EFFECTS_TOKEN; the resolver's watcher/ping sends it.) */
89
+ authToken?: string;
90
+ }
91
+ export interface SweepStats {
92
+ ran: number;
93
+ skipped: number;
94
+ failed: number;
95
+ /** Human-readable failure lines (effect + token + message) — surfaced by `abx render`. */
96
+ errors: string[];
97
+ }
98
+ export declare class EffectRunner {
99
+ private readonly opts;
100
+ private readonly log;
101
+ /** Per-artifact-key failed-run counter (see MAX_ATTEMPTS). */
102
+ private readonly attempts;
103
+ /** The notification queue: per-project pending work, merged on enqueue (tokenIds union;
104
+ * `null` = all minted). The REAL work ledger stays "artifacts missing at the current settled
105
+ * inputsHash" — this map only holds what we've been told about and not yet probed, so losing
106
+ * it (restart) costs nothing: the floor sweep re-derives everything. */
107
+ private readonly pending;
108
+ private draining;
109
+ private idleWaiters;
110
+ constructor(opts: RunnerOptions);
111
+ /** Merge a notification into the queue and kick the drain. Returns the queue depth. */
112
+ enqueue(address: string, tokenIds?: string[], force?: boolean): number;
113
+ /** Resolves when the queue is fully drained (test/ops seam). */
114
+ idle(): Promise<void>;
115
+ /** Single drain loop: one project at a time (notifications for other projects merge while a
116
+ * drain runs), tokens in ascending order (token 0 — the collection page — first), renders
117
+ * fanned across `concurrency` workers. Over-notification lands on the settled-hash probe and
118
+ * costs a skip, never a re-render. */
119
+ private drain;
120
+ /** All projects the resolver serves → sweep each code project. */
121
+ sweepAll(): Promise<SweepStats>;
122
+ /** Sweep one project: run every effect for every token whose current output is missing.
123
+ * `opts.force` re-renders even when the current output already exists (the repair lane for a
124
+ * bad/blank/timed-out capture — the art is otherwise deterministic, so a normal sweep skips it). */
125
+ sweepProject(address: string, tokenIds?: string[], opts?: {
126
+ force?: boolean;
127
+ }): Promise<SweepStats>;
128
+ /** One (effect, token): compute the current address; run only when the output is missing —
129
+ * unless `force` re-renders and overwrites the artifact at the current inputsHash. */
130
+ runToken(effect: EffectModule, state: ProjectState, token: TokenState, force?: boolean): Promise<'ran' | 'skipped'>;
131
+ /** Best-effort run-state report to the resolver's observability plane (`/admin/effect-status`).
132
+ * No admin token (co-located) ⇒ no-op: status degrades to derived up-to-date|stale; correctness
133
+ * is never here — it stays artifact-presence at the settled inputsHash. */
134
+ private reportStatus;
135
+ /**
136
+ * Publish one stored output to the resolver's admin control plane so a node that doesn't share this
137
+ * backend's disk can serve it. Inline-bound outputs (`INLINE_OUTPUTS` — they stitch into the
138
+ * metadata JSON, so the resolver needs the content, not a redirect) → always bytes. Everything
139
+ * else → a durable locator when the backend exposes one (ipfs/ar), so the resolver 302-redirects;
140
+ * else raw bytes. `contentType` is the DECLARED mimeType.
141
+ */
142
+ private publishArtifact;
143
+ /** Record one artifact row into the co-located registry (`RunnerOptions.recordArtifact`). */
144
+ private record;
145
+ /** Re-publish already-stored outputs for (effect, token) at the current inputsHash — no re-render.
146
+ * Restores a resolver that lost its volume (the bytes still live in this backend). */
147
+ private republishFromStorage;
148
+ /** Re-record registry rows for outputs already stored at the current inputsHash — presence probes
149
+ * only, no byte reads, no re-render. The deterministic-image lane records its stable public URL
150
+ * (its sidecar marker already matched, or we wouldn't be in the skip path). */
151
+ private recordFromStorage;
152
+ /** The trigger surface + health. Two write lanes, distinct on purpose:
153
+ * - `POST /notify {address, tokenIds?}` — the NOTIFICATION lane (the resolver's watcher):
154
+ * "something changed here". Enqueues + 202 immediately; the queue drains async. Coarse and
155
+ * effect-agnostic — never says what to do.
156
+ * - `POST /run {address, tokenIds?, force?}` — the COMMAND lane (`abx render --effects-url`):
157
+ * synchronous sweep, returns the stats.
158
+ * Both require `Authorization: Bearer <authToken>` when one is configured (a public runner
159
+ * with an open force-render endpoint is a resource-burn hole); `/health` stays open. */
160
+ startHttp(port: number): Server;
161
+ /** The sweep loop — the trigger floor. Pings make it immediate; this makes it eventual. */
162
+ startLoop(intervalMs: number): NodeJS.Timeout;
163
+ private artifactKey;
164
+ /**
165
+ * The deterministic-thumbnail target for (project, token), or null if this project/backend isn't
166
+ * on that lane. Active only when the backend both serves a public base and can write an arbitrary
167
+ * key (cloud/S3), AND the project's on-chain collection `image` is a `url-template` whose
168
+ * substituted URL lives under that public base. Then the still is written to the exact object the
169
+ * on-chain pointer names (`<publicBase>/<objectKey>`) — marketplaces read the on-chain tokenURI →
170
+ * this URL, never a resolver, and the URL is stable across re-renders (the pixels change, the
171
+ * pointer doesn't → no chain write to update art). Any other host → null (the normal locator lane).
172
+ */
173
+ private deterministicImageTarget;
174
+ private getState;
175
+ private fetchJson;
176
+ }
177
+ //# sourceMappingURL=harness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,466 @@
1
+ import { createServer } from 'node:http';
2
+ import { hexToString } from 'viem';
3
+ import { buildTokenData, contentDigestOf, inputsHash, isCodeProject, renderArtifactKey, } from '@artblocks/abx-sdk';
4
+ /** Give up on an artifact key after this many failed runs (a deterministically-crashing script
5
+ * must not hot-loop). In-memory: a runner restart, a NEW inputsHash (the state changed), or an
6
+ * explicit `force` all reset it. The failure itself is reported to the resolver with the error. */
7
+ const MAX_ATTEMPTS = 3;
8
+ /** Outputs that BIND into the metadata JSON itself (the resolver needs the content, not a
9
+ * redirect) — published/recorded as bytes, never as a locator. Everything else prefers the
10
+ * backend's durable locator (ipfs/ar/https) and falls back to bytes. */
11
+ const INLINE_OUTPUTS = new Set(['traits']);
12
+ export class EffectRunner {
13
+ opts;
14
+ log;
15
+ /** Per-artifact-key failed-run counter (see MAX_ATTEMPTS). */
16
+ attempts = new Map();
17
+ /** The notification queue: per-project pending work, merged on enqueue (tokenIds union;
18
+ * `null` = all minted). The REAL work ledger stays "artifacts missing at the current settled
19
+ * inputsHash" — this map only holds what we've been told about and not yet probed, so losing
20
+ * it (restart) costs nothing: the floor sweep re-derives everything. */
21
+ pending = new Map();
22
+ draining = false;
23
+ idleWaiters = [];
24
+ constructor(opts) {
25
+ this.opts = opts;
26
+ this.log = opts.log ?? ((line) => console.log(`[effects] ${line}`));
27
+ }
28
+ /** Merge a notification into the queue and kick the drain. Returns the queue depth. */
29
+ enqueue(address, tokenIds, force = false) {
30
+ const key = address.toLowerCase();
31
+ const cur = this.pending.get(key) ?? { tokenIds: new Set(), force: false };
32
+ if (!tokenIds?.length || cur.tokenIds === null)
33
+ cur.tokenIds = null; // no hint ⇒ all minted
34
+ else
35
+ for (const t of tokenIds)
36
+ cur.tokenIds.add(t);
37
+ cur.force = cur.force || force;
38
+ this.pending.set(key, cur);
39
+ void this.drain();
40
+ return this.pending.size;
41
+ }
42
+ /** Resolves when the queue is fully drained (test/ops seam). */
43
+ idle() {
44
+ if (!this.draining && this.pending.size === 0)
45
+ return Promise.resolve();
46
+ return new Promise((resolve) => this.idleWaiters.push(resolve));
47
+ }
48
+ /** Single drain loop: one project at a time (notifications for other projects merge while a
49
+ * drain runs), tokens in ascending order (token 0 — the collection page — first), renders
50
+ * fanned across `concurrency` workers. Over-notification lands on the settled-hash probe and
51
+ * costs a skip, never a re-render. */
52
+ async drain() {
53
+ if (this.draining)
54
+ return;
55
+ this.draining = true;
56
+ try {
57
+ for (;;) {
58
+ const next = this.pending.entries().next();
59
+ if (next.done)
60
+ break;
61
+ const [address, req] = next.value;
62
+ this.pending.delete(address);
63
+ try {
64
+ const stats = await this.sweepProject(address, req.tokenIds ? [...req.tokenIds] : undefined, {
65
+ force: req.force,
66
+ });
67
+ if (stats.ran || stats.failed) {
68
+ this.log(`queue: ${address} ran=${stats.ran} skipped=${stats.skipped} failed=${stats.failed}`);
69
+ }
70
+ }
71
+ catch (err) {
72
+ this.log(`queue: ${address} drain failed: ${err.message}`);
73
+ }
74
+ }
75
+ }
76
+ finally {
77
+ this.draining = false;
78
+ for (const resolve of this.idleWaiters.splice(0))
79
+ resolve();
80
+ }
81
+ }
82
+ /** All projects the resolver serves → sweep each code project. */
83
+ async sweepAll() {
84
+ const total = { ran: 0, skipped: 0, failed: 0, errors: [] };
85
+ const projects = (await this.fetchJson(`${this.opts.resolverUrl}/api/projects`));
86
+ for (const p of projects ?? []) {
87
+ const s = await this.sweepProject(p.address);
88
+ total.ran += s.ran;
89
+ total.skipped += s.skipped;
90
+ total.failed += s.failed;
91
+ total.errors.push(...s.errors);
92
+ }
93
+ return total;
94
+ }
95
+ /** Sweep one project: run every effect for every token whose current output is missing.
96
+ * `opts.force` re-renders even when the current output already exists (the repair lane for a
97
+ * bad/blank/timed-out capture — the art is otherwise deterministic, so a normal sweep skips it). */
98
+ async sweepProject(address, tokenIds, opts) {
99
+ const stats = { ran: 0, skipped: 0, failed: 0, errors: [] };
100
+ const state = await this.getState(address);
101
+ if (!state || !isCodeProject(state))
102
+ return stats;
103
+ const tokens = (tokenIds ? state.tokens.filter((t) => tokenIds.includes(t.tokenId)) : state.tokens.filter((t) => t.minted))
104
+ // ascending — token 0 (the collection page / oldest listings) lands first on a big fan-out
105
+ .sort((a, b) => (BigInt(a.tokenId) < BigInt(b.tokenId) ? -1 : 1));
106
+ // An explicit token id that matched nothing minted is a no-op that must SAY so — not a silent
107
+ // `ran=0` that reads as "already done". (Over-notification from the watcher passes ids too, but
108
+ // it never names a specific unminted id, so this only fires on a deliberate `abx render <addr> <id>`.)
109
+ if (tokenIds?.length && tokens.length === 0) {
110
+ const line = `${address}: token(s) ${tokenIds.join(',')} not minted / out of range — nothing to render`;
111
+ stats.errors.push(line);
112
+ this.log(line);
113
+ }
114
+ const work = tokens.flatMap((token) => this.opts.effects.map((effect) => ({ token, effect })));
115
+ // Bounded worker pool (default 1): a contract-scope param change fans out to EVERY minted
116
+ // token — drain at a deliberate width instead of serially-forever or 1000 Chromiums at once.
117
+ const width = Math.max(1, Math.min(this.opts.concurrency ?? 1, work.length || 1));
118
+ let cursor = 0;
119
+ const worker = async () => {
120
+ for (;;) {
121
+ const item = work[cursor++];
122
+ if (!item)
123
+ return;
124
+ try {
125
+ const outcome = await this.runToken(item.effect, state, item.token, opts?.force ?? false);
126
+ stats[outcome] += 1;
127
+ }
128
+ catch (err) {
129
+ stats.failed += 1;
130
+ const line = `${item.effect.key} ${address}/${item.token.tokenId}: ${err.message}`;
131
+ stats.errors.push(line);
132
+ this.log(`${line} (failed)`);
133
+ }
134
+ }
135
+ };
136
+ await Promise.all(Array.from({ length: width }, worker));
137
+ return stats;
138
+ }
139
+ /** One (effect, token): compute the current address; run only when the output is missing —
140
+ * unless `force` re-renders and overwrites the artifact at the current inputsHash. */
141
+ async runToken(effect, state, token, force = false) {
142
+ // SETTLED tokenData (augment: false): effect outputs are addressed by event-derived state
143
+ // only. A volatile augment hook (block timestamp, oracle) feeds the LIVE VIEW — the capture
144
+ // snapshots whatever it shows — but must never re-address the artifact every block. An effect
145
+ // that reads live data itself owns its own addressing (fold it into the output key, or accept
146
+ // re-runs per trigger).
147
+ const tokenData = await buildTokenData(this.opts.client, state, token, { augment: false });
148
+ const hash = inputsHash(contentDigestOf(state), tokenData.json, this.opts.environmentId);
149
+ const probeKey = this.artifactKey(state, token, hash, effect, effect.outputs[0].key);
150
+ // Deterministic-image lane: if this project's on-chain `image` is a url-template pointing at
151
+ // THIS backend's public base, the still lands at a STABLE per-token key the chain already names
152
+ // (`<publicBase>/<key>`), overwritten in place — no resolver in the marketplace path. Idempotency
153
+ // rides a sidecar `.abxhash` marker instead of the hash-embedded object key.
154
+ const detImg = this.deterministicImageTarget(state, token);
155
+ const currentAtHash = detImg
156
+ ? new TextDecoder().decode((await this.opts.storage.getObject?.(`${detImg.objectKey}.abxhash`))?.bytes ?? new Uint8Array()) === hash
157
+ : await this.opts.storage.has(probeKey);
158
+ if (!force && currentAtHash) {
159
+ // current output exists — idempotent (no re-render). Republish from storage on request so a
160
+ // resolver that lost its volume can be restored without re-rendering (RunnerOptions.republish).
161
+ // (No republish in the deterministic lane: the bytes already live at the public S3 key.)
162
+ if (!detImg && this.opts.adminToken && this.opts.republish)
163
+ await this.republishFromStorage(effect, state, token, hash);
164
+ // Registry repair lane: re-record rows for whatever is stored at the current hash — a cheap
165
+ // local upsert per output, so a wiped artifact registry self-heals on the next sweep.
166
+ if (this.opts.recordArtifact)
167
+ await this.recordFromStorage(effect, state, token, hash, detImg);
168
+ return 'skipped';
169
+ }
170
+ // Attempt cap: a run that keeps failing at THIS key stops retrying (a new inputsHash or an
171
+ // explicit force resets it; the resolver holds the reported failure + error meanwhile).
172
+ if (!force && (this.attempts.get(probeKey) ?? 0) >= MAX_ATTEMPTS) {
173
+ return 'skipped';
174
+ }
175
+ if (force)
176
+ this.attempts.delete(probeKey);
177
+ const ctx = {
178
+ client: this.opts.client,
179
+ state,
180
+ token,
181
+ tokenData,
182
+ liveViewUrl: `${this.opts.resolverUrl}/a/${state.chainId}/${state.address}/${token.tokenId}`,
183
+ };
184
+ await this.reportStatus(probeKey, state, token, effect, 'rendering');
185
+ let outputs;
186
+ try {
187
+ outputs = await effect.run(ctx);
188
+ }
189
+ catch (err) {
190
+ const n = (this.attempts.get(probeKey) ?? 0) + 1;
191
+ this.attempts.set(probeKey, n);
192
+ await this.reportStatus(probeKey, state, token, effect, 'failed', err.message, n);
193
+ throw err;
194
+ }
195
+ for (const [outputKey, output] of Object.entries(outputs)) {
196
+ if (!output)
197
+ continue; // optional output (e.g. a script that reported no traits)
198
+ // Declared-type enforcement (`data-plane.md → Declared type, never sniffed`): an undeclared
199
+ // output has no type to serve — refuse it loudly rather than sniff or guess.
200
+ const decl = effect.outputs.find((o) => o.key === outputKey);
201
+ if (!decl) {
202
+ this.log(`${effect.key} ${state.address}/${token.tokenId}: undeclared output '${outputKey}' NOT stored — declare it in EffectModule.outputs with its mimeType`);
203
+ continue;
204
+ }
205
+ if (output.contentType !== decl.mimeType) {
206
+ this.log(`${effect.key}/${outputKey}: runtime contentType '${output.contentType}' ≠ declared '${decl.mimeType}' — the declaration wins`);
207
+ }
208
+ const key = this.artifactKey(state, token, hash, effect, outputKey);
209
+ if (outputKey === 'image' && detImg) {
210
+ // Overwrite the stable per-token object + its inputsHash marker. The on-chain url-template
211
+ // is the authoritative image URL, so there is NO resolver locator publish for the image —
212
+ // but the registry row still lands (locator = the public URL) so the manifest lists it.
213
+ await this.opts.storage.putObject(detImg.objectKey, { bytes: output.bytes, contentType: decl.mimeType });
214
+ await this.opts.storage.putObject(`${detImg.objectKey}.abxhash`, { bytes: new TextEncoder().encode(hash), contentType: 'text/plain' });
215
+ if (this.opts.recordArtifact)
216
+ await this.record(state, token, hash, effect, decl, key, detImg.url);
217
+ this.log(`image ${state.address}/${token.tokenId} → ${detImg.url} (deterministic; ${output.bytes.length}B)`);
218
+ continue;
219
+ }
220
+ await this.opts.storage.put(key, { bytes: output.bytes, contentType: decl.mimeType });
221
+ if (this.opts.recordArtifact)
222
+ await this.record(state, token, hash, effect, decl, key);
223
+ if (this.opts.adminToken)
224
+ await this.publishArtifact(state, token, hash, effect, decl, { bytes: output.bytes, contentType: decl.mimeType }, key);
225
+ }
226
+ this.attempts.delete(probeKey);
227
+ await this.reportStatus(probeKey, state, token, effect, 'done');
228
+ this.log(`${effect.key} ${state.address}/${token.tokenId} → ${hash.slice(0, 10)}…`);
229
+ return 'ran';
230
+ }
231
+ /** Best-effort run-state report to the resolver's observability plane (`/admin/effect-status`).
232
+ * No admin token (co-located) ⇒ no-op: status degrades to derived up-to-date|stale; correctness
233
+ * is never here — it stays artifact-presence at the settled inputsHash. */
234
+ async reportStatus(key, state, token, effect, status, error, attempts) {
235
+ if (!this.opts.adminToken)
236
+ return;
237
+ try {
238
+ await fetch(`${this.opts.resolverUrl}/admin/effect-status`, {
239
+ method: 'POST',
240
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${this.opts.adminToken}` },
241
+ body: JSON.stringify({ key, address: state.address, tokenId: token.tokenId, effectKey: effect.key, status, error, attempts }),
242
+ });
243
+ }
244
+ catch {
245
+ // observability only — a failed report never fails the run
246
+ }
247
+ }
248
+ /**
249
+ * Publish one stored output to the resolver's admin control plane so a node that doesn't share this
250
+ * backend's disk can serve it. Inline-bound outputs (`INLINE_OUTPUTS` — they stitch into the
251
+ * metadata JSON, so the resolver needs the content, not a redirect) → always bytes. Everything
252
+ * else → a durable locator when the backend exposes one (ipfs/ar), so the resolver 302-redirects;
253
+ * else raw bytes. `contentType` is the DECLARED mimeType.
254
+ */
255
+ async publishArtifact(state, token, hash, effect, decl, output, key) {
256
+ const locator = INLINE_OUTPUTS.has(decl.key) ? null : ((await this.opts.storage.locator?.(key)) ?? null);
257
+ const body = {
258
+ address: state.address,
259
+ tokenId: token.tokenId,
260
+ inputsHash: hash,
261
+ output: decl.key,
262
+ effectKey: effect.key,
263
+ contentType: decl.mimeType,
264
+ };
265
+ if (locator)
266
+ body.locator = locator;
267
+ else
268
+ body.bytes_base64 = Buffer.from(output.bytes).toString('base64');
269
+ const res = await fetch(`${this.opts.resolverUrl}/admin/effect-artifacts`, {
270
+ method: 'POST',
271
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${this.opts.adminToken}` },
272
+ body: JSON.stringify(body),
273
+ });
274
+ if (!res.ok) {
275
+ throw new Error(`publish ${decl.key} ${state.address}/${token.tokenId} → ${res.status} ${(await res.text().catch(() => '')).slice(0, 200)}`);
276
+ }
277
+ this.log(`published ${decl.key} ${state.address}/${token.tokenId} (${locator ? `locator ${locator}` : `${output.bytes.length}B`})`);
278
+ }
279
+ /** Record one artifact row into the co-located registry (`RunnerOptions.recordArtifact`). */
280
+ async record(state, token, hash, effect, decl, key, locatorOverride) {
281
+ const locator = locatorOverride ??
282
+ (INLINE_OUTPUTS.has(decl.key) ? null : ((await this.opts.storage.locator?.(key)) ?? null));
283
+ await this.opts.recordArtifact({
284
+ key,
285
+ address: state.address.toLowerCase(),
286
+ tokenId: token.tokenId,
287
+ effectKey: effect.key,
288
+ outputKey: decl.key,
289
+ inputsHash: hash,
290
+ contentType: decl.mimeType,
291
+ locator,
292
+ });
293
+ }
294
+ /** Re-publish already-stored outputs for (effect, token) at the current inputsHash — no re-render.
295
+ * Restores a resolver that lost its volume (the bytes still live in this backend). */
296
+ async republishFromStorage(effect, state, token, hash) {
297
+ for (const decl of effect.outputs) {
298
+ const key = this.artifactKey(state, token, hash, effect, decl.key);
299
+ const stored = await this.opts.storage.get(key);
300
+ if (stored)
301
+ await this.publishArtifact(state, token, hash, effect, decl, stored, key);
302
+ }
303
+ }
304
+ /** Re-record registry rows for outputs already stored at the current inputsHash — presence probes
305
+ * only, no byte reads, no re-render. The deterministic-image lane records its stable public URL
306
+ * (its sidecar marker already matched, or we wouldn't be in the skip path). */
307
+ async recordFromStorage(effect, state, token, hash, detImg) {
308
+ for (const decl of effect.outputs) {
309
+ const key = this.artifactKey(state, token, hash, effect, decl.key);
310
+ if (decl.key === 'image' && detImg) {
311
+ await this.record(state, token, hash, effect, decl, key, detImg.url);
312
+ continue;
313
+ }
314
+ if (await this.opts.storage.has(key))
315
+ await this.record(state, token, hash, effect, decl, key);
316
+ }
317
+ }
318
+ /** The trigger surface + health. Two write lanes, distinct on purpose:
319
+ * - `POST /notify {address, tokenIds?}` — the NOTIFICATION lane (the resolver's watcher):
320
+ * "something changed here". Enqueues + 202 immediately; the queue drains async. Coarse and
321
+ * effect-agnostic — never says what to do.
322
+ * - `POST /run {address, tokenIds?, force?}` — the COMMAND lane (`abx render --effects-url`):
323
+ * synchronous sweep, returns the stats.
324
+ * Both require `Authorization: Bearer <authToken>` when one is configured (a public runner
325
+ * with an open force-render endpoint is a resource-burn hole); `/health` stays open. */
326
+ startHttp(port) {
327
+ const server = createServer(async (req, res) => {
328
+ try {
329
+ const url = new URL(req.url ?? '/', 'http://localhost');
330
+ if (url.pathname === '/health') {
331
+ res.writeHead(200, { 'content-type': 'application/json' });
332
+ return res.end(JSON.stringify({
333
+ ok: true,
334
+ effects: this.opts.effects.map((e) => ({ key: e.key, outputs: e.outputs })),
335
+ queued: this.pending.size,
336
+ }));
337
+ }
338
+ if (this.opts.authToken && req.headers.authorization !== `Bearer ${this.opts.authToken}`) {
339
+ res.writeHead(401, { 'content-type': 'application/json' });
340
+ return res.end(JSON.stringify({ error: 'unauthorized — send Authorization: Bearer <ABX_EFFECTS_TOKEN>' }));
341
+ }
342
+ if (req.method === 'POST' && url.pathname === '/notify') {
343
+ const body = JSON.parse(await readBody(req));
344
+ if (!body.address) {
345
+ res.writeHead(400, { 'content-type': 'application/json' });
346
+ return res.end(JSON.stringify({ error: 'address required' }));
347
+ }
348
+ const depth = this.enqueue(body.address, body.tokenIds, body.force ?? false);
349
+ res.writeHead(202, { 'content-type': 'application/json' });
350
+ return res.end(JSON.stringify({ queued: true, depth }));
351
+ }
352
+ if (req.method === 'POST' && url.pathname === '/run') {
353
+ const body = JSON.parse(await readBody(req));
354
+ if (!body.address) {
355
+ res.writeHead(400, { 'content-type': 'application/json' });
356
+ return res.end(JSON.stringify({ error: 'address required' }));
357
+ }
358
+ const stats = await this.sweepProject(body.address, body.tokenIds, { force: body.force });
359
+ res.writeHead(200, { 'content-type': 'application/json' });
360
+ return res.end(JSON.stringify(stats));
361
+ }
362
+ res.writeHead(404, { 'content-type': 'application/json' });
363
+ res.end(JSON.stringify({ error: 'unknown route' }));
364
+ }
365
+ catch (err) {
366
+ res.writeHead(500, { 'content-type': 'application/json' });
367
+ res.end(JSON.stringify({ error: err.message }));
368
+ }
369
+ });
370
+ server.listen(port);
371
+ return server;
372
+ }
373
+ /** The sweep loop — the trigger floor. Pings make it immediate; this makes it eventual. */
374
+ startLoop(intervalMs) {
375
+ let running = false;
376
+ return setInterval(async () => {
377
+ if (running)
378
+ return; // one sweep at a time; overlap is wasted work, never corruption
379
+ running = true;
380
+ try {
381
+ const s = await this.sweepAll();
382
+ if (s.ran || s.failed)
383
+ this.log(`sweep: ran=${s.ran} skipped=${s.skipped} failed=${s.failed}`);
384
+ }
385
+ catch (err) {
386
+ this.log(`sweep failed: ${err.message}`);
387
+ }
388
+ finally {
389
+ running = false;
390
+ }
391
+ }, intervalMs);
392
+ }
393
+ artifactKey(state, token, hash, effect, outputKey) {
394
+ return renderArtifactKey(state.chainId, state.address, token.tokenId, hash, outputKey, effect.key);
395
+ }
396
+ /**
397
+ * The deterministic-thumbnail target for (project, token), or null if this project/backend isn't
398
+ * on that lane. Active only when the backend both serves a public base and can write an arbitrary
399
+ * key (cloud/S3), AND the project's on-chain collection `image` is a `url-template` whose
400
+ * substituted URL lives under that public base. Then the still is written to the exact object the
401
+ * on-chain pointer names (`<publicBase>/<objectKey>`) — marketplaces read the on-chain tokenURI →
402
+ * this URL, never a resolver, and the URL is stable across re-renders (the pixels change, the
403
+ * pointer doesn't → no chain write to update art). Any other host → null (the normal locator lane).
404
+ */
405
+ deterministicImageTarget(state, token) {
406
+ const publicBase = this.opts.storage.publicBase;
407
+ if (!publicBase || !this.opts.storage.putObject || !this.opts.storage.getObject)
408
+ return null;
409
+ const img = state.collectionFields.find((f) => f.field === 'image' && f.representation === 'url-template');
410
+ if (!img)
411
+ return null;
412
+ let template;
413
+ try {
414
+ template = hexToString(img.value);
415
+ }
416
+ catch {
417
+ return null;
418
+ }
419
+ const url = template.split('{id}').join(token.tokenId);
420
+ const prefix = `${publicBase}/`;
421
+ if (!url.startsWith(prefix))
422
+ return null; // template points at a different host — leave it to the normal path
423
+ return { objectKey: url.slice(prefix.length), url };
424
+ }
425
+ async getState(address) {
426
+ if (this.opts.fetchState)
427
+ return this.opts.fetchState(address);
428
+ return (await this.fetchJson(`${this.opts.resolverUrl}/api/project/${address}`));
429
+ }
430
+ async fetchJson(url) {
431
+ // Retry transient failures — a hosted resolver can cold-start / briefly 502 on fly, and a
432
+ // one-shot `abx render --remote` must not silently no-op against one that's just waking up.
433
+ // Two OUTCOMES, deliberately distinct: a genuine 4xx (e.g. 404 unknown project) → `null`, so
434
+ // the caller SKIPS cleanly (there is nothing to do). A network failure or a 5xx/429 that
435
+ // survives every retry means the resolver is UNREACHABLE → we THROW, so the caller reports a
436
+ // real failure instead of a clean `ran=0 failed=0`. (The old code returned null for both, so a
437
+ // killed/unreachable resolver read as benign "nothing to do" — a failure disguised as success.)
438
+ let lastErr;
439
+ for (let attempt = 0; attempt < 4; attempt++) {
440
+ try {
441
+ const res = await fetch(url);
442
+ if (res.ok)
443
+ return await res.json();
444
+ if (res.status < 500 && res.status !== 429)
445
+ return null;
446
+ lastErr = new Error(`HTTP ${res.status}`);
447
+ }
448
+ catch (err) {
449
+ lastErr = err;
450
+ }
451
+ if (attempt < 3)
452
+ await new Promise((r) => setTimeout(r, 750 * (attempt + 1)));
453
+ }
454
+ throw new Error(`resolver unreachable at ${url} — ${lastErr?.message ?? String(lastErr)} ` +
455
+ `(is \`abx serve\` running there?)`);
456
+ }
457
+ }
458
+ function readBody(req) {
459
+ return new Promise((resolve, reject) => {
460
+ let data = '';
461
+ req.on('data', (c) => (data += c));
462
+ req.on('end', () => resolve(data));
463
+ req.on('error', reject);
464
+ });
465
+ }
466
+ //# sourceMappingURL=harness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness.js","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { EffectRunner } from './harness.js';
2
+ export type { EffectModule, EffectContext, EffectOutput, EffectOutputDecl, EffectArtifactRecord, RunnerOptions, SweepStats, } from './harness.js';
3
+ export { renderEffect } from './render.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,cAAc,CAAC;AAC1C,YAAY,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { EffectRunner } from './harness.js';
2
+ export { renderEffect } from './render.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,cAAc,CAAC;AAU1C,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
package/dist/main.js ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import { createPublicClient, http } from 'viem';
3
+ import { loadDotEnv } from '@artblocks/abx-sdk';
4
+ import { resolveBackend } from '@artblocks/abx-storage';
5
+ import { EffectRunner } from './harness.js';
6
+ import { renderEffect } from './render.js';
7
+ /**
8
+ * The effect runner entrypoint — the deployed, long-running form (an artist runs this
9
+ * beside their resolver during a drop; `docker compose` or a bare `pnpm abx-effects`).
10
+ *
11
+ * Config (env): ABX_RESOLVER_URL (required) · ABX_RPC_URL (recommended — augment hooks
12
+ * + data-backed params need reads) · ABX_EFFECTS_PORT (default 8788) ·
13
+ * ABX_EFFECTS_INTERVAL_MS (default 300000 — the SAFETY FLOOR: the resolver's chain watcher
14
+ * `POST /notify` is the primary trigger; the sweep only catches a missed ping / cold start) ·
15
+ * ABX_EFFECTS_CONCURRENCY (default 1 — parallel renders while draining) ·
16
+ * ABX_EFFECTS_TOKEN (gates /run + /notify — REQUIRED on a public runner) ·
17
+ * ABX_ENVIRONMENT_ID (default web:any) ·
18
+ * ABX_RESOLVER_ADMIN_TOKEN (optional — set it in the locator-bridge topology, where this runner
19
+ * does NOT share the resolver's storage disk, so each render is PUBLISHED to the resolver's control
20
+ * plane + run status is REPORTED to /admin/effect-status. Publishing is also what registers each
21
+ * declared output in the resolver's artifact registry — the `artifacts` manifest's enumeration
22
+ * surface — so every deployed runner should set it; the token-less co-located form is the CLI's
23
+ * `abx effects`, which records rows into the shared store directly) ·
24
+ * storage backend env (the custody home this runner uploads renders to).
25
+ *
26
+ * One-shot repair mode (the `abx render` escape hatch rides this):
27
+ * pnpm abx-effects --once <address> [tokenId…]
28
+ */
29
+ async function main() {
30
+ loadDotEnv();
31
+ const resolverUrl = (process.env.ABX_RESOLVER_URL ?? 'http://localhost:8787').replace(/\/$/, '');
32
+ const rpc = process.env.ABX_RPC_URL;
33
+ const client = rpc ? createPublicClient({ transport: http(rpc) }) : null;
34
+ const runner = new EffectRunner({
35
+ resolverUrl,
36
+ client,
37
+ storage: resolveBackend(),
38
+ effects: [renderEffect()],
39
+ environmentId: process.env.ABX_ENVIRONMENT_ID ?? 'web:any',
40
+ // With a token, publish each render to the resolver's control plane (locator bridge) so a runner
41
+ // that doesn't share the resolver's disk still lands the thumbnail. Omit ⇒ co-located shared store.
42
+ adminToken: process.env.ABX_RESOLVER_ADMIN_TOKEN,
43
+ concurrency: Number(process.env.ABX_EFFECTS_CONCURRENCY ?? 1),
44
+ authToken: process.env.ABX_EFFECTS_TOKEN,
45
+ });
46
+ const args = process.argv.slice(2);
47
+ if (args[0] === '--once') {
48
+ const [address, ...tokenIds] = args.slice(1);
49
+ if (!address)
50
+ throw new Error('usage: abx-effects --once <address> [tokenId…]');
51
+ const stats = await runner.sweepProject(address, tokenIds.length ? tokenIds : undefined);
52
+ console.log(`[effects] once: ran=${stats.ran} skipped=${stats.skipped} failed=${stats.failed}`);
53
+ return;
54
+ }
55
+ const port = Number(process.env.ABX_EFFECTS_PORT ?? 8788);
56
+ // 300s: the sweep is the SAFETY FLOOR (missed ping / cold start / new project), not the trigger —
57
+ // the resolver's chain watcher POSTs /notify the moment settled state changes.
58
+ const intervalMs = Number(process.env.ABX_EFFECTS_INTERVAL_MS ?? 300_000);
59
+ runner.startHttp(port);
60
+ runner.startLoop(intervalMs);
61
+ console.log(`[effects] runner up — resolver=${resolverUrl} port=${port} sweep=${intervalMs}ms (safety floor) ` +
62
+ `(the resolver's watcher POSTs /notify for immediacy; /run is the synchronous command lane)`);
63
+ }
64
+ main().catch((err) => {
65
+ console.error(`[effects] fatal: ${err.message}`);
66
+ process.exit(1);
67
+ });
68
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EAAC,kBAAkB,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AAC9C,OAAO,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAC,cAAc,EAAC,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAC,YAAY,EAAC,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,KAAK,UAAU,IAAI;IACjB,UAAU,EAAE,CAAC;IACb,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,uBAAuB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACjG,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IACpC,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,EAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,EAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEvE,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;QAC9B,WAAW;QACX,MAAM;QACN,OAAO,EAAE,cAAc,EAAE;QACzB,OAAO,EAAE,CAAC,YAAY,EAAE,CAAC;QACzB,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,SAAS;QAC1D,iGAAiG;QACjG,oGAAoG;QACpG,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB;QAChD,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,CAAC,CAAC;QAC7D,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB;KACzC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;QACzB,MAAM,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QAChF,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACzF,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,OAAO,WAAW,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAChG,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,CAAC;IAC1D,kGAAkG;IAClG,+EAA+E;IAC/E,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,OAAO,CAAC,CAAC;IAC1E,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAC7B,OAAO,CAAC,GAAG,CACT,kCAAkC,WAAW,SAAS,IAAI,UAAU,UAAU,oBAAoB;QAChG,4FAA4F,CAC/F,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,oBAAqB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { EffectModule } from './harness.js';
2
+ /**
3
+ * The reference `render` effect: capture the live view — the exact document a collector
4
+ * sees — as the token's still, and collect the script's reported traits.
5
+ *
6
+ * Playwright is loaded **dynamically at first run** (never a static import): the
7
+ * resolver image must never grow a browser, so the dependency exists only where
8
+ * captures actually run (the effects image, or a laptop that ran
9
+ * `pnpm add playwright && npx playwright install chromium`).
10
+ *
11
+ * Capture contract (mirrors `specs/protocol/code-projects.md`): wait for `abx.done()`,
12
+ * else the project's `render.captureDelay` contract param (ms), else a default timeout.
13
+ * Dimensions: `render.imageWidth` (default 1200 — pass a larger width for a hi-def
14
+ * master when image is the primary display format) × `render.aspectRatio` (w/h,
15
+ * default 1).
16
+ *
17
+ * Outputs (typed, per the data plane's declared-outputs rule): `image` (the still) and
18
+ * `traits` always; `print` — a print-resolution export, the plane's reference unbound
19
+ * artifact — only when the project sets the `render.printWidth` contract param (a second
20
+ * capture at that width; absent ⇒ null, no artifact, no boilerplate). Because
21
+ * `render.printWidth` is a contract param it rides tokenData, so enabling it re-addresses
22
+ * every token — the whole collection re-renders once with the print export included.
23
+ */
24
+ export declare function renderEffect(defaults?: {
25
+ width?: number;
26
+ timeoutMs?: number;
27
+ }): EffectModule;
28
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgB,YAAY,EAAe,MAAM,cAAc,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,GAAE;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAM,GAAG,YAAY,CAuC9F"}
package/dist/render.js ADDED
@@ -0,0 +1,101 @@
1
+ import { contractParamString } from '@artblocks/abx-sdk';
2
+ /**
3
+ * The reference `render` effect: capture the live view — the exact document a collector
4
+ * sees — as the token's still, and collect the script's reported traits.
5
+ *
6
+ * Playwright is loaded **dynamically at first run** (never a static import): the
7
+ * resolver image must never grow a browser, so the dependency exists only where
8
+ * captures actually run (the effects image, or a laptop that ran
9
+ * `pnpm add playwright && npx playwright install chromium`).
10
+ *
11
+ * Capture contract (mirrors `specs/protocol/code-projects.md`): wait for `abx.done()`,
12
+ * else the project's `render.captureDelay` contract param (ms), else a default timeout.
13
+ * Dimensions: `render.imageWidth` (default 1200 — pass a larger width for a hi-def
14
+ * master when image is the primary display format) × `render.aspectRatio` (w/h,
15
+ * default 1).
16
+ *
17
+ * Outputs (typed, per the data plane's declared-outputs rule): `image` (the still) and
18
+ * `traits` always; `print` — a print-resolution export, the plane's reference unbound
19
+ * artifact — only when the project sets the `render.printWidth` contract param (a second
20
+ * capture at that width; absent ⇒ null, no artifact, no boilerplate). Because
21
+ * `render.printWidth` is a contract param it rides tokenData, so enabling it re-addresses
22
+ * every token — the whole collection re-renders once with the print export included.
23
+ */
24
+ export function renderEffect(defaults = {}) {
25
+ return {
26
+ key: 'render',
27
+ outputs: [
28
+ { key: 'image', mimeType: 'image/png' },
29
+ { key: 'traits', mimeType: 'application/json' },
30
+ { key: 'print', mimeType: 'image/png' },
31
+ ],
32
+ async run(ctx) {
33
+ const width = Number(contractParamString(ctx.state, 'render.imageWidth') ?? defaults.width ?? 1200);
34
+ const aspect = Number(contractParamString(ctx.state, 'render.aspectRatio') ?? 1) || 1;
35
+ const captureDelay = Number(contractParamString(ctx.state, 'render.captureDelay') ?? 0);
36
+ const timeoutMs = captureDelay > 0 ? captureDelay : (defaults.timeoutMs ?? 10_000);
37
+ const printWidth = Number(contractParamString(ctx.state, 'render.printWidth') ?? 0);
38
+ const pw = await loadPlaywright();
39
+ const browser = await pw.chromium.launch();
40
+ try {
41
+ const still = await capture(browser, ctx.liveViewUrl, width, Math.round(width / aspect), timeoutMs, true);
42
+ // The print export re-runs the same deterministic document at print resolution — a
43
+ // fresh page, so viewport-sized sketches lay out natively rather than being rescaled.
44
+ const print = printWidth > 0
45
+ ? await capture(browser, ctx.liveViewUrl, printWidth, Math.round(printWidth / aspect), timeoutMs, false)
46
+ : null;
47
+ return {
48
+ image: { bytes: still.png, contentType: 'image/png' },
49
+ traits: still.traits
50
+ ? { bytes: new TextEncoder().encode(still.traits), contentType: 'application/json' }
51
+ : null,
52
+ print: print ? { bytes: print.png, contentType: 'image/png' } : null,
53
+ };
54
+ }
55
+ finally {
56
+ await browser.close();
57
+ }
58
+ },
59
+ };
60
+ }
61
+ /** One capture: load the live view at (width, height), wait for `abx.done()` (else the timeout),
62
+ * screenshot — and read the script's reported traits when asked. */
63
+ async function capture(browser, liveViewUrl, width, height, timeoutMs, wantTraits) {
64
+ const page = await browser.newPage({ viewport: { width, height } });
65
+ const resp = await page.goto(liveViewUrl, { waitUntil: 'load', timeout: 60_000 });
66
+ // Refuse to capture a failed navigation. The live view 302s to the content gateway; if that
67
+ // ends in an error status we'd otherwise screenshot the gateway's error page and store it as
68
+ // the "still" — a silent garbage thumbnail. The common trigger is a fresh Arweave/Turbo
69
+ // upload that hasn't propagated yet ("hashpath cannot be resolved on this node, yet"), or a
70
+ // dead/HTML-refusing gateway. Fail loudly instead so `abx render` reports it and nothing is stored.
71
+ const status = resp?.status?.() ?? 0;
72
+ if (status >= 400) {
73
+ throw new Error(`live view returned HTTP ${status} (${resp?.url?.() ?? liveViewUrl}) — the content isn't servable yet. ` +
74
+ `Arweave/Turbo uploads take time to propagate; retry the render shortly. If it persists, the gateway may be down ` +
75
+ `or not serving HTML. Nothing was stored.`);
76
+ }
77
+ // abx.done() is the capture point; the timeout is the fallback for scripts that never call it.
78
+ await page
79
+ .waitForFunction('window.abx && window.abx.__done === true', undefined, { timeout: timeoutMs })
80
+ .catch(() => undefined);
81
+ const png = (await page.screenshot({ type: 'png' }));
82
+ const traits = wantTraits
83
+ ? (await page
84
+ .evaluate('window.abx && window.abx.__traits ? JSON.stringify(window.abx.__traits) : null')
85
+ .catch(() => null))
86
+ : null;
87
+ return { png, traits };
88
+ }
89
+ /** Runtime-optional resolution so the workspace never hard-depends on a browser. */
90
+ async function loadPlaywright() {
91
+ try {
92
+ const { createRequire } = await import('node:module');
93
+ const requireHere = createRequire(import.meta.url);
94
+ return requireHere('playwright');
95
+ }
96
+ catch {
97
+ throw new Error('playwright could not load — captures need its browsers installed where they run: ' +
98
+ '`npx playwright install chromium` (the effects container image ships them baked in).');
99
+ }
100
+ }
101
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@artblocks/abx-effects",
3
+ "version": "0.1.0-alpha.0",
4
+ "license": "MIT",
5
+ "description": "ABX Self-Host Toolkit (Layer 3) — the reference effect runner. A deployed, long-running service hosting spine-subscribing effects (render first) behind one harness; stateless via inputsHash addressing. The playwright npm package is a normal (small, browserless) dependency pinned EXACTLY to the Dockerfile.effects base-image tag; browsers install only where captures run (`npx playwright install chromium`).",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "LICENSE"
17
+ ],
18
+ "sideEffects": false,
19
+ "engines": {
20
+ "node": ">=22.5.0"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/ArtBlocks/abx.git",
25
+ "directory": "packages/effects"
26
+ },
27
+ "homepage": "https://github.com/ArtBlocks/abx#readme",
28
+ "bugs": "https://github.com/ArtBlocks/abx/issues",
29
+ "keywords": [
30
+ "abx",
31
+ "nft",
32
+ "effects",
33
+ "playwright",
34
+ "render",
35
+ "art-blocks"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {
41
+ "playwright": "1.61.1",
42
+ "viem": "^2.21.0",
43
+ "@artblocks/abx-storage": "0.1.0-alpha.0",
44
+ "@artblocks/abx-sdk": "0.1.0-alpha.0"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -b tsconfig.build.json"
48
+ },
49
+ "types": "./dist/index.d.ts",
50
+ "bin": {
51
+ "abx-effects": "./dist/main.js"
52
+ }
53
+ }