@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.
Files changed (78) hide show
  1. package/README.md +326 -32
  2. package/dist/bin/dappServer.mjs +685 -104
  3. package/dist/node/DappActor.d.ts +28 -7
  4. package/dist/node/DappActor.d.ts.map +1 -1
  5. package/dist/node/actor/AbstractActor.d.ts +13 -8
  6. package/dist/node/actor/AbstractActor.d.ts.map +1 -1
  7. package/dist/node/actor/types.d.ts +7 -0
  8. package/dist/node/actor/types.d.ts.map +1 -1
  9. package/dist/node/bin/dappServer.d.ts +11 -4
  10. package/dist/node/bin/dappServer.d.ts.map +1 -1
  11. package/dist/node/bin/loadReducer.d.ts +17 -0
  12. package/dist/node/bin/loadReducer.d.ts.map +1 -0
  13. package/dist/node/boot/bootDappActors.d.ts +12 -1
  14. package/dist/node/boot/bootDappActors.d.ts.map +1 -1
  15. package/dist/node/consumer/api.d.ts +56 -0
  16. package/dist/node/consumer/api.d.ts.map +1 -0
  17. package/dist/node/consumer/hash.d.ts +6 -0
  18. package/dist/node/consumer/hash.d.ts.map +1 -0
  19. package/dist/node/consumer/parse.d.ts +18 -0
  20. package/dist/node/consumer/parse.d.ts.map +1 -0
  21. package/dist/node/consumer.d.ts +9 -0
  22. package/dist/node/consumer.d.ts.map +1 -0
  23. package/dist/node/consumer.mjs +330 -0
  24. package/dist/node/consumer.mjs.map +7 -0
  25. package/dist/node/examples/productionComposition.d.ts +118 -0
  26. package/dist/node/examples/productionComposition.d.ts.map +1 -0
  27. package/dist/node/index.d.ts +25 -3
  28. package/dist/node/index.d.ts.map +1 -1
  29. package/dist/node/index.mjs +1730 -68
  30. package/dist/node/index.mjs.map +4 -4
  31. package/dist/node/locator/createDappLocator.d.ts +32 -5
  32. package/dist/node/locator/createDappLocator.d.ts.map +1 -1
  33. package/dist/node/providers/DappBucketStore.d.ts +26 -21
  34. package/dist/node/providers/DappBucketStore.d.ts.map +1 -1
  35. package/dist/node/providers/capabilities.d.ts +49 -0
  36. package/dist/node/providers/capabilities.d.ts.map +1 -0
  37. package/dist/node/providers/errors.d.ts +11 -0
  38. package/dist/node/providers/errors.d.ts.map +1 -0
  39. package/dist/node/providers/hashBytes.d.ts +3 -0
  40. package/dist/node/providers/hashBytes.d.ts.map +1 -0
  41. package/dist/node/providers/joinKey.d.ts +11 -0
  42. package/dist/node/providers/joinKey.d.ts.map +1 -0
  43. package/dist/node/providers/memoryBucketStore.d.ts +14 -0
  44. package/dist/node/providers/memoryBucketStore.d.ts.map +1 -0
  45. package/dist/node/providers/types.d.ts +99 -0
  46. package/dist/node/providers/types.d.ts.map +1 -0
  47. package/dist/node/publication/canonicalHead.d.ts +7 -0
  48. package/dist/node/publication/canonicalHead.d.ts.map +1 -0
  49. package/dist/node/publication/commitHead.d.ts +9 -0
  50. package/dist/node/publication/commitHead.d.ts.map +1 -0
  51. package/dist/node/publication/constants.d.ts +27 -0
  52. package/dist/node/publication/constants.d.ts.map +1 -0
  53. package/dist/node/publication/gc.d.ts +42 -0
  54. package/dist/node/publication/gc.d.ts.map +1 -0
  55. package/dist/node/publication/incremental.d.ts +83 -0
  56. package/dist/node/publication/incremental.d.ts.map +1 -0
  57. package/dist/node/publication/publishGeneration.d.ts +18 -0
  58. package/dist/node/publication/publishGeneration.d.ts.map +1 -0
  59. package/dist/node/publication/putCreateOnly.d.ts +11 -0
  60. package/dist/node/publication/putCreateOnly.d.ts.map +1 -0
  61. package/dist/node/publication/releases.d.ts +49 -0
  62. package/dist/node/publication/releases.d.ts.map +1 -0
  63. package/dist/node/publication/safety.d.ts +55 -0
  64. package/dist/node/publication/safety.d.ts.map +1 -0
  65. package/dist/node/publication/shard.d.ts +7 -0
  66. package/dist/node/publication/shard.d.ts.map +1 -0
  67. package/dist/node/publication/status.d.ts +27 -0
  68. package/dist/node/publication/status.d.ts.map +1 -0
  69. package/dist/node/publication/statusPatch.d.ts +10 -0
  70. package/dist/node/publication/statusPatch.d.ts.map +1 -0
  71. package/dist/node/publication/types.d.ts +138 -0
  72. package/dist/node/publication/types.d.ts.map +1 -0
  73. package/dist/node/publication/writeGenerationObjects.d.ts +5 -0
  74. package/dist/node/publication/writeGenerationObjects.d.ts.map +1 -0
  75. package/dist/node/reducer/DappReducer.d.ts +37 -14
  76. package/dist/node/reducer/DappReducer.d.ts.map +1 -1
  77. package/examples/countFactsReducer.mjs +96 -0
  78. package/package.json +16 -8
@@ -1,17 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "node:fs";
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: new Promise((res, rej) => {
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 interval only fires while the actor is
70
- * `started`, and a run is skipped if the previous one is still in flight, so
71
- * a slow pass can never stack on top of itself. Must be called from
72
- * `startHandler` (guards on `starting` status), mirroring the xl1 actor.
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" || !this._intervals.has(timerName)) return;
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. Not driven automatically here (no orchestrator);
108
- * a caller invokes it after `start()` if it wants `whenReady()` to resolve.
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
- await Promise.allSettled(this._inFlight.values());
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
- this._data = await this.locator.getInstance(DappDataStoreMoniker);
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("DappReduce", async () => {
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
- await this.params.reducer.reduce({
186
- data: this._data,
187
- index: this._index,
188
- logger: this.logger,
189
- state: this._state
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. Returns them so the caller can stop them on shutdown.
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
- for (const spec of specs) {
203
- const params = {
204
- locator,
205
- name: spec.name,
206
- reducer: spec.reducer,
207
- ...spec.reduceIntervalMs !== void 0 && { reduceIntervalMs: spec.reduceIntervalMs }
208
- };
209
- const actor = await DappActor.create(params);
210
- await actor.start();
211
- actors.push(actor);
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
- return actors;
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
- return name === "NoSuchKey" || name === "NotFound";
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} pointed at the local dapp server. Uses
224
- * path-style addressing so `/{bucket}/{key}` resolves as a subfolder of the
225
- * one origin. Under `--ssl auto` the server speaks HTTPS with a locally
226
- * trusted certificate; the daemon exports its CA via `NODE_EXTRA_CA_CERTS`, so
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 { bucket } = options;
231
- const client = new S3Client({
232
- endpoint: options.endpoint,
233
- region: "us-east-1",
234
- forcePathStyle: true,
235
- credentials: {
236
- accessKeyId: options.accessKeyId ?? DEFAULT_CREDENTIAL,
237
- secretAccessKey: options.secretAccessKey ?? DEFAULT_CREDENTIAL
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 await (await client.send(new GetObjectCommand({
644
+ return metaFromResponse(await client.send(new HeadObjectCommand({
245
645
  Bucket: bucket,
246
- Key: key
247
- }))).Body?.transformToByteArray();
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
- await client.send(new PutObjectCommand({
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 response = await client.send(new ListObjectsV2Command({
265
- Bucket: bucket,
266
- Prefix: prefix,
267
- ContinuationToken: token
268
- }));
269
- for (const item of response.Contents ?? []) if (item.Key !== void 0) keys.push(item.Key);
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 bucket keyed by
281
- * moniker; extra providers can be `register`ed before actors are created.
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 bucketOptions = {
286
- endpoint: options.endpoint,
287
- ...options.accessKeyId !== void 0 && { accessKeyId: options.accessKeyId },
288
- ...options.secretAccessKey !== void 0 && { secretAccessKey: options.secretAccessKey }
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
- const instances = /* @__PURE__ */ new Map([
291
- [DappDataStoreMoniker, createS3BucketStore({
292
- ...bucketOptions,
293
- bucket: "data"
294
- })],
295
- [DappStateStoreMoniker, createS3BucketStore({
296
- ...bucketOptions,
297
- bucket: "state"
298
- })],
299
- [DappIndexStoreMoniker, createS3BucketStore({
300
- ...bucketOptions,
301
- bucket: "index"
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 to derive `state` and
327
- * `index` from `data`.
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 S3-compatible storage server (from
337
- * `@ariestools/aries-dapp-serve`) and, in the same process, a shared
338
- * process-wide locator plus the DappActor(s) that derive `state`/`index` from
339
- * `data` on an interval. Spawned by `aries dapp up`.
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 backing = resolveBacking(BACKING);
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 actors = await bootDappActors(createDappLocator({
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: noopReducer,
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
- 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)`);
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
  };