@ariestools/aries-dapp-core 0.1.7 → 0.1.9
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 +326 -32
- package/dist/bin/dappServer.mjs +685 -104
- package/dist/node/DappActor.d.ts +28 -7
- package/dist/node/DappActor.d.ts.map +1 -1
- package/dist/node/actor/AbstractActor.d.ts +13 -8
- package/dist/node/actor/AbstractActor.d.ts.map +1 -1
- package/dist/node/actor/types.d.ts +7 -0
- package/dist/node/actor/types.d.ts.map +1 -1
- package/dist/node/bin/dappServer.d.ts +11 -4
- package/dist/node/bin/dappServer.d.ts.map +1 -1
- package/dist/node/bin/loadReducer.d.ts +17 -0
- package/dist/node/bin/loadReducer.d.ts.map +1 -0
- package/dist/node/boot/bootDappActors.d.ts +12 -1
- package/dist/node/boot/bootDappActors.d.ts.map +1 -1
- package/dist/node/consumer/api.d.ts +56 -0
- package/dist/node/consumer/api.d.ts.map +1 -0
- package/dist/node/consumer/hash.d.ts +6 -0
- package/dist/node/consumer/hash.d.ts.map +1 -0
- package/dist/node/consumer/parse.d.ts +18 -0
- package/dist/node/consumer/parse.d.ts.map +1 -0
- package/dist/node/consumer.d.ts +9 -0
- package/dist/node/consumer.d.ts.map +1 -0
- package/dist/node/consumer.mjs +330 -0
- package/dist/node/consumer.mjs.map +7 -0
- package/dist/node/examples/productionComposition.d.ts +118 -0
- package/dist/node/examples/productionComposition.d.ts.map +1 -0
- package/dist/node/index.d.ts +25 -3
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.mjs +1730 -68
- package/dist/node/index.mjs.map +4 -4
- package/dist/node/locator/createDappLocator.d.ts +32 -5
- package/dist/node/locator/createDappLocator.d.ts.map +1 -1
- package/dist/node/providers/DappBucketStore.d.ts +26 -21
- package/dist/node/providers/DappBucketStore.d.ts.map +1 -1
- package/dist/node/providers/capabilities.d.ts +49 -0
- package/dist/node/providers/capabilities.d.ts.map +1 -0
- package/dist/node/providers/errors.d.ts +11 -0
- package/dist/node/providers/errors.d.ts.map +1 -0
- package/dist/node/providers/hashBytes.d.ts +3 -0
- package/dist/node/providers/hashBytes.d.ts.map +1 -0
- package/dist/node/providers/joinKey.d.ts +11 -0
- package/dist/node/providers/joinKey.d.ts.map +1 -0
- package/dist/node/providers/memoryBucketStore.d.ts +14 -0
- package/dist/node/providers/memoryBucketStore.d.ts.map +1 -0
- package/dist/node/providers/types.d.ts +99 -0
- package/dist/node/providers/types.d.ts.map +1 -0
- package/dist/node/publication/canonicalHead.d.ts +7 -0
- package/dist/node/publication/canonicalHead.d.ts.map +1 -0
- package/dist/node/publication/commitHead.d.ts +9 -0
- package/dist/node/publication/commitHead.d.ts.map +1 -0
- package/dist/node/publication/constants.d.ts +27 -0
- package/dist/node/publication/constants.d.ts.map +1 -0
- package/dist/node/publication/gc.d.ts +42 -0
- package/dist/node/publication/gc.d.ts.map +1 -0
- package/dist/node/publication/incremental.d.ts +83 -0
- package/dist/node/publication/incremental.d.ts.map +1 -0
- package/dist/node/publication/publishGeneration.d.ts +18 -0
- package/dist/node/publication/publishGeneration.d.ts.map +1 -0
- package/dist/node/publication/putCreateOnly.d.ts +11 -0
- package/dist/node/publication/putCreateOnly.d.ts.map +1 -0
- package/dist/node/publication/releases.d.ts +49 -0
- package/dist/node/publication/releases.d.ts.map +1 -0
- package/dist/node/publication/safety.d.ts +55 -0
- package/dist/node/publication/safety.d.ts.map +1 -0
- package/dist/node/publication/shard.d.ts +7 -0
- package/dist/node/publication/shard.d.ts.map +1 -0
- package/dist/node/publication/status.d.ts +27 -0
- package/dist/node/publication/status.d.ts.map +1 -0
- package/dist/node/publication/statusPatch.d.ts +10 -0
- package/dist/node/publication/statusPatch.d.ts.map +1 -0
- package/dist/node/publication/types.d.ts +138 -0
- package/dist/node/publication/types.d.ts.map +1 -0
- package/dist/node/publication/writeGenerationObjects.d.ts +5 -0
- package/dist/node/publication/writeGenerationObjects.d.ts.map +1 -0
- package/dist/node/reducer/DappReducer.d.ts +37 -14
- package/dist/node/reducer/DappReducer.d.ts.map +1 -1
- package/examples/countFactsReducer.mjs +96 -0
- package/package.json +16 -8
package/dist/bin/dappServer.mjs
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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";
|
|
3
6
|
import { DAPP_BACKINGS, DAPP_BUCKETS, isDappBackingKind, resolveBacking, startDappServer } from "@ariestools/aries-dapp-serve";
|
|
4
7
|
import { AbstractCreatable, ConsoleLogger, IdLogger, assertEx, creatable } from "@ariestools/sdk";
|
|
5
|
-
import { GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
8
|
+
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
6
10
|
//#region src/actor/AbstractActor.ts
|
|
7
|
-
function createDeferred() {
|
|
11
|
+
function createDeferred$1() {
|
|
8
12
|
let resolve;
|
|
9
13
|
let reject;
|
|
14
|
+
const promise = new Promise((res, rej) => {
|
|
15
|
+
resolve = res;
|
|
16
|
+
reject = rej;
|
|
17
|
+
});
|
|
18
|
+
promise.catch(() => {});
|
|
10
19
|
return {
|
|
11
|
-
promise
|
|
12
|
-
resolve = res;
|
|
13
|
-
reject = rej;
|
|
14
|
-
}),
|
|
20
|
+
promise,
|
|
15
21
|
reject,
|
|
16
22
|
resolve
|
|
17
23
|
};
|
|
@@ -32,9 +38,10 @@ function createDeferred() {
|
|
|
32
38
|
var AbstractActor = class extends AbstractCreatable {
|
|
33
39
|
_intervals = /* @__PURE__ */ new Map();
|
|
34
40
|
_timeouts = /* @__PURE__ */ new Map();
|
|
41
|
+
_abortController = new AbortController();
|
|
35
42
|
_idLogger;
|
|
36
43
|
_inFlight = /* @__PURE__ */ new Map();
|
|
37
|
-
_readyDeferred = createDeferred();
|
|
44
|
+
_readyDeferred = createDeferred$1();
|
|
38
45
|
_readyError;
|
|
39
46
|
_readyState = "pending";
|
|
40
47
|
get logger() {
|
|
@@ -53,6 +60,10 @@ var AbstractActor = class extends AbstractCreatable {
|
|
|
53
60
|
get locator() {
|
|
54
61
|
return this.params.locator;
|
|
55
62
|
}
|
|
63
|
+
/** Abort signal cancelled when the actor stops (or is replaced on restart). */
|
|
64
|
+
get signal() {
|
|
65
|
+
return this._abortController.signal;
|
|
66
|
+
}
|
|
56
67
|
static async paramsHandler(params) {
|
|
57
68
|
const inParams = params ?? {};
|
|
58
69
|
const baseParams = await super.paramsHandler({
|
|
@@ -65,11 +76,14 @@ var AbstractActor = class extends AbstractCreatable {
|
|
|
65
76
|
locator
|
|
66
77
|
};
|
|
67
78
|
}
|
|
79
|
+
/** Override to prove the actor can do useful work. Default: no-op. */
|
|
80
|
+
async readyHandler() {}
|
|
68
81
|
/**
|
|
69
|
-
* Register a recurring task. The
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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).
|
|
73
87
|
*/
|
|
74
88
|
registerTimer(timerName, callback, dueTimeMs, periodMs) {
|
|
75
89
|
if (this.status !== "starting") {
|
|
@@ -77,7 +91,7 @@ var AbstractActor = class extends AbstractCreatable {
|
|
|
77
91
|
return;
|
|
78
92
|
}
|
|
79
93
|
const tick = () => {
|
|
80
|
-
if (this.status !== "started"
|
|
94
|
+
if (this.status !== "started") return;
|
|
81
95
|
if (this._inFlight.has(timerName)) {
|
|
82
96
|
this.logger.warn(`Skipping timer '${this.name}:${timerName}' because the previous run is still in flight.`);
|
|
83
97
|
return;
|
|
@@ -99,13 +113,15 @@ var AbstractActor = class extends AbstractCreatable {
|
|
|
99
113
|
}));
|
|
100
114
|
};
|
|
101
115
|
const timeoutId = setTimeout(() => {
|
|
116
|
+
if (this.status !== "started") return;
|
|
102
117
|
this._intervals.set(timerName, setInterval(tick, periodMs));
|
|
118
|
+
tick();
|
|
103
119
|
}, dueTimeMs);
|
|
104
120
|
this._timeouts.set(timerName, timeoutId);
|
|
105
121
|
}
|
|
106
122
|
/**
|
|
107
|
-
* Run the warm-pass once.
|
|
108
|
-
*
|
|
123
|
+
* Run the warm-pass once. The standard boot path invokes this after
|
|
124
|
+
* `start()` so readiness either resolves or rejects.
|
|
109
125
|
*/
|
|
110
126
|
async runReadyHandler() {
|
|
111
127
|
if (this._readyState !== "pending") return;
|
|
@@ -121,22 +137,81 @@ var AbstractActor = class extends AbstractCreatable {
|
|
|
121
137
|
throw err;
|
|
122
138
|
}
|
|
123
139
|
}
|
|
140
|
+
async startHandler() {
|
|
141
|
+
if (this._abortController.signal.aborted) this._abortController = new AbortController();
|
|
142
|
+
await super.startHandler();
|
|
143
|
+
}
|
|
124
144
|
async stopHandler() {
|
|
125
145
|
await super.stopHandler();
|
|
146
|
+
this._abortController.abort();
|
|
126
147
|
for (const timeoutRef of this._timeouts.values()) clearTimeout(timeoutRef);
|
|
127
148
|
this._timeouts.clear();
|
|
128
149
|
for (const intervalRef of this._intervals.values()) clearInterval(intervalRef);
|
|
129
150
|
this._intervals.clear();
|
|
130
|
-
|
|
151
|
+
const inFlight = [...this._inFlight.values()];
|
|
131
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
|
+
})]);
|
|
132
161
|
}
|
|
133
162
|
async whenReady() {
|
|
134
163
|
await this._readyDeferred.promise;
|
|
135
164
|
}
|
|
136
|
-
/** Override to prove the actor can do useful work. Default: no-op. */
|
|
137
|
-
async readyHandler() {}
|
|
138
165
|
};
|
|
139
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
|
|
140
215
|
//#region src/providers/monikers.ts
|
|
141
216
|
/**
|
|
142
217
|
* Provider monikers for the three dapp buckets. An actor declares these in its
|
|
@@ -146,6 +221,108 @@ const DappDataStoreMoniker = "DappDataStore";
|
|
|
146
221
|
const DappStateStoreMoniker = "DappStateStore";
|
|
147
222
|
const DappIndexStoreMoniker = "DappIndexStore";
|
|
148
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
|
|
149
326
|
//#region \0@oxc-project+runtime@0.140.0/helpers/esm/decorate.js
|
|
150
327
|
function __decorate(decorators, target, key, desc) {
|
|
151
328
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -157,6 +334,29 @@ function __decorate(decorators, target, key, desc) {
|
|
|
157
334
|
//#region src/DappActor.ts
|
|
158
335
|
const DEFAULT_REDUCE_INTERVAL_MS = 5e3;
|
|
159
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
|
+
}
|
|
160
360
|
let DappActor = class DappActor extends AbstractActor {
|
|
161
361
|
static dependencies = [
|
|
162
362
|
DappDataStoreMoniker,
|
|
@@ -166,28 +366,88 @@ let DappActor = class DappActor extends AbstractActor {
|
|
|
166
366
|
_data;
|
|
167
367
|
_index;
|
|
168
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
|
+
}
|
|
169
376
|
async createHandler() {
|
|
170
377
|
await super.createHandler();
|
|
171
|
-
|
|
378
|
+
const dataWriter = await this.locator.getInstance(DappDataStoreMoniker);
|
|
379
|
+
this._data = asReadOnly(dataWriter);
|
|
172
380
|
this._state = await this.locator.getInstance(DappStateStoreMoniker);
|
|
173
381
|
this._index = await this.locator.getInstance(DappIndexStoreMoniker);
|
|
174
382
|
}
|
|
383
|
+
async readyHandler() {
|
|
384
|
+
await this._firstPass.promise;
|
|
385
|
+
}
|
|
175
386
|
async startHandler() {
|
|
176
387
|
await super.startHandler();
|
|
177
388
|
const interval = this.params.reduceIntervalMs ?? DEFAULT_REDUCE_INTERVAL_MS;
|
|
178
389
|
const firstRunDelay = this.params.firstRunDelayMs ?? DEFAULT_FIRST_RUN_DELAY_MS;
|
|
179
|
-
this.registerTimer(
|
|
390
|
+
this.registerTimer(REDUCE_TIMER, async () => {
|
|
180
391
|
await this.runReducePass();
|
|
181
392
|
}, firstRunDelay, interval);
|
|
182
|
-
this.logger.info(`DappActor started: reducer '${this.params.reducer.name}' every ${interval}ms`);
|
|
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);
|
|
183
410
|
}
|
|
184
411
|
async runReducePass() {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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);
|
|
191
451
|
}
|
|
192
452
|
};
|
|
193
453
|
DappActor = __decorate([creatable()], DappActor);
|
|
@@ -195,112 +455,356 @@ DappActor = __decorate([creatable()], DappActor);
|
|
|
195
455
|
//#region src/boot/bootDappActors.ts
|
|
196
456
|
/**
|
|
197
457
|
* Creates and starts one {@link DappActor} per spec, all sharing the single
|
|
198
|
-
* process-wide locator.
|
|
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.
|
|
199
461
|
*/
|
|
200
462
|
async function bootDappActors(locator, specs) {
|
|
201
463
|
const actors = [];
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
|
212
501
|
}
|
|
213
|
-
|
|
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(/\/+$/, "");
|
|
214
530
|
}
|
|
215
531
|
//#endregion
|
|
216
532
|
//#region src/providers/DappBucketStore.ts
|
|
217
533
|
const DEFAULT_CREDENTIAL = "S3RVER";
|
|
534
|
+
const DEFAULT_REGION = "us-east-1";
|
|
218
535
|
function isNotFound(error) {
|
|
219
536
|
const name = error.name;
|
|
220
|
-
|
|
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
|
+
};
|
|
221
587
|
}
|
|
222
588
|
/**
|
|
223
|
-
* S3-backed {@link DappBucketStore}
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* this client trusts it without any per-request handler configuration.
|
|
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.
|
|
228
593
|
*/
|
|
229
594
|
function createS3BucketStore(options) {
|
|
230
|
-
const {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
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
|
+
}
|
|
240
604
|
return {
|
|
605
|
+
role,
|
|
241
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
|
+
},
|
|
242
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) {
|
|
243
643
|
try {
|
|
244
|
-
return
|
|
644
|
+
return metaFromResponse(await client.send(new HeadObjectCommand({
|
|
245
645
|
Bucket: bucket,
|
|
246
|
-
Key: key
|
|
247
|
-
})))
|
|
646
|
+
Key: resolve(key)
|
|
647
|
+
})));
|
|
248
648
|
} catch (error) {
|
|
249
649
|
if (isNotFound(error)) return void 0;
|
|
250
650
|
throw error;
|
|
251
651
|
}
|
|
252
652
|
},
|
|
253
|
-
async put(key, body) {
|
|
254
|
-
|
|
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({
|
|
255
675
|
Bucket: bucket,
|
|
256
|
-
Key: key
|
|
257
|
-
Body: body
|
|
676
|
+
Key: resolve(key)
|
|
258
677
|
}));
|
|
259
678
|
},
|
|
260
679
|
async list(prefix) {
|
|
261
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) {
|
|
262
705
|
let token;
|
|
263
706
|
do {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
token = response.IsTruncated === true ? response.NextContinuationToken : void 0;
|
|
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;
|
|
271
713
|
} while (token !== void 0);
|
|
272
|
-
return keys;
|
|
273
714
|
}
|
|
274
715
|
};
|
|
275
716
|
}
|
|
276
717
|
//#endregion
|
|
277
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
|
+
}
|
|
278
762
|
/**
|
|
279
763
|
* Builds the process-wide locator: one instance, shared across every actor
|
|
280
|
-
* (xl1-cli style). It pre-registers an S3-backed store per
|
|
281
|
-
*
|
|
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.
|
|
282
772
|
*/
|
|
283
773
|
function createDappLocator(options) {
|
|
284
774
|
const logger = options.logger ?? new ConsoleLogger();
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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);
|
|
289
791
|
};
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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;
|
|
304
808
|
return {
|
|
305
809
|
context: { logger },
|
|
306
810
|
has(moniker) {
|
|
@@ -316,27 +820,83 @@ function createDappLocator(options) {
|
|
|
316
820
|
},
|
|
317
821
|
async tryGetInstance(moniker) {
|
|
318
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();
|
|
319
829
|
}
|
|
320
830
|
};
|
|
321
831
|
}
|
|
322
832
|
//#endregion
|
|
323
833
|
//#region src/reducer/DappReducer.ts
|
|
324
834
|
/**
|
|
325
|
-
* Default reducer: does nothing. The harness runs the full timer/provider
|
|
326
|
-
* spine with this in place; supply a real reducer
|
|
327
|
-
*
|
|
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.
|
|
328
838
|
*/
|
|
329
839
|
const noopReducer = {
|
|
330
840
|
name: "noop",
|
|
331
841
|
async reduce() {}
|
|
332
842
|
};
|
|
333
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
|
|
334
887
|
//#region src/bin/dappServer.ts
|
|
335
888
|
/**
|
|
336
|
-
* Composed dapp daemon: boots the
|
|
337
|
-
* `@ariestools/aries-dapp-serve`) and, in
|
|
338
|
-
* process-wide locator plus
|
|
339
|
-
*
|
|
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/`.
|
|
340
900
|
*/
|
|
341
901
|
const PORT = Number(process.env.DAPP_PORT ?? 8801);
|
|
342
902
|
const HOST = process.env.HOST ?? "127.0.0.1";
|
|
@@ -345,10 +905,22 @@ const PUBLIC_HOST = process.env.DAPP_PUBLIC_HOST;
|
|
|
345
905
|
const TLS_CERT_PATH = process.env.DAPP_TLS_CERT;
|
|
346
906
|
const TLS_KEY_PATH = process.env.DAPP_TLS_KEY;
|
|
347
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
|
+
}
|
|
348
919
|
async function main() {
|
|
349
920
|
if (!isDappBackingKind(BACKING)) throw new Error(`Unsupported DAPP_BACKING '${BACKING}' (supported: ${DAPP_BACKINGS.join(", ")})`);
|
|
350
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");
|
|
351
|
-
const
|
|
922
|
+
const reducer = await resolveDaemonReducer();
|
|
923
|
+
const backing = resolveBacking(BACKING, { ...BACKING === "disk" && { directory: DATA_DIR } });
|
|
352
924
|
const server = await startDappServer({
|
|
353
925
|
backing,
|
|
354
926
|
host: HOST,
|
|
@@ -360,17 +932,26 @@ async function main() {
|
|
|
360
932
|
}
|
|
361
933
|
});
|
|
362
934
|
const logger = new ConsoleLogger();
|
|
363
|
-
const
|
|
935
|
+
const locator = createDappLocator({
|
|
364
936
|
endpoint: server.baseUrl,
|
|
365
|
-
logger
|
|
366
|
-
|
|
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, [{
|
|
367
943
|
name: "DappActor",
|
|
368
|
-
reducer
|
|
369
|
-
reduceIntervalMs: REDUCE_INTERVAL_MS
|
|
944
|
+
reducer,
|
|
945
|
+
reduceIntervalMs: REDUCE_INTERVAL_MS,
|
|
946
|
+
firstRunDelayMs: FIRST_RUN_DELAY_MS,
|
|
947
|
+
publishStatus: reducer.name !== "noop"
|
|
370
948
|
}]);
|
|
371
|
-
|
|
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);
|
|
372
952
|
const shutdown = async () => {
|
|
373
953
|
for (const actor of actors) await actor.stop();
|
|
954
|
+
locator.destroy();
|
|
374
955
|
await server.close();
|
|
375
956
|
process.exit(0);
|
|
376
957
|
};
|