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