@ariestools/aries-dapp-core 0.1.8 → 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 CHANGED
@@ -152,12 +152,85 @@ published head).
152
152
  ## Local daemon (dev only)
153
153
 
154
154
  The composed bin (`dist/bin/dappServer.mjs`) starts the storage server and a
155
- `noopReducer` actor. `aries dapp up` spawns it.
155
+ DappActor. `aries dapp up` spawns it.
156
156
 
157
- Env: `DAPP_PORT` (8801), `HOST` (127.0.0.1), `DAPP_BACKING` (`memory`),
158
- `DAPP_PUBLIC_HOST`, `DAPP_TLS_CERT`, `DAPP_TLS_KEY`, `DAPP_REDUCE_INTERVAL_MS`
159
- (5000). Under `--ssl auto` the daemon exports the local CA via
160
- `NODE_EXTRA_CA_CERTS`.
157
+ ### CLI
158
+
159
+ ```sh
160
+ # Empty spine (noop reducer)
161
+ aries dapp up
162
+
163
+ # Real project reducer (ESM .mjs or package export — TypeScript not supported)
164
+ aries dapp up --reducer ./dist/indexer.mjs
165
+ aries dapp up --reducer ./dist/indexer.mjs --reducer-export myIndex
166
+ aries dapp up --reducer @myorg/my-dapp/indexer
167
+
168
+ # Persist buckets across restarts
169
+ aries dapp up --backing disk --data-dir ~/.aries/dapp/data
170
+ ```
171
+
172
+ Example reducer shipped with this package:
173
+
174
+ ```sh
175
+ aries dapp up --reducer ./node_modules/@ariestools/aries-dapp-core/examples/countFactsReducer.mjs
176
+ # monorepo:
177
+ aries dapp up --reducer ./packages/dapp-core/examples/countFactsReducer.mjs
178
+ ```
179
+
180
+ ### Reducer module contract
181
+
182
+ The module must export a `DappReducer`:
183
+
184
+ - **default export**, or
185
+ - named export `reducer`, or
186
+ - any name via `--reducer-export` / `DAPP_REDUCER_EXPORT`
187
+
188
+ ```js
189
+ // dist/indexer.mjs
190
+ export default {
191
+ name: 'my-index',
192
+ version: '1',
193
+ async reduce({ data, state, index, signal, logger }) {
194
+ // read data, publishGeneration / publishIncremental, return progress
195
+ },
196
+ }
197
+ ```
198
+
199
+ Local publication should use `safety: { mode: 'unfenced' }` (or honor
200
+ `DAPP_PUBLICATION_SAFETY`, which the daemon sets to `unfenced` by default).
201
+ s3rver does not support reliable head CAS.
202
+
203
+ ### Env (daemon)
204
+
205
+ | Variable | Default | Notes |
206
+ | --- | --- | --- |
207
+ | `DAPP_PORT` | `8801` | |
208
+ | `HOST` | `127.0.0.1` | |
209
+ | `DAPP_BACKING` | `memory` | `memory` \| `disk` |
210
+ | `DAPP_DATA_DIR` | `~/.aries/dapp/data` | disk root |
211
+ | `DAPP_REDUCER` | _(noop)_ | path to `.mjs`/`.js` or package export |
212
+ | `DAPP_REDUCER_EXPORT` | default / `reducer` | named export |
213
+ | `DAPP_REDUCE_INTERVAL_MS` | `5000` | |
214
+ | `DAPP_FIRST_RUN_DELAY_MS` | `0` | |
215
+ | `DAPP_PUBLICATION_SAFETY` | `unfenced` | set by daemon for project reducers |
216
+ | `DAPP_PUBLIC_HOST` / `DAPP_TLS_*` | | `--ssl auto` |
217
+
218
+ Under `--ssl auto` the daemon exports the local CA via `NODE_EXTRA_CA_CERTS`.
219
+
220
+ ## Production entrypoint (not the CLI bin)
221
+
222
+ Do **not** run `aries-dapp-server` or `aries dapp up` in production. Write a
223
+ project-owned process that:
224
+
225
+ 1. Builds an `S3Client` for R2/S3 (credentials from env/IAM — never logged)
226
+ 2. Calls `createDappLocator({ client, bindings, ownClient: true })`
227
+ 3. Implements a `DappReducer` (import statically — no dynamic CLI load required)
228
+ 4. Calls `bootDappActors` with `safety: { mode: 'conditional-head' }` inside
229
+ `publishGeneration` / `publishIncremental`
230
+ 5. Listens for SIGTERM and stops actors + `locator.destroy()`
231
+
232
+ See `examples/productionComposition.ts` and the composition section above.
233
+ There is no inbound public listener; clients read head/status/objects from CDN.
161
234
 
162
235
  ## Least-privilege IAM / R2 notes
163
236
 
@@ -1,8 +1,12 @@
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
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
11
  function createDeferred$1() {
8
12
  let resolve;
@@ -837,16 +841,62 @@ const noopReducer = {
837
841
  async reduce() {}
838
842
  };
839
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
840
887
  //#region src/bin/dappServer.ts
841
888
  /**
842
889
  * **Local development only.** Composed dapp daemon: boots the embedded
843
890
  * 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`.
891
+ * the same process, a shared process-wide locator plus DappActor(s).
892
+ * Spawned by `aries dapp up`.
846
893
  *
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.
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/`.
850
900
  */
851
901
  const PORT = Number(process.env.DAPP_PORT ?? 8801);
852
902
  const HOST = process.env.HOST ?? "127.0.0.1";
@@ -855,10 +905,22 @@ const PUBLIC_HOST = process.env.DAPP_PUBLIC_HOST;
855
905
  const TLS_CERT_PATH = process.env.DAPP_TLS_CERT;
856
906
  const TLS_KEY_PATH = process.env.DAPP_TLS_KEY;
857
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
+ }
858
919
  async function main() {
859
920
  if (!isDappBackingKind(BACKING)) throw new Error(`Unsupported DAPP_BACKING '${BACKING}' (supported: ${DAPP_BACKINGS.join(", ")})`);
860
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");
861
- const backing = resolveBacking(BACKING);
922
+ const reducer = await resolveDaemonReducer();
923
+ const backing = resolveBacking(BACKING, { ...BACKING === "disk" && { directory: DATA_DIR } });
862
924
  const server = await startDappServer({
863
925
  backing,
864
926
  host: HOST,
@@ -872,16 +934,21 @@ async function main() {
872
934
  const logger = new ConsoleLogger();
873
935
  const locator = createDappLocator({
874
936
  endpoint: server.baseUrl,
875
- logger
937
+ logger,
938
+ readOnlyData: false
876
939
  });
940
+ process.env.DAPP_PUBLICATION_SAFETY ??= "unfenced";
941
+ process.env.DAPP_ENDPOINT ??= server.baseUrl;
877
942
  const actors = await bootDappActors(locator, [{
878
943
  name: "DappActor",
879
- reducer: noopReducer,
944
+ reducer,
880
945
  reduceIntervalMs: REDUCE_INTERVAL_MS,
881
- firstRunDelayMs: 0,
882
- publishStatus: false
946
+ firstRunDelayMs: FIRST_RUN_DELAY_MS,
947
+ publishStatus: reducer.name !== "noop"
883
948
  }]);
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)`);
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);
885
952
  const shutdown = async () => {
886
953
  for (const actor of actors) await actor.stop();
887
954
  locator.destroy();
@@ -2,12 +2,15 @@
2
2
  /**
3
3
  * **Local development only.** Composed dapp daemon: boots the embedded
4
4
  * S3-compatible storage server (from `@ariestools/aries-dapp-serve`) and, in
5
- * the same process, a shared process-wide locator plus a `noopReducer`
6
- * DappActor. Spawned by `aries dapp up`.
5
+ * the same process, a shared process-wide locator plus DappActor(s).
6
+ * Spawned by `aries dapp up`.
7
7
  *
8
- * Production indexers must not use this binary. Compose `dapp-core` from a
9
- * project-owned entrypoint with real store bindings, an injected S3/R2 client,
10
- * and a real reducer — see the package README.
8
+ * Optional `DAPP_REDUCER` loads a project-owned ESM reducer (`.mjs` path or
9
+ * package export). Without it, `noopReducer` runs so the spine still works.
10
+ *
11
+ * Production indexers should **not** use this binary. Compose `dapp-core` from
12
+ * a project-owned entrypoint with real R2/S3 bindings and a real reducer —
13
+ * see the package README and `examples/`.
11
14
  */
12
15
  export {};
13
16
  //# sourceMappingURL=dappServer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dappServer.d.ts","sourceRoot":"","sources":["../../../src/bin/dappServer.ts"],"names":[],"mappings":";AAEA;;;;;;;;;GASG"}
1
+ {"version":3,"file":"dappServer.d.ts","sourceRoot":"","sources":["../../../src/bin/dappServer.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;GAYG"}
@@ -0,0 +1,17 @@
1
+ import type { DappReducer } from '../reducer/DappReducer.ts';
2
+ export interface LoadReducerOptions {
3
+ /**
4
+ * Named export to prefer when the module has no default export.
5
+ * Defaults to trying `reducer`, then `default`.
6
+ */
7
+ exportName?: string;
8
+ /** Absolute or relative path to a `.mjs` file, or a resolvable package export. */
9
+ spec: string;
10
+ }
11
+ /**
12
+ * Dynamically load a project-owned reducer for the local dapp daemon.
13
+ * Supports filesystem `.mjs`/`.js` paths and package export strings.
14
+ * TypeScript sources are not supported — build to ESM first.
15
+ */
16
+ export declare function loadReducer(options: LoadReducerOptions): Promise<DappReducer>;
17
+ //# sourceMappingURL=loadReducer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadReducer.d.ts","sourceRoot":"","sources":["../../../src/bin/loadReducer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAI5D,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAA;CACb;AA6DD;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAOnF"}
@@ -1,10 +1,14 @@
1
1
  /**
2
- * Project-owned production composition example (documentation / type-check only).
2
+ * Project-owned **production** composition example (documentation / type-check only).
3
3
  *
4
- * This is **not** a runnable daemon and is **not** imported by the published bin.
5
- * Copy the pattern into your application entrypoint. dapp-core stays
6
- * application-neutral: you supply the reducer, store bindings, credentials, and
7
- * any XL1/gateway readers. There is no inbound public listener and no
4
+ * This is **not** the local `aries dapp up` / `aries-dapp-server` path.
5
+ * For local dev with a pluggable reducer, use:
6
+ *
7
+ * aries dapp up --reducer ./dist/indexer.mjs
8
+ *
9
+ * For production: copy this pattern into your application entrypoint. dapp-core
10
+ * stays application-neutral: you supply the reducer, store bindings, credentials,
11
+ * and any XL1/gateway readers. There is no inbound public listener and no
8
12
  * application authority/signing seed stored in dapp-core.
9
13
  *
10
14
  * ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"productionComposition.d.ts","sourceRoot":"","sources":["../../../src/examples/productionComposition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+GG;AAEH,OAAO,EAAE,CAAA"}
1
+ {"version":3,"file":"productionComposition.d.ts","sourceRoot":"","sources":["../../../src/examples/productionComposition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmHG;AAEH,OAAO,EAAE,CAAA"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Example local-dev reducer for `aries dapp up --reducer`.
3
+ *
4
+ * Counts JSON objects under data/facts/, then publishes a coherent generation
5
+ * with the local unfenced safety mode (s3rver does not support head CAS).
6
+ *
7
+ * Usage (from a built monorepo):
8
+ *
9
+ * pnpm xy build @ariestools/aries-dapp-core
10
+ * aries dapp up --reducer ./packages/dapp-core/examples/countFactsReducer.mjs
11
+ *
12
+ * Seed data (path-style, any credentials):
13
+ *
14
+ * export S3_ENDPOINT=http://127.0.0.1:8801
15
+ * # put facts into the data bucket, e.g. with aws cli --endpoint-url
16
+ *
17
+ * Production: do **not** use this daemon. Compose dapp-core in your own
18
+ * process with R2 bindings and safety: { mode: 'conditional-head' }.
19
+ */
20
+
21
+ // Package-name import is intentional so this file is a drop-in template for
22
+ // app repos; monorepo eslint forbids absolute self-imports, so disable here.
23
+ // eslint-disable-next-line workspaces/no-absolute-imports -- example template
24
+ import {
25
+ publishGeneration,
26
+ readPublishedHead,
27
+ } from '@ariestools/aries-dapp-core'
28
+
29
+ /** @type {import('@ariestools/aries-dapp-core').DappReducer} */
30
+ export const reducer = {
31
+ name: 'count-facts',
32
+ version: '1',
33
+ async reduce({
34
+ data, state, index, signal, logger,
35
+ }) {
36
+ const keys = (await data.list('facts/')).sort()
37
+ const facts = []
38
+ for (const key of keys) {
39
+ const body = await data.get(key)
40
+ if (body === undefined) continue
41
+ facts.push(JSON.parse(new TextDecoder().decode(body)))
42
+ }
43
+
44
+ const prior = await readPublishedHead(state)
45
+ const safetyMode = process.env.DAPP_PUBLICATION_SAFETY === 'conditional-head'
46
+ ? 'conditional-head'
47
+ : 'unfenced'
48
+
49
+ const published = await publishGeneration({
50
+ state,
51
+ index,
52
+ safety: { mode: safetyMode },
53
+ assertSafety: safetyMode === 'conditional-head',
54
+ ...(prior?.headEtag !== undefined && { expectedHeadEtag: prior.headEtag }),
55
+ ...(prior?.head.generation !== undefined
56
+ && { previousGeneration: prior.head.generation }),
57
+ revision: (prior?.head.revision ?? 0) + 1,
58
+ reducer: { name: 'count-facts', version: '1' },
59
+ schemaVersion: '1',
60
+ source: {
61
+ cursor: String(facts.length),
62
+ completedPosition: String(facts.length),
63
+ observedHead: String(facts.length),
64
+ },
65
+ stateObjects: [{
66
+ key: 'view.json',
67
+ body: JSON.stringify({ count: facts.length, facts }),
68
+ contentType: 'application/json',
69
+ }],
70
+ indexObjects: [{
71
+ key: 'by-id.json',
72
+ body: JSON.stringify(Object.fromEntries(
73
+ facts.map(f => [String(f.id ?? ''), f]),
74
+ )),
75
+ contentType: 'application/json',
76
+ }],
77
+ signal,
78
+ })
79
+
80
+ if (!published.ok) {
81
+ logger.warn(`publish conflict: ${published.conflict.kind} ${published.conflict.message}`)
82
+ return
83
+ }
84
+
85
+ logger.info(`published generation ${published.generation} (count=${facts.length})`)
86
+ return {
87
+ cursor: String(facts.length),
88
+ lastCompletedPosition: String(facts.length),
89
+ observedSourceHead: String(facts.length),
90
+ generation: published.generation,
91
+ generationRoot: published.manifestKey,
92
+ }
93
+ },
94
+ }
95
+
96
+ export default reducer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ariestools/aries-dapp-core",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Aries dapp backend core: the DappActor (forked xl1 actor pattern), process-wide provider locator, pluggable reducer, and the composed daemon that runs the storage server plus actors",
5
5
  "keywords": [
6
6
  "ariestools",
@@ -35,17 +35,18 @@
35
35
  },
36
36
  "files": [
37
37
  "dist",
38
+ "examples",
38
39
  "README.md"
39
40
  ],
40
41
  "dependencies": {
41
- "@ariestools/sdk": "~8.1.1",
42
+ "@ariestools/sdk": "~8.1.2",
42
43
  "@aws-sdk/client-s3": "~3.1090.0",
43
44
  "tslib": "~2.8.1",
44
- "@ariestools/aries-dapp-serve": "~0.1.8"
45
+ "@ariestools/aries-dapp-serve": "~0.1.9"
45
46
  },
46
47
  "devDependencies": {
47
- "@ariestools/toolchain": "~8.7.16",
48
- "@ariestools/tsconfig": "~8.7.16",
48
+ "@ariestools/toolchain": "~8.7.20",
49
+ "@ariestools/tsconfig": "~8.7.20",
49
50
  "@opentelemetry/api": "~1.9.1",
50
51
  "@types/node": "~26.1.1",
51
52
  "rolldown": "~1.2.0",