@ariestools/cli 0.1.8 → 0.1.10
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/README.md +32 -0
- package/dist/bin/aries.mjs +611 -188
- package/dist/bin/daemons/chain-server.mjs +152 -0
- package/dist/bin/daemons/dapp-server.mjs +970 -0
- package/dist/bin/daemons/datalake-dev.mjs +111566 -0
- package/dist/node/datalake.d.ts +220 -0
- package/dist/node/datalake.mjs +475 -0
- package/package.json +21 -11
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { DAPP_BACKINGS, DAPP_BUCKETS, isDappBackingKind, resolveBacking, startDappServer } from "@ariestools/aries-dapp-serve";
|
|
7
|
+
import { AbstractCreatable, ConsoleLogger, IdLogger, assertEx, creatable } from "@ariestools/sdk";
|
|
8
|
+
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
//#region src/actor/AbstractActor.ts
|
|
11
|
+
function createDeferred$1() {
|
|
12
|
+
let resolve;
|
|
13
|
+
let reject;
|
|
14
|
+
const promise = new Promise((res, rej) => {
|
|
15
|
+
resolve = res;
|
|
16
|
+
reject = rej;
|
|
17
|
+
});
|
|
18
|
+
promise.catch(() => {});
|
|
19
|
+
return {
|
|
20
|
+
promise,
|
|
21
|
+
reject,
|
|
22
|
+
resolve
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Lean, dapp-owned fork of the xl1 `ActorV3` pattern: the create/start/stop
|
|
27
|
+
* lifecycle from `AbstractCreatable`, plus non-overlapping interval timers and
|
|
28
|
+
* a readiness contract. It deliberately swaps the chain's
|
|
29
|
+
* `ProviderFactoryLocator`/`LocatorConfig` machinery for the small
|
|
30
|
+
* {@link DappProviderLocator} seam, so a dapp actor resolves providers by
|
|
31
|
+
* moniker without dragging the chain protocol config model in.
|
|
32
|
+
*
|
|
33
|
+
* The locator is process-wide: the daemon builds it once and passes the same
|
|
34
|
+
* instance to every actor via `params.locator` (see `bootDappActors`). When
|
|
35
|
+
* this graduates into a shared cli/actor toolkit — and, later, xl1-protocol —
|
|
36
|
+
* this base can re-adopt the real locator.
|
|
37
|
+
*/
|
|
38
|
+
var AbstractActor = class extends AbstractCreatable {
|
|
39
|
+
_intervals = /* @__PURE__ */ new Map();
|
|
40
|
+
_timeouts = /* @__PURE__ */ new Map();
|
|
41
|
+
_abortController = new AbortController();
|
|
42
|
+
_idLogger;
|
|
43
|
+
_inFlight = /* @__PURE__ */ new Map();
|
|
44
|
+
_readyDeferred = createDeferred$1();
|
|
45
|
+
_readyError;
|
|
46
|
+
_readyState = "pending";
|
|
47
|
+
get logger() {
|
|
48
|
+
this._idLogger ??= new IdLogger(assertEx(this.context.logger, () => `Logger is required in context for actor ${this.name}.`), () => this.name);
|
|
49
|
+
return this._idLogger;
|
|
50
|
+
}
|
|
51
|
+
get readyError() {
|
|
52
|
+
return this._readyError;
|
|
53
|
+
}
|
|
54
|
+
get readyState() {
|
|
55
|
+
return this._readyState;
|
|
56
|
+
}
|
|
57
|
+
get context() {
|
|
58
|
+
return this.locator.context;
|
|
59
|
+
}
|
|
60
|
+
get locator() {
|
|
61
|
+
return this.params.locator;
|
|
62
|
+
}
|
|
63
|
+
/** Abort signal cancelled when the actor stops (or is replaced on restart). */
|
|
64
|
+
get signal() {
|
|
65
|
+
return this._abortController.signal;
|
|
66
|
+
}
|
|
67
|
+
static async paramsHandler(params) {
|
|
68
|
+
const inParams = params ?? {};
|
|
69
|
+
const baseParams = await super.paramsHandler({
|
|
70
|
+
...inParams,
|
|
71
|
+
name: inParams.name ?? "UnknownActor"
|
|
72
|
+
});
|
|
73
|
+
const locator = assertEx(inParams.locator, () => `params.locator is required for actor ${String(baseParams.name)}.`);
|
|
74
|
+
return {
|
|
75
|
+
...baseParams,
|
|
76
|
+
locator
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Override to prove the actor can do useful work. Default: no-op. */
|
|
80
|
+
async readyHandler() {}
|
|
81
|
+
/**
|
|
82
|
+
* Register a recurring task. The first invocation fires after `dueTimeMs`
|
|
83
|
+
* (the documented first-run delay); subsequent invocations fire every
|
|
84
|
+
* `periodMs` while the actor is `started`. A run is skipped if the previous
|
|
85
|
+
* one is still in flight, so a slow pass can never stack on top of itself.
|
|
86
|
+
* Must be called from `startHandler` (guards on `starting` status).
|
|
87
|
+
*/
|
|
88
|
+
registerTimer(timerName, callback, dueTimeMs, periodMs) {
|
|
89
|
+
if (this.status !== "starting") {
|
|
90
|
+
this.logger.warn(`Cannot register timer '${timerName}' because actor is not starting.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const tick = () => {
|
|
94
|
+
if (this.status !== "started") return;
|
|
95
|
+
if (this._inFlight.has(timerName)) {
|
|
96
|
+
this.logger.warn(`Skipping timer '${this.name}:${timerName}' because the previous run is still in flight.`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const run = (async () => {
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
try {
|
|
102
|
+
await callback();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
105
|
+
this.logger.error(`Error in timer '${this.name}:${timerName}': ${err.message}`);
|
|
106
|
+
} finally {
|
|
107
|
+
const duration = Date.now() - startTime;
|
|
108
|
+
if (duration > periodMs) this.logger.warn(`Timer '${this.name}:${timerName}' took ${duration}ms, longer than its ${periodMs}ms period.`);
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
this._inFlight.set(timerName, run.finally(() => {
|
|
112
|
+
this._inFlight.delete(timerName);
|
|
113
|
+
}));
|
|
114
|
+
};
|
|
115
|
+
const timeoutId = setTimeout(() => {
|
|
116
|
+
if (this.status !== "started") return;
|
|
117
|
+
this._intervals.set(timerName, setInterval(tick, periodMs));
|
|
118
|
+
tick();
|
|
119
|
+
}, dueTimeMs);
|
|
120
|
+
this._timeouts.set(timerName, timeoutId);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Run the warm-pass once. The standard boot path invokes this after
|
|
124
|
+
* `start()` so readiness either resolves or rejects.
|
|
125
|
+
*/
|
|
126
|
+
async runReadyHandler() {
|
|
127
|
+
if (this._readyState !== "pending") return;
|
|
128
|
+
try {
|
|
129
|
+
await this.readyHandler();
|
|
130
|
+
this._readyState = "ready";
|
|
131
|
+
this._readyDeferred.resolve();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
134
|
+
this._readyState = "failed";
|
|
135
|
+
this._readyError = err;
|
|
136
|
+
this._readyDeferred.reject(err);
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async startHandler() {
|
|
141
|
+
if (this._abortController.signal.aborted) this._abortController = new AbortController();
|
|
142
|
+
await super.startHandler();
|
|
143
|
+
}
|
|
144
|
+
async stopHandler() {
|
|
145
|
+
await super.stopHandler();
|
|
146
|
+
this._abortController.abort();
|
|
147
|
+
for (const timeoutRef of this._timeouts.values()) clearTimeout(timeoutRef);
|
|
148
|
+
this._timeouts.clear();
|
|
149
|
+
for (const intervalRef of this._intervals.values()) clearInterval(intervalRef);
|
|
150
|
+
this._intervals.clear();
|
|
151
|
+
const inFlight = [...this._inFlight.values()];
|
|
152
|
+
this._inFlight.clear();
|
|
153
|
+
const timeoutMs = this.params.shutdownTimeoutMs;
|
|
154
|
+
if (timeoutMs === void 0) {
|
|
155
|
+
await Promise.allSettled(inFlight);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await Promise.race([Promise.allSettled(inFlight), new Promise((resolve) => {
|
|
159
|
+
setTimeout(resolve, timeoutMs);
|
|
160
|
+
})]);
|
|
161
|
+
}
|
|
162
|
+
async whenReady() {
|
|
163
|
+
await this._readyDeferred.promise;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/providers/capabilities.ts
|
|
168
|
+
/** Thrown when a read-only capability wrapper receives a write. */
|
|
169
|
+
var ReadOnlyStoreError = class extends Error {
|
|
170
|
+
name = "ReadOnlyStoreError";
|
|
171
|
+
constructor(role, operation = "put") {
|
|
172
|
+
super(`Store role "${role}" is read-only; ${operation} is not permitted`);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function bindReader(store) {
|
|
176
|
+
return {
|
|
177
|
+
role: store.role,
|
|
178
|
+
bucket: store.bucket,
|
|
179
|
+
prefix: store.prefix,
|
|
180
|
+
...store.publicBaseUrl !== void 0 && { publicBaseUrl: store.publicBaseUrl },
|
|
181
|
+
destroy: () => store.destroy(),
|
|
182
|
+
get: (key) => store.get(key),
|
|
183
|
+
getWithMeta: (key) => store.getWithMeta(key),
|
|
184
|
+
list: (listPrefix) => store.list(listPrefix),
|
|
185
|
+
listAsync: (listPrefix) => store.listAsync(listPrefix),
|
|
186
|
+
listPage: (options) => store.listPage(options),
|
|
187
|
+
publicUrl: (key) => store.publicUrl(key),
|
|
188
|
+
resolveKey: (key) => store.resolveKey(key),
|
|
189
|
+
stat: (key) => store.stat(key)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Wrap any writer as a read-only reader. Other methods and destroy delegate to
|
|
194
|
+
* the underlying store.
|
|
195
|
+
*/
|
|
196
|
+
function asReadOnly(store) {
|
|
197
|
+
return bindReader(store);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Wrap a writer so `put` fails even if the caller still holds the writer type
|
|
201
|
+
* (defense in depth for the data-role locator registration).
|
|
202
|
+
*/
|
|
203
|
+
function asReadOnlyWriter(store) {
|
|
204
|
+
return {
|
|
205
|
+
...bindReader(store),
|
|
206
|
+
async put() {
|
|
207
|
+
throw new ReadOnlyStoreError(store.role, "put");
|
|
208
|
+
},
|
|
209
|
+
async delete() {
|
|
210
|
+
throw new ReadOnlyStoreError(store.role, "delete");
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/providers/monikers.ts
|
|
216
|
+
/**
|
|
217
|
+
* Provider monikers for the three dapp buckets. An actor declares these in its
|
|
218
|
+
* `static dependencies` and resolves them from the locator at create time.
|
|
219
|
+
*/
|
|
220
|
+
const DappDataStoreMoniker = "DappDataStore";
|
|
221
|
+
const DappStateStoreMoniker = "DappStateStore";
|
|
222
|
+
const DappIndexStoreMoniker = "DappIndexStore";
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/publication/constants.ts
|
|
225
|
+
/** Well-known relative keys published by the coherent generation protocol. */
|
|
226
|
+
const HEAD_KEY = "head.json";
|
|
227
|
+
const STATUS_KEY = "status.json";
|
|
228
|
+
/** CDN caching: mutable head and status objects that must revalidate. */
|
|
229
|
+
const CACHE_CONTROL_REVALIDATE = "public, max-age=0, must-revalidate";
|
|
230
|
+
const INDEXER_STATUS_SCHEMA = "network.aries.dapp.indexer.status";
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/publication/status.ts
|
|
233
|
+
function encodeStatus(status) {
|
|
234
|
+
return `${JSON.stringify(status, null, 2)}\n`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Read the durable indexer status object from a state (or status) store.
|
|
238
|
+
* Returns undefined when no status has been published yet.
|
|
239
|
+
*/
|
|
240
|
+
async function readIndexerStatus(store, key = STATUS_KEY) {
|
|
241
|
+
const body = await store.get(key);
|
|
242
|
+
if (body === void 0) return void 0;
|
|
243
|
+
const parsed = JSON.parse(new TextDecoder().decode(body));
|
|
244
|
+
if (parsed === null || typeof parsed !== "object") throw new Error(`Invalid indexer status at "${key}": not an object`);
|
|
245
|
+
const record = parsed;
|
|
246
|
+
if (record.schema !== "network.aries.dapp.indexer.status") throw new Error(`Invalid indexer status schema at "${key}": ${String(record.schema)}`);
|
|
247
|
+
return record;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Publish a machine-readable status object with revalidate CDN caching.
|
|
251
|
+
* Status never carries credentials or private application material.
|
|
252
|
+
*/
|
|
253
|
+
async function writeIndexerStatus(store, input) {
|
|
254
|
+
const key = input.key ?? "status.json";
|
|
255
|
+
const status = {
|
|
256
|
+
schema: INDEXER_STATUS_SCHEMA,
|
|
257
|
+
consecutiveFailures: input.consecutiveFailures,
|
|
258
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
259
|
+
...input.floor !== void 0 && { floor: input.floor },
|
|
260
|
+
...input.cursor !== void 0 && { cursor: input.cursor },
|
|
261
|
+
...input.lastCompletedPosition !== void 0 && { lastCompletedPosition: input.lastCompletedPosition },
|
|
262
|
+
...input.observedSourceHead !== void 0 && { observedSourceHead: input.observedSourceHead },
|
|
263
|
+
...input.generation !== void 0 && { generation: input.generation },
|
|
264
|
+
...input.generationRoot !== void 0 && { generationRoot: input.generationRoot },
|
|
265
|
+
...input.lastSuccessAt !== void 0 && { lastSuccessAt: input.lastSuccessAt },
|
|
266
|
+
...input.lastErrorAt !== void 0 && { lastErrorAt: input.lastErrorAt },
|
|
267
|
+
...input.lastError !== void 0 && { lastError: input.lastError }
|
|
268
|
+
};
|
|
269
|
+
await store.put(key, encodeStatus(status), {
|
|
270
|
+
contentType: "application/json",
|
|
271
|
+
cacheControl: CACHE_CONTROL_REVALIDATE
|
|
272
|
+
});
|
|
273
|
+
return status;
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/publication/statusPatch.ts
|
|
277
|
+
function pickDefined(source, keys) {
|
|
278
|
+
const out = {};
|
|
279
|
+
for (const key of keys) {
|
|
280
|
+
const value = source[key];
|
|
281
|
+
if (value !== void 0) out[key] = value;
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
const PROGRESS_KEYS = [
|
|
286
|
+
"floor",
|
|
287
|
+
"cursor",
|
|
288
|
+
"lastCompletedPosition",
|
|
289
|
+
"observedSourceHead",
|
|
290
|
+
"generation",
|
|
291
|
+
"generationRoot"
|
|
292
|
+
];
|
|
293
|
+
/** Carry forward durable progress fields from a prior status object. */
|
|
294
|
+
function priorProgressFields(prior) {
|
|
295
|
+
if (prior === void 0) return {};
|
|
296
|
+
return {
|
|
297
|
+
...pickDefined(prior, PROGRESS_KEYS),
|
|
298
|
+
...prior.lastSuccessAt !== void 0 && { lastSuccessAt: prior.lastSuccessAt }
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** Build success status fields from reduce progress, falling back to prior. */
|
|
302
|
+
function successStatusFields(progress, prior, now) {
|
|
303
|
+
const fromProgress = progress === void 0 ? {} : pickDefined(progress, PROGRESS_KEYS);
|
|
304
|
+
const generation = progress?.generation ?? prior?.generation;
|
|
305
|
+
const generationRoot = progress?.generationRoot ?? prior?.generationRoot;
|
|
306
|
+
return {
|
|
307
|
+
consecutiveFailures: 0,
|
|
308
|
+
...fromProgress,
|
|
309
|
+
...generation !== void 0 && { generation },
|
|
310
|
+
...generationRoot !== void 0 && { generationRoot },
|
|
311
|
+
lastSuccessAt: now,
|
|
312
|
+
...prior?.lastError !== void 0 && { lastError: prior.lastError },
|
|
313
|
+
...prior?.lastErrorAt !== void 0 && { lastErrorAt: prior.lastErrorAt }
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/** Build failure status fields while preserving prior checkpoint progress. */
|
|
317
|
+
function failureStatusFields(prior, consecutiveFailures, error, now) {
|
|
318
|
+
return {
|
|
319
|
+
consecutiveFailures,
|
|
320
|
+
...priorProgressFields(prior),
|
|
321
|
+
lastError: error.message,
|
|
322
|
+
lastErrorAt: now
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region \0@oxc-project+runtime@0.140.0/helpers/esm/decorate.js
|
|
327
|
+
function __decorate(decorators, target, key, desc) {
|
|
328
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
329
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
330
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
331
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/DappActor.ts
|
|
335
|
+
const DEFAULT_REDUCE_INTERVAL_MS = 5e3;
|
|
336
|
+
const DEFAULT_FIRST_RUN_DELAY_MS = 2e3;
|
|
337
|
+
const REDUCE_TIMER = "DappReduce";
|
|
338
|
+
function createDeferred() {
|
|
339
|
+
let resolve;
|
|
340
|
+
let reject;
|
|
341
|
+
const promise = new Promise((res, rej) => {
|
|
342
|
+
resolve = res;
|
|
343
|
+
reject = rej;
|
|
344
|
+
});
|
|
345
|
+
promise.catch(() => {});
|
|
346
|
+
return {
|
|
347
|
+
promise,
|
|
348
|
+
reject,
|
|
349
|
+
resolve
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function withDefaultGenerationRoot(result) {
|
|
353
|
+
if (result === void 0) return;
|
|
354
|
+
if (result.generation === void 0 || result.generationRoot !== void 0) return result;
|
|
355
|
+
return {
|
|
356
|
+
...result,
|
|
357
|
+
generationRoot: HEAD_KEY
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
let DappActor = class DappActor extends AbstractActor {
|
|
361
|
+
static dependencies = [
|
|
362
|
+
DappDataStoreMoniker,
|
|
363
|
+
DappStateStoreMoniker,
|
|
364
|
+
DappIndexStoreMoniker
|
|
365
|
+
];
|
|
366
|
+
_data;
|
|
367
|
+
_index;
|
|
368
|
+
_state;
|
|
369
|
+
_consecutiveFailures = 0;
|
|
370
|
+
_firstPass = createDeferred();
|
|
371
|
+
_firstPassSettled = false;
|
|
372
|
+
/** Consecutive reduce failures since the last success. */
|
|
373
|
+
get consecutiveFailures() {
|
|
374
|
+
return this._consecutiveFailures;
|
|
375
|
+
}
|
|
376
|
+
async createHandler() {
|
|
377
|
+
await super.createHandler();
|
|
378
|
+
const dataWriter = await this.locator.getInstance(DappDataStoreMoniker);
|
|
379
|
+
this._data = asReadOnly(dataWriter);
|
|
380
|
+
this._state = await this.locator.getInstance(DappStateStoreMoniker);
|
|
381
|
+
this._index = await this.locator.getInstance(DappIndexStoreMoniker);
|
|
382
|
+
}
|
|
383
|
+
async readyHandler() {
|
|
384
|
+
await this._firstPass.promise;
|
|
385
|
+
}
|
|
386
|
+
async startHandler() {
|
|
387
|
+
await super.startHandler();
|
|
388
|
+
const interval = this.params.reduceIntervalMs ?? DEFAULT_REDUCE_INTERVAL_MS;
|
|
389
|
+
const firstRunDelay = this.params.firstRunDelayMs ?? DEFAULT_FIRST_RUN_DELAY_MS;
|
|
390
|
+
this.registerTimer(REDUCE_TIMER, async () => {
|
|
391
|
+
await this.runReducePass();
|
|
392
|
+
}, firstRunDelay, interval);
|
|
393
|
+
this.logger.info(`DappActor started: reducer '${this.params.reducer.name}' every ${interval}ms (first run in ${firstRunDelay}ms)`);
|
|
394
|
+
}
|
|
395
|
+
async stopHandler() {
|
|
396
|
+
this.settleFirstPass(/* @__PURE__ */ new Error("Actor stopped before first reduce pass completed"));
|
|
397
|
+
await super.stopHandler();
|
|
398
|
+
}
|
|
399
|
+
async publishStatusFromProgress(progress, error) {
|
|
400
|
+
if (this.params.publishStatus === false) return;
|
|
401
|
+
let prior;
|
|
402
|
+
try {
|
|
403
|
+
prior = await readIndexerStatus(this._state);
|
|
404
|
+
} catch {
|
|
405
|
+
prior = void 0;
|
|
406
|
+
}
|
|
407
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
408
|
+
const fields = error === void 0 ? successStatusFields(progress, prior, now) : failureStatusFields(prior, this._consecutiveFailures, error, now);
|
|
409
|
+
await writeIndexerStatus(this._state, fields);
|
|
410
|
+
}
|
|
411
|
+
async runReducePass() {
|
|
412
|
+
try {
|
|
413
|
+
const result = await this.params.reducer.reduce({
|
|
414
|
+
data: this._data,
|
|
415
|
+
index: this._index,
|
|
416
|
+
logger: this.logger,
|
|
417
|
+
signal: this.signal,
|
|
418
|
+
state: this._state
|
|
419
|
+
});
|
|
420
|
+
const progress = withDefaultGenerationRoot(result === void 0 ? void 0 : result);
|
|
421
|
+
this._consecutiveFailures = 0;
|
|
422
|
+
await this.safePublishStatus(progress);
|
|
423
|
+
this.settleFirstPass();
|
|
424
|
+
} catch (error) {
|
|
425
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
426
|
+
this._consecutiveFailures += 1;
|
|
427
|
+
await this.safePublishStatus(void 0, err);
|
|
428
|
+
this.settleFirstPass(err);
|
|
429
|
+
this.logger.error(`Error in reduce pass for '${this.params.reducer.name}': ${err.message}`);
|
|
430
|
+
const max = this.params.maxConsecutiveFailures;
|
|
431
|
+
if (max !== void 0 && this._consecutiveFailures >= max) {
|
|
432
|
+
this.logger.error(`Reducer '${this.params.reducer.name}' failed ${this._consecutiveFailures} consecutive times; stopping actor`);
|
|
433
|
+
setTimeout(() => {
|
|
434
|
+
this.stop();
|
|
435
|
+
}, 0);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async safePublishStatus(progress, error) {
|
|
440
|
+
try {
|
|
441
|
+
await this.publishStatusFromProgress(progress, error);
|
|
442
|
+
} catch (statusError) {
|
|
443
|
+
this.logger.error(`Failed to publish indexer status: ${String(statusError)}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
settleFirstPass(error) {
|
|
447
|
+
if (this._firstPassSettled) return;
|
|
448
|
+
this._firstPassSettled = true;
|
|
449
|
+
if (error === void 0) this._firstPass.resolve();
|
|
450
|
+
else this._firstPass.reject(error);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
DappActor = __decorate([creatable()], DappActor);
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/boot/bootDappActors.ts
|
|
456
|
+
/**
|
|
457
|
+
* Creates and starts one {@link DappActor} per spec, all sharing the single
|
|
458
|
+
* process-wide locator. On partial failure, actors already started are stopped
|
|
459
|
+
* before the error is rethrown. Returns them so the caller can stop them on
|
|
460
|
+
* shutdown.
|
|
461
|
+
*/
|
|
462
|
+
async function bootDappActors(locator, specs) {
|
|
463
|
+
const actors = [];
|
|
464
|
+
try {
|
|
465
|
+
for (const spec of specs) {
|
|
466
|
+
const params = {
|
|
467
|
+
locator,
|
|
468
|
+
name: spec.name,
|
|
469
|
+
reducer: spec.reducer,
|
|
470
|
+
...spec.reduceIntervalMs !== void 0 && { reduceIntervalMs: spec.reduceIntervalMs },
|
|
471
|
+
...spec.firstRunDelayMs !== void 0 && { firstRunDelayMs: spec.firstRunDelayMs },
|
|
472
|
+
...spec.maxConsecutiveFailures !== void 0 && { maxConsecutiveFailures: spec.maxConsecutiveFailures },
|
|
473
|
+
...spec.publishStatus !== void 0 && { publishStatus: spec.publishStatus },
|
|
474
|
+
...spec.shutdownTimeoutMs !== void 0 && { shutdownTimeoutMs: spec.shutdownTimeoutMs }
|
|
475
|
+
};
|
|
476
|
+
const actor = await DappActor.create(params);
|
|
477
|
+
await actor.start();
|
|
478
|
+
actors.push(actor);
|
|
479
|
+
if (spec.runReady !== false) await actor.runReadyHandler();
|
|
480
|
+
}
|
|
481
|
+
return actors;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
for (const actor of [...actors].reverse()) try {
|
|
484
|
+
await actor.stop();
|
|
485
|
+
} catch {}
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/providers/errors.ts
|
|
491
|
+
/**
|
|
492
|
+
* Thrown when a conditional put fails (object already exists under
|
|
493
|
+
* `ifNotExists`, or ETag mismatch under `ifMatch`).
|
|
494
|
+
*/
|
|
495
|
+
var ConditionalWriteError = class extends Error {
|
|
496
|
+
key;
|
|
497
|
+
name = "ConditionalWriteError";
|
|
498
|
+
constructor(key, message) {
|
|
499
|
+
super(message ?? `Conditional write failed for key "${key}"`);
|
|
500
|
+
this.key = key;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/providers/joinKey.ts
|
|
505
|
+
/**
|
|
506
|
+
* Join a role prefix with a relative object key into a physical S3 key.
|
|
507
|
+
* Empty prefix yields the relative key unchanged; empty relative yields the
|
|
508
|
+
* prefix without a trailing slash.
|
|
509
|
+
*/
|
|
510
|
+
function joinKey(prefix, relativeKey) {
|
|
511
|
+
const cleanPrefix = prefix.replaceAll(/^\/+|\/+$/g, "");
|
|
512
|
+
const cleanKey = relativeKey.replaceAll(/^\/+/g, "");
|
|
513
|
+
if (cleanPrefix.length === 0) return cleanKey;
|
|
514
|
+
if (cleanKey.length === 0) return cleanPrefix;
|
|
515
|
+
return `${cleanPrefix}/${cleanKey}`;
|
|
516
|
+
}
|
|
517
|
+
/** Strip a role prefix from a physical key, returning the relative key. */
|
|
518
|
+
function stripPrefix(prefix, physicalKey) {
|
|
519
|
+
const cleanPrefix = prefix.replaceAll(/^\/+|\/+$/g, "");
|
|
520
|
+
if (cleanPrefix.length === 0) return physicalKey;
|
|
521
|
+
const withSlash = `${cleanPrefix}/`;
|
|
522
|
+
if (physicalKey === cleanPrefix) return "";
|
|
523
|
+
if (physicalKey.startsWith(withSlash)) return physicalKey.slice(withSlash.length);
|
|
524
|
+
return physicalKey;
|
|
525
|
+
}
|
|
526
|
+
/** Normalize an optional public base URL (no trailing slash). */
|
|
527
|
+
function normalizePublicBaseUrl(url) {
|
|
528
|
+
if (url === void 0 || url.length === 0) return void 0;
|
|
529
|
+
return url.replace(/\/+$/, "");
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/providers/DappBucketStore.ts
|
|
533
|
+
const DEFAULT_CREDENTIAL = "S3RVER";
|
|
534
|
+
const DEFAULT_REGION = "us-east-1";
|
|
535
|
+
function isNotFound(error) {
|
|
536
|
+
const name = error.name;
|
|
537
|
+
const status = error.$metadata?.httpStatusCode;
|
|
538
|
+
return name === "NoSuchKey" || name === "NotFound" || name === "404" || status === 404;
|
|
539
|
+
}
|
|
540
|
+
function isConditionalFailure(error) {
|
|
541
|
+
const name = error.name;
|
|
542
|
+
const status = error.$metadata?.httpStatusCode;
|
|
543
|
+
return name === "PreconditionFailed" || name === "ConditionalRequestConflict" || name === "412" || status === 412;
|
|
544
|
+
}
|
|
545
|
+
function toBodyBytes(body) {
|
|
546
|
+
return typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
547
|
+
}
|
|
548
|
+
function metaFromResponse(response) {
|
|
549
|
+
return {
|
|
550
|
+
...response.CacheControl !== void 0 && { cacheControl: response.CacheControl },
|
|
551
|
+
...response.ContentEncoding !== void 0 && { contentEncoding: response.ContentEncoding },
|
|
552
|
+
...response.ContentLength !== void 0 && { contentLength: response.ContentLength },
|
|
553
|
+
...response.ContentType !== void 0 && { contentType: response.ContentType },
|
|
554
|
+
...response.ETag !== void 0 && { etag: response.ETag },
|
|
555
|
+
...response.LastModified !== void 0 && { lastModified: response.LastModified },
|
|
556
|
+
...response.Metadata !== void 0 && Object.keys(response.Metadata).length > 0 && { metadata: response.Metadata }
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Resolve a shared S3 client from an injected instance or construct options.
|
|
561
|
+
* Callers that build three role stores should resolve once and pass the same
|
|
562
|
+
* client to each so dapp-core does not own one client per role.
|
|
563
|
+
*/
|
|
564
|
+
function resolveClientCredentials(options) {
|
|
565
|
+
if (options.credentials !== void 0) return options.credentials;
|
|
566
|
+
if (options.endpoint === void 0) return void 0;
|
|
567
|
+
return {
|
|
568
|
+
accessKeyId: DEFAULT_CREDENTIAL,
|
|
569
|
+
secretAccessKey: DEFAULT_CREDENTIAL
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function resolveS3Client(options) {
|
|
573
|
+
if (options.client !== void 0) return {
|
|
574
|
+
client: options.client,
|
|
575
|
+
ownClient: options.ownClient ?? false
|
|
576
|
+
};
|
|
577
|
+
const credentials = resolveClientCredentials(options);
|
|
578
|
+
return {
|
|
579
|
+
client: new S3Client({
|
|
580
|
+
...options.endpoint !== void 0 && { endpoint: options.endpoint },
|
|
581
|
+
region: options.region ?? DEFAULT_REGION,
|
|
582
|
+
forcePathStyle: options.forcePathStyle ?? options.endpoint !== void 0,
|
|
583
|
+
...credentials !== void 0 && { credentials }
|
|
584
|
+
}),
|
|
585
|
+
ownClient: options.ownClient ?? true
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* S3-backed {@link DappBucketStore}. Accepts an injected client (production)
|
|
590
|
+
* or builds one from endpoint/credentials (local s3rver / simple configs).
|
|
591
|
+
* Path-style addressing is the default when an endpoint is provided so local
|
|
592
|
+
* fixtures and many R2 setups work without virtual-host DNS.
|
|
593
|
+
*/
|
|
594
|
+
function createS3BucketStore(options) {
|
|
595
|
+
const { role } = options;
|
|
596
|
+
const bindingPrefix = (options.binding.prefix ?? "").replaceAll(/^\/+|\/+$/g, "");
|
|
597
|
+
const publicBaseUrl = normalizePublicBaseUrl(options.binding.publicBaseUrl);
|
|
598
|
+
const { client, ownClient } = resolveS3Client(options);
|
|
599
|
+
const bucket = options.binding.bucket;
|
|
600
|
+
let destroyed = false;
|
|
601
|
+
function resolve(relativeKey) {
|
|
602
|
+
return joinKey(bindingPrefix, relativeKey);
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
role,
|
|
606
|
+
bucket,
|
|
607
|
+
prefix: bindingPrefix,
|
|
608
|
+
...publicBaseUrl !== void 0 && { publicBaseUrl },
|
|
609
|
+
resolveKey(key) {
|
|
610
|
+
return resolve(key);
|
|
611
|
+
},
|
|
612
|
+
publicUrl(key) {
|
|
613
|
+
if (publicBaseUrl === void 0) return void 0;
|
|
614
|
+
const relative = key.replaceAll(/^\/+/g, "");
|
|
615
|
+
return relative.length === 0 ? publicBaseUrl : `${publicBaseUrl}/${relative}`;
|
|
616
|
+
},
|
|
617
|
+
destroy() {
|
|
618
|
+
if (destroyed) return;
|
|
619
|
+
destroyed = true;
|
|
620
|
+
if (ownClient) client.destroy();
|
|
621
|
+
},
|
|
622
|
+
async get(key) {
|
|
623
|
+
return (await this.getWithMeta(key))?.body;
|
|
624
|
+
},
|
|
625
|
+
async getWithMeta(key) {
|
|
626
|
+
try {
|
|
627
|
+
const response = await client.send(new GetObjectCommand({
|
|
628
|
+
Bucket: bucket,
|
|
629
|
+
Key: resolve(key)
|
|
630
|
+
}));
|
|
631
|
+
const body = await response.Body?.transformToByteArray();
|
|
632
|
+
if (body === void 0) return void 0;
|
|
633
|
+
return {
|
|
634
|
+
body,
|
|
635
|
+
meta: metaFromResponse(response)
|
|
636
|
+
};
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (isNotFound(error)) return void 0;
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
async stat(key) {
|
|
643
|
+
try {
|
|
644
|
+
return metaFromResponse(await client.send(new HeadObjectCommand({
|
|
645
|
+
Bucket: bucket,
|
|
646
|
+
Key: resolve(key)
|
|
647
|
+
})));
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (isNotFound(error)) return void 0;
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
async put(key, body, putOptions) {
|
|
654
|
+
const physicalKey = resolve(key);
|
|
655
|
+
try {
|
|
656
|
+
const response = await client.send(new PutObjectCommand({
|
|
657
|
+
Bucket: bucket,
|
|
658
|
+
Key: physicalKey,
|
|
659
|
+
Body: toBodyBytes(body),
|
|
660
|
+
...putOptions?.contentType !== void 0 && { ContentType: putOptions.contentType },
|
|
661
|
+
...putOptions?.contentEncoding !== void 0 && { ContentEncoding: putOptions.contentEncoding },
|
|
662
|
+
...putOptions?.cacheControl !== void 0 && { CacheControl: putOptions.cacheControl },
|
|
663
|
+
...putOptions?.metadata !== void 0 && { Metadata: putOptions.metadata },
|
|
664
|
+
...putOptions?.ifNotExists === true && { IfNoneMatch: "*" },
|
|
665
|
+
...putOptions?.ifMatch !== void 0 && { IfMatch: putOptions.ifMatch }
|
|
666
|
+
}));
|
|
667
|
+
return { ...response.ETag !== void 0 && { etag: response.ETag } };
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (isConditionalFailure(error)) throw new ConditionalWriteError(key);
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
async delete(key) {
|
|
674
|
+
await client.send(new DeleteObjectCommand({
|
|
675
|
+
Bucket: bucket,
|
|
676
|
+
Key: resolve(key)
|
|
677
|
+
}));
|
|
678
|
+
},
|
|
679
|
+
async list(prefix) {
|
|
680
|
+
const keys = [];
|
|
681
|
+
for await (const key of this.listAsync(prefix)) keys.push(key);
|
|
682
|
+
return keys;
|
|
683
|
+
},
|
|
684
|
+
async listPage(pageOptions = {}) {
|
|
685
|
+
const physicalPrefix = pageOptions.prefix === void 0 ? bindingPrefix.length === 0 ? void 0 : `${bindingPrefix}/` : resolve(pageOptions.prefix);
|
|
686
|
+
const response = await client.send(new ListObjectsV2Command({
|
|
687
|
+
Bucket: bucket,
|
|
688
|
+
...physicalPrefix !== void 0 && { Prefix: physicalPrefix },
|
|
689
|
+
...pageOptions.continuationToken !== void 0 && { ContinuationToken: pageOptions.continuationToken },
|
|
690
|
+
...pageOptions.maxKeys !== void 0 && { MaxKeys: pageOptions.maxKeys }
|
|
691
|
+
}));
|
|
692
|
+
const keys = [];
|
|
693
|
+
for (const item of response.Contents ?? []) {
|
|
694
|
+
if (item.Key === void 0) continue;
|
|
695
|
+
keys.push(stripPrefix(bindingPrefix, item.Key));
|
|
696
|
+
}
|
|
697
|
+
const isTruncated = response.IsTruncated === true;
|
|
698
|
+
return {
|
|
699
|
+
keys,
|
|
700
|
+
isTruncated,
|
|
701
|
+
...isTruncated && response.NextContinuationToken !== void 0 && { continuationToken: response.NextContinuationToken }
|
|
702
|
+
};
|
|
703
|
+
},
|
|
704
|
+
async *listAsync(prefix) {
|
|
705
|
+
let token;
|
|
706
|
+
do {
|
|
707
|
+
const page = await this.listPage({
|
|
708
|
+
...prefix !== void 0 && { prefix },
|
|
709
|
+
...token !== void 0 && { continuationToken: token }
|
|
710
|
+
});
|
|
711
|
+
for (const key of page.keys) yield key;
|
|
712
|
+
token = page.continuationToken;
|
|
713
|
+
} while (token !== void 0);
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/locator/createDappLocator.ts
|
|
719
|
+
const DEFAULT_BINDINGS = {
|
|
720
|
+
data: { bucket: "data" },
|
|
721
|
+
state: { bucket: "state" },
|
|
722
|
+
index: { bucket: "index" }
|
|
723
|
+
};
|
|
724
|
+
const ROLE_MONIKERS = {
|
|
725
|
+
data: DappDataStoreMoniker,
|
|
726
|
+
state: DappStateStoreMoniker,
|
|
727
|
+
index: DappIndexStoreMoniker
|
|
728
|
+
};
|
|
729
|
+
function resolveCredentials(options) {
|
|
730
|
+
if (options.credentials !== void 0) return options.credentials;
|
|
731
|
+
if (options.accessKeyId !== void 0 || options.secretAccessKey !== void 0) return {
|
|
732
|
+
accessKeyId: options.accessKeyId ?? "S3RVER",
|
|
733
|
+
secretAccessKey: options.secretAccessKey ?? "S3RVER"
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function resolveSharedClient(options) {
|
|
737
|
+
const credentials = resolveCredentials(options);
|
|
738
|
+
if (options.client !== void 0) return {
|
|
739
|
+
client: options.client,
|
|
740
|
+
ownClient: options.ownClient ?? false
|
|
741
|
+
};
|
|
742
|
+
return resolveS3Client({
|
|
743
|
+
...options.endpoint !== void 0 && { endpoint: options.endpoint },
|
|
744
|
+
...options.region !== void 0 && { region: options.region },
|
|
745
|
+
...options.forcePathStyle !== void 0 && { forcePathStyle: options.forcePathStyle },
|
|
746
|
+
...credentials !== void 0 && { credentials },
|
|
747
|
+
ownClient: options.ownClient
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
function resolveRoleClient(role, options, shared) {
|
|
751
|
+
const override = options.clients?.[role];
|
|
752
|
+
if (override === void 0) return shared;
|
|
753
|
+
if (typeof override.send === "function") return {
|
|
754
|
+
client: override,
|
|
755
|
+
ownClient: false
|
|
756
|
+
};
|
|
757
|
+
return resolveS3Client({
|
|
758
|
+
...override,
|
|
759
|
+
ownClient: override.ownClient ?? true
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Builds the process-wide locator: one instance, shared across every actor
|
|
764
|
+
* (xl1-cli style). It pre-registers an S3-backed store per logical role using
|
|
765
|
+
* either the default local bucket names or explicit production bindings.
|
|
766
|
+
* Extra providers can be `register`ed before actors are created.
|
|
767
|
+
*
|
|
768
|
+
* Prefer a shared {@link client} when all roles share one principal; use
|
|
769
|
+
* {@link CreateDappLocatorOptions.clients} for distinct least-privilege
|
|
770
|
+
* credentials per role. Call `destroy()` on shutdown when the locator owns
|
|
771
|
+
* any client it constructed.
|
|
772
|
+
*/
|
|
773
|
+
function createDappLocator(options) {
|
|
774
|
+
const logger = options.logger ?? new ConsoleLogger();
|
|
775
|
+
const bindings = options.bindings ?? DEFAULT_BINDINGS;
|
|
776
|
+
const shared = resolveSharedClient(options);
|
|
777
|
+
const readOnlyData = options.readOnlyData !== false;
|
|
778
|
+
const roles = [
|
|
779
|
+
"data",
|
|
780
|
+
"state",
|
|
781
|
+
"index"
|
|
782
|
+
];
|
|
783
|
+
const instances = /* @__PURE__ */ new Map();
|
|
784
|
+
const stores = [];
|
|
785
|
+
const ownedClients = [];
|
|
786
|
+
const seenClients = /* @__PURE__ */ new Set();
|
|
787
|
+
const trackClient = (owned) => {
|
|
788
|
+
if (!owned.ownClient || seenClients.has(owned.client)) return;
|
|
789
|
+
seenClients.add(owned.client);
|
|
790
|
+
ownedClients.push(owned);
|
|
791
|
+
};
|
|
792
|
+
trackClient(shared);
|
|
793
|
+
for (const role of roles) {
|
|
794
|
+
const binding = bindings[role];
|
|
795
|
+
const roleClient = resolveRoleClient(role, options, shared);
|
|
796
|
+
trackClient(roleClient);
|
|
797
|
+
let store = createS3BucketStore({
|
|
798
|
+
role,
|
|
799
|
+
binding,
|
|
800
|
+
client: roleClient.client,
|
|
801
|
+
ownClient: false
|
|
802
|
+
});
|
|
803
|
+
if (role === "data" && readOnlyData) store = asReadOnlyWriter(store);
|
|
804
|
+
stores.push(store);
|
|
805
|
+
instances.set(ROLE_MONIKERS[role], store);
|
|
806
|
+
}
|
|
807
|
+
let destroyed = false;
|
|
808
|
+
return {
|
|
809
|
+
context: { logger },
|
|
810
|
+
has(moniker) {
|
|
811
|
+
return instances.has(moniker);
|
|
812
|
+
},
|
|
813
|
+
register(moniker, instance) {
|
|
814
|
+
instances.set(moniker, instance);
|
|
815
|
+
},
|
|
816
|
+
async getInstance(moniker) {
|
|
817
|
+
const found = instances.get(moniker);
|
|
818
|
+
if (found === void 0) throw new Error(`No provider registered for moniker "${moniker}"`);
|
|
819
|
+
return found;
|
|
820
|
+
},
|
|
821
|
+
async tryGetInstance(moniker) {
|
|
822
|
+
return instances.get(moniker);
|
|
823
|
+
},
|
|
824
|
+
destroy() {
|
|
825
|
+
if (destroyed) return;
|
|
826
|
+
destroyed = true;
|
|
827
|
+
for (const store of stores) store.destroy();
|
|
828
|
+
for (const owned of ownedClients) if (owned.ownClient) owned.client.destroy();
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/reducer/DappReducer.ts
|
|
834
|
+
/**
|
|
835
|
+
* Default reducer: does nothing. The local harness runs the full timer/provider
|
|
836
|
+
* spine with this in place; production entrypoints must supply a real reducer.
|
|
837
|
+
* **Development only** — not a production indexer.
|
|
838
|
+
*/
|
|
839
|
+
const noopReducer = {
|
|
840
|
+
name: "noop",
|
|
841
|
+
async reduce() {}
|
|
842
|
+
};
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/bin/loadReducer.ts
|
|
845
|
+
const require = createRequire(import.meta.url);
|
|
846
|
+
function isPathSpec(spec) {
|
|
847
|
+
return spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("file:") || path.isAbsolute(spec) || spec.endsWith(".mjs") || spec.endsWith(".js") || spec.endsWith(".cjs");
|
|
848
|
+
}
|
|
849
|
+
function resolveFileUrl(spec) {
|
|
850
|
+
if (spec.startsWith("file:")) return spec;
|
|
851
|
+
const absolute = path.isAbsolute(spec) ? spec : path.resolve(process.cwd(), spec);
|
|
852
|
+
if (!existsSync(absolute)) throw new Error(`Reducer module not found: ${absolute}`);
|
|
853
|
+
return pathToFileURL(absolute).href;
|
|
854
|
+
}
|
|
855
|
+
function resolvePackageUrl(spec) {
|
|
856
|
+
try {
|
|
857
|
+
return pathToFileURL(require.resolve(spec, { paths: [process.cwd()] })).href;
|
|
858
|
+
} catch {
|
|
859
|
+
throw new Error(`Could not resolve reducer package "${spec}" from ${process.cwd()}. Pass a path to a .mjs file or an installed package export.`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function pickReducer(mod, exportName) {
|
|
863
|
+
if (exportName !== void 0 && exportName.length > 0) {
|
|
864
|
+
if (!(exportName in mod)) throw new Error(`Reducer module has no export named "${exportName}"`);
|
|
865
|
+
return mod[exportName];
|
|
866
|
+
}
|
|
867
|
+
if (mod.default !== void 0) return mod.default;
|
|
868
|
+
if (mod.reducer !== void 0) return mod.reducer;
|
|
869
|
+
throw new Error("Reducer module must default-export a DappReducer, or export `reducer`, or set DAPP_REDUCER_EXPORT / --reducer-export to a named export");
|
|
870
|
+
}
|
|
871
|
+
function assertReducer(value, label) {
|
|
872
|
+
if (value === null || typeof value !== "object") throw new TypeError(`${label} is not an object`);
|
|
873
|
+
const candidate = value;
|
|
874
|
+
if (typeof candidate.name !== "string" || candidate.name.length === 0) throw new TypeError(`${label} must have a non-empty string "name"`);
|
|
875
|
+
if (typeof candidate.reduce !== "function") throw new TypeError(`${label} must have a "reduce" function`);
|
|
876
|
+
return candidate;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Dynamically load a project-owned reducer for the local dapp daemon.
|
|
880
|
+
* Supports filesystem `.mjs`/`.js` paths and package export strings.
|
|
881
|
+
* TypeScript sources are not supported — build to ESM first.
|
|
882
|
+
*/
|
|
883
|
+
async function loadReducer(options) {
|
|
884
|
+
return assertReducer(pickReducer(await (isPathSpec(options.spec) ? import(resolveFileUrl(options.spec)) : import(resolvePackageUrl(options.spec))), options.exportName), `Reducer from ${options.spec}`);
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/bin/dappServer.ts
|
|
888
|
+
/**
|
|
889
|
+
* **Local development only.** Composed dapp daemon: boots the embedded
|
|
890
|
+
* S3-compatible storage server (from `@ariestools/aries-dapp-serve`) and, in
|
|
891
|
+
* the same process, a shared process-wide locator plus DappActor(s).
|
|
892
|
+
* Spawned by `aries dapp up`.
|
|
893
|
+
*
|
|
894
|
+
* Optional `DAPP_REDUCER` loads a project-owned ESM reducer (`.mjs` path or
|
|
895
|
+
* package export). Without it, `noopReducer` runs so the spine still works.
|
|
896
|
+
*
|
|
897
|
+
* Production indexers should **not** use this binary. Compose `dapp-core` from
|
|
898
|
+
* a project-owned entrypoint with real R2/S3 bindings and a real reducer —
|
|
899
|
+
* see the package README and `examples/`.
|
|
900
|
+
*/
|
|
901
|
+
const PORT = Number(process.env.DAPP_PORT ?? 8801);
|
|
902
|
+
const HOST = process.env.HOST ?? "127.0.0.1";
|
|
903
|
+
const BACKING = process.env.DAPP_BACKING ?? "memory";
|
|
904
|
+
const PUBLIC_HOST = process.env.DAPP_PUBLIC_HOST;
|
|
905
|
+
const TLS_CERT_PATH = process.env.DAPP_TLS_CERT;
|
|
906
|
+
const TLS_KEY_PATH = process.env.DAPP_TLS_KEY;
|
|
907
|
+
const REDUCE_INTERVAL_MS = Number(process.env.DAPP_REDUCE_INTERVAL_MS ?? 5e3);
|
|
908
|
+
const FIRST_RUN_DELAY_MS = Number(process.env.DAPP_FIRST_RUN_DELAY_MS ?? 0);
|
|
909
|
+
const REDUCER_SPEC = process.env.DAPP_REDUCER;
|
|
910
|
+
const REDUCER_EXPORT = process.env.DAPP_REDUCER_EXPORT;
|
|
911
|
+
const DATA_DIR = process.env.DAPP_DATA_DIR ?? path.join(process.env.ARIES_HOME ?? path.join(homedir(), ".aries"), "dapp", "data");
|
|
912
|
+
async function resolveDaemonReducer() {
|
|
913
|
+
if (REDUCER_SPEC === void 0 || REDUCER_SPEC.length === 0) return noopReducer;
|
|
914
|
+
return await loadReducer({
|
|
915
|
+
spec: REDUCER_SPEC,
|
|
916
|
+
...REDUCER_EXPORT !== void 0 && REDUCER_EXPORT.length > 0 && { exportName: REDUCER_EXPORT }
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
async function main() {
|
|
920
|
+
if (!isDappBackingKind(BACKING)) throw new Error(`Unsupported DAPP_BACKING '${BACKING}' (supported: ${DAPP_BACKINGS.join(", ")})`);
|
|
921
|
+
if (TLS_CERT_PATH === void 0 !== (TLS_KEY_PATH === void 0)) throw new Error("DAPP_TLS_CERT and DAPP_TLS_KEY must be provided together");
|
|
922
|
+
const reducer = await resolveDaemonReducer();
|
|
923
|
+
const backing = resolveBacking(BACKING, { ...BACKING === "disk" && { directory: DATA_DIR } });
|
|
924
|
+
const server = await startDappServer({
|
|
925
|
+
backing,
|
|
926
|
+
host: HOST,
|
|
927
|
+
port: PORT,
|
|
928
|
+
publicHost: PUBLIC_HOST,
|
|
929
|
+
tls: TLS_CERT_PATH === void 0 || TLS_KEY_PATH === void 0 ? void 0 : {
|
|
930
|
+
cert: readFileSync(TLS_CERT_PATH),
|
|
931
|
+
key: readFileSync(TLS_KEY_PATH)
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
const logger = new ConsoleLogger();
|
|
935
|
+
const locator = createDappLocator({
|
|
936
|
+
endpoint: server.baseUrl,
|
|
937
|
+
logger,
|
|
938
|
+
readOnlyData: false
|
|
939
|
+
});
|
|
940
|
+
process.env.DAPP_PUBLICATION_SAFETY ??= "unfenced";
|
|
941
|
+
process.env.DAPP_ENDPOINT ??= server.baseUrl;
|
|
942
|
+
const actors = await bootDappActors(locator, [{
|
|
943
|
+
name: "DappActor",
|
|
944
|
+
reducer,
|
|
945
|
+
reduceIntervalMs: REDUCE_INTERVAL_MS,
|
|
946
|
+
firstRunDelayMs: FIRST_RUN_DELAY_MS,
|
|
947
|
+
publishStatus: reducer.name !== "noop"
|
|
948
|
+
}]);
|
|
949
|
+
const versionSuffix = reducer.version === void 0 ? "" : `@${reducer.version}`;
|
|
950
|
+
const reducerLine = REDUCER_SPEC === void 0 ? "\nreducer: noop (pass DAPP_REDUCER / --reducer for real logic)" : `\nreducer: ${REDUCER_SPEC}`;
|
|
951
|
+
console.log(`[dev-only] dapp server listening at ${server.baseUrl}\n` + DAPP_BUCKETS.map((bucket) => ` /${bucket.padEnd(5)} ${server.baseUrl}/${bucket}`).join("\n") + `\nbacking: ${backing.description}\nactors: ${actors.length} (reducer '${reducer.name}'${versionSuffix}, every ${REDUCE_INTERVAL_MS}ms)` + reducerLine);
|
|
952
|
+
const shutdown = async () => {
|
|
953
|
+
for (const actor of actors) await actor.stop();
|
|
954
|
+
locator.destroy();
|
|
955
|
+
await server.close();
|
|
956
|
+
process.exit(0);
|
|
957
|
+
};
|
|
958
|
+
process.on("SIGINT", () => {
|
|
959
|
+
shutdown();
|
|
960
|
+
});
|
|
961
|
+
process.on("SIGTERM", () => {
|
|
962
|
+
shutdown();
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
main().catch((error) => {
|
|
966
|
+
console.error(error);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
});
|
|
969
|
+
//#endregion
|
|
970
|
+
export {};
|