@nwire/forge 0.10.1 → 0.11.0
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/dist/framework-events.d.ts +44 -2
- package/dist/framework-events.js +10 -1
- package/dist/helpers/cli-runner.d.ts +1 -1
- package/dist/helpers/cli-runner.js +5 -4
- package/dist/index.d.ts +24 -12
- package/dist/index.js +24 -12
- package/dist/plugins/actions-chain.d.ts +48 -0
- package/dist/plugins/actions-chain.js +251 -0
- package/dist/plugins/actions-plugin.d.ts +40 -0
- package/dist/plugins/actions-plugin.js +76 -0
- package/dist/plugins/actors-chain.d.ts +52 -0
- package/dist/plugins/actors-chain.js +204 -0
- package/dist/plugins/actors-plugin.d.ts +36 -0
- package/dist/plugins/actors-plugin.js +62 -0
- package/dist/plugins/dlq-plugin.d.ts +29 -0
- package/dist/plugins/dlq-plugin.js +37 -0
- package/dist/plugins/idempotency-plugin.d.ts +28 -0
- package/dist/plugins/idempotency-plugin.js +57 -0
- package/dist/plugins/projections-chain.d.ts +34 -0
- package/dist/plugins/projections-chain.js +86 -0
- package/dist/plugins/projections-plugin.d.ts +36 -0
- package/dist/plugins/projections-plugin.js +63 -0
- package/dist/plugins/queries-chain.d.ts +33 -0
- package/dist/plugins/queries-chain.js +77 -0
- package/dist/plugins/queries-plugin.d.ts +41 -0
- package/dist/plugins/queries-plugin.js +74 -0
- package/dist/plugins/workflows-chain.d.ts +51 -0
- package/dist/plugins/workflows-chain.js +203 -0
- package/dist/plugins/workflows-plugin.d.ts +47 -0
- package/dist/plugins/workflows-plugin.js +81 -0
- package/dist/runtime/create-forge-app.d.ts +11 -11
- package/dist/runtime/create-forge-app.js +28 -32
- package/dist/runtime/forge-dispatcher.d.ts +24 -0
- package/dist/runtime/forge-dispatcher.js +76 -22
- package/dist/runtime/forge-plugin.d.ts +57 -42
- package/dist/runtime/forge-plugin.js +72 -59
- package/dist/runtime/with-forge.d.ts +26 -0
- package/dist/runtime/with-forge.js +30 -0
- package/dist/stores/idempotency-store.d.ts +15 -0
- package/dist/stores/idempotency-store.js +7 -0
- package/package.json +11 -11
|
@@ -1,55 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Forge plugin — installs the dispatcher, materialises forge's framework
|
|
3
|
+
* hooks, pins the action handler step on the dispatch chain, and registers
|
|
4
|
+
* any domain primitives passed in options.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
6
|
+
* import { createApp } from "@nwire/app";
|
|
7
|
+
* import { createForgePlugin, FORGE_DISPATCHER_BINDING } from "@nwire/forge";
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
9
|
+
* const app = createApp({
|
|
10
|
+
* appName: "orders",
|
|
11
|
+
* plugins: [createForgePlugin({
|
|
12
|
+
* actors: [Order],
|
|
13
|
+
* projections: [OrdersDashboard],
|
|
14
|
+
* handlers: [placeOrder, sendReceipt],
|
|
15
|
+
* })],
|
|
16
|
+
* });
|
|
8
17
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
18
|
+
* await app.start();
|
|
19
|
+
* const dispatcher = app.container.resolve(FORGE_DISPATCHER_BINDING);
|
|
11
20
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* there to call `dispatch / publish / fireDueTimers / …`.
|
|
17
|
-
* 3. Materialise the Action* / Event* framework-hook slots forge's
|
|
18
|
-
* module augmentation declares — kernel pre-instantiates only
|
|
19
|
-
* App* / Plugin* / Wire* slots; forge's domain slots come in via
|
|
20
|
-
* the plugin so a non-forge app's runtime stays uncluttered.
|
|
21
|
-
* 4. Pin the action handler chain step on `runtime.dispatchHook$` at
|
|
22
|
-
* priority `-Infinity` so user-registered `runtime.use()` middleware
|
|
23
|
-
* stays strictly OUTSIDE the handler invocation.
|
|
21
|
+
* Handlers and direct registrations land on the dispatcher during the
|
|
22
|
+
* `AppBooting` chain — after every plugin's setup ran (so the dispatcher
|
|
23
|
+
* is bound) but before plugin boot queues fire (so downstream plugins'
|
|
24
|
+
* boot work can reach domain routes already wired).
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* their own `bind({ dispose })` registrations.
|
|
26
|
+
* `forgePlugin` is the default-options shortcut for tests, demos, and
|
|
27
|
+
* single-process apps. For custom stores or a cross-service bus, use
|
|
28
|
+
* `createForgePlugin(opts)`.
|
|
29
29
|
*/
|
|
30
30
|
import { ForgeDispatcher } from "./forge-dispatcher.js";
|
|
31
|
-
/**
|
|
31
|
+
/** Container binding the forge dispatcher is registered under. */
|
|
32
32
|
export const FORGE_DISPATCHER_BINDING = "forge.dispatcher";
|
|
33
|
-
/** Capability bindings
|
|
33
|
+
/** Capability bindings consumed by handler / resolver ctx via `ctx.resolve(...)`. */
|
|
34
34
|
export const FORGE_EXECUTE_BINDING = "execute";
|
|
35
35
|
export const FORGE_SEND_BINDING = "send";
|
|
36
36
|
export const FORGE_USE_PROJECTION_BINDING = "useProjection";
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Construct a forge plugin with custom stores, bus, and/or domain
|
|
39
|
+
* primitives. The plugin's setup is synchronous; domain registration
|
|
40
|
+
* happens during the App's `AppBooting` chain.
|
|
41
41
|
*/
|
|
42
|
-
export const forgePlugin = createForgePlugin();
|
|
43
42
|
export function createForgePlugin(options = {}) {
|
|
44
43
|
return {
|
|
45
44
|
name: "forge",
|
|
46
45
|
setup({ runtime, bind, dispose, hooks }) {
|
|
47
46
|
const dispatcher = new ForgeDispatcher(runtime, options);
|
|
48
|
-
// 1. Bind the dispatcher on the container so the app handle's
|
|
49
|
-
// `dispatch / publish / request / …` delegators can resolve it.
|
|
50
47
|
bind(FORGE_DISPATCHER_BINDING, dispatcher);
|
|
51
|
-
//
|
|
52
|
-
//
|
|
48
|
+
// Capability factories for handler / HTTP resolver ctx
|
|
49
|
+
// (`ctx.resolve("execute" | "send" | "useProjection")`).
|
|
53
50
|
const execute = (action, input, envelope) => dispatcher.dispatch(action, input, envelope);
|
|
54
51
|
const send = (action, input, envelope) => {
|
|
55
52
|
void dispatcher.dispatch(action, input, envelope);
|
|
@@ -60,62 +57,78 @@ export function createForgePlugin(options = {}) {
|
|
|
60
57
|
bind(FORGE_EXECUTE_BINDING, () => execute);
|
|
61
58
|
bind(FORGE_SEND_BINDING, () => send);
|
|
62
59
|
bind(FORGE_USE_PROJECTION_BINDING, () => useProjection);
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
// Materialise forge's Action* / Event* hook slots and the
|
|
61
|
+
// EventPublishing chain. Apps without forge don't carry these.
|
|
62
|
+
for (const slot of [
|
|
63
|
+
"ActionDispatching",
|
|
64
|
+
"ActionCompleted",
|
|
65
|
+
"ActionFailed",
|
|
66
|
+
"EventRecording",
|
|
67
|
+
"EventRecorded",
|
|
68
|
+
"EventPublishing",
|
|
69
|
+
]) {
|
|
68
70
|
runtime.defineHook(slot);
|
|
69
71
|
}
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
72
|
+
// Attach forge's atomic publish chain to the EventPublishing hook.
|
|
73
|
+
// Each step does its work for one event and calls next() to descend
|
|
74
|
+
// to the next priority slot. The idempotency step short-circuits
|
|
75
|
+
// (skips next) when a duplicate is seen.
|
|
76
|
+
dispatcher.attachPublishChain();
|
|
77
|
+
// Pin the dispatch chain handler step at priority -Infinity so any
|
|
78
|
+
// user `runtime.use()` middleware stays strictly outside the
|
|
79
|
+
// handler invocation.
|
|
74
80
|
runtime.dispatchHook$.use(async (hctx, next) => {
|
|
75
81
|
hctx.result = await hctx.coreFn();
|
|
76
82
|
await next();
|
|
77
83
|
}, { name: "handler", priority: -Infinity });
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// === "handler"` (forge HandlerDefinition shape) — anything else
|
|
83
|
-
// stays on the runtime side.
|
|
84
|
+
// Domain registration runs at AppBooting — after every plugin's
|
|
85
|
+
// setup ran (so the dispatcher is bound) but before plugin boot
|
|
86
|
+
// queues fire (so downstream plugins' boot work can reach domain
|
|
87
|
+
// routes already wired).
|
|
84
88
|
hooks.AppBooting.use(async (_, next) => {
|
|
85
|
-
// Forward runtime-registered
|
|
89
|
+
// Forward any runtime-registered handlers to the dispatcher.
|
|
86
90
|
for (const name of runtime.listHandlers()) {
|
|
87
91
|
const def = runtime.getHandler(name);
|
|
88
92
|
if (def?.$kind === "handler") {
|
|
89
93
|
dispatcher.registerActionHandler(def);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
|
-
// Register
|
|
96
|
+
// Register option-declared primitives.
|
|
97
|
+
for (const handler of options.handlers ?? []) {
|
|
98
|
+
dispatcher.registerActionHandler(handler);
|
|
99
|
+
}
|
|
93
100
|
for (const actor of options.actors ?? []) {
|
|
94
101
|
dispatcher.registerActor(actor);
|
|
95
102
|
}
|
|
96
|
-
for (const workflow of options.workflows ?? []) {
|
|
97
|
-
dispatcher.registerWorkflow(workflow);
|
|
98
|
-
}
|
|
99
103
|
for (const projection of options.projections ?? []) {
|
|
100
104
|
dispatcher.registerProjection(projection);
|
|
101
105
|
}
|
|
102
106
|
for (const query of options.queries ?? []) {
|
|
103
107
|
dispatcher.registerQuery(query);
|
|
104
108
|
}
|
|
109
|
+
for (const workflow of options.workflows ?? []) {
|
|
110
|
+
dispatcher.registerWorkflow(workflow);
|
|
111
|
+
}
|
|
105
112
|
for (const call of options.externalCalls ?? []) {
|
|
106
113
|
dispatcher.registerExternalCall(call);
|
|
107
114
|
}
|
|
108
115
|
await next();
|
|
109
|
-
}, { name: "forge.
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// adapters that DO own threads pass themselves in via opts and
|
|
113
|
-
// handle their own dispose via `bind(name, store, { dispose })`.
|
|
116
|
+
}, { name: "forge.register-domain", priority: 1_000_000 });
|
|
117
|
+
// Cleanup hook — durable adopters with their own dispose schedules
|
|
118
|
+
// register via `bind(name, store, { dispose })`.
|
|
114
119
|
dispose(async () => {
|
|
115
|
-
// No-op for in-memory; reserved for future timer-scheduler
|
|
116
|
-
// ownership semantics.
|
|
117
120
|
void dispatcher;
|
|
118
121
|
});
|
|
119
122
|
},
|
|
120
123
|
};
|
|
121
124
|
}
|
|
125
|
+
/** Default forge plugin — in-memory stores, no bus, no domain options. */
|
|
126
|
+
export const forgePlugin = createForgePlugin();
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the forge dispatcher from an app's container. Convenience for
|
|
129
|
+
* test code and tooling that needs direct dispatcher access; handler code
|
|
130
|
+
* uses `ctx.execute` / `ctx.send` / `ctx.publish` instead.
|
|
131
|
+
*/
|
|
132
|
+
export function forgeDispatcher(app) {
|
|
133
|
+
return app.container.resolve(FORGE_DISPATCHER_BINDING);
|
|
134
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `withForge(opts)` — App-transformer that installs forge.
|
|
3
|
+
*
|
|
4
|
+
* A plain function `App → App` that calls `app.with(createForgePlugin(opts))`
|
|
5
|
+
* under the hood. Use it as the canonical install path for forge:
|
|
6
|
+
*
|
|
7
|
+
* const app = withForge({
|
|
8
|
+
* actors: [Order],
|
|
9
|
+
* projections: [OrdersDashboard],
|
|
10
|
+
* actions: [placeOrder, sendReceipt],
|
|
11
|
+
* })(createApp({ appName: "orders" }));
|
|
12
|
+
*
|
|
13
|
+
* Equivalent to writing the chain by hand:
|
|
14
|
+
*
|
|
15
|
+
* const app = createApp({ appName: "orders" })
|
|
16
|
+
* .with(createForgePlugin({ actors: [Order], ... }));
|
|
17
|
+
*
|
|
18
|
+
* Use whichever reads better at the call site.
|
|
19
|
+
*/
|
|
20
|
+
import type { App } from "@nwire/app";
|
|
21
|
+
import { type ForgePluginOptions } from "./forge-plugin.js";
|
|
22
|
+
/**
|
|
23
|
+
* Returns an App transformer that installs forge into any App it's
|
|
24
|
+
* applied to. The transformer preserves the existing `<TCaps>`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withForge(opts?: ForgePluginOptions): <TCaps>(app: App<TCaps>) => App<TCaps>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `withForge(opts)` — App-transformer that installs forge.
|
|
3
|
+
*
|
|
4
|
+
* A plain function `App → App` that calls `app.with(createForgePlugin(opts))`
|
|
5
|
+
* under the hood. Use it as the canonical install path for forge:
|
|
6
|
+
*
|
|
7
|
+
* const app = withForge({
|
|
8
|
+
* actors: [Order],
|
|
9
|
+
* projections: [OrdersDashboard],
|
|
10
|
+
* actions: [placeOrder, sendReceipt],
|
|
11
|
+
* })(createApp({ appName: "orders" }));
|
|
12
|
+
*
|
|
13
|
+
* Equivalent to writing the chain by hand:
|
|
14
|
+
*
|
|
15
|
+
* const app = createApp({ appName: "orders" })
|
|
16
|
+
* .with(createForgePlugin({ actors: [Order], ... }));
|
|
17
|
+
*
|
|
18
|
+
* Use whichever reads better at the call site.
|
|
19
|
+
*/
|
|
20
|
+
import { createForgePlugin } from "./forge-plugin.js";
|
|
21
|
+
/**
|
|
22
|
+
* Returns an App transformer that installs forge into any App it's
|
|
23
|
+
* applied to. The transformer preserves the existing `<TCaps>`.
|
|
24
|
+
*/
|
|
25
|
+
export function withForge(opts = {}) {
|
|
26
|
+
return (app) => {
|
|
27
|
+
app.with(createForgePlugin(opts));
|
|
28
|
+
return app;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -22,11 +22,26 @@ export interface IdempotencyStore {
|
|
|
22
22
|
record(messageId: string, opts?: {
|
|
23
23
|
ttlMs?: number;
|
|
24
24
|
}): void | Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Atomic check-and-record. Returns true on the first call for a given
|
|
27
|
+
* key and false on subsequent calls. Used by the EventPublishing
|
|
28
|
+
* idempotency step to dedup correctly under concurrent publishes.
|
|
29
|
+
*
|
|
30
|
+
* Adapters backed by Redis / Postgres / etc. should implement this as
|
|
31
|
+
* a single round-trip (SETNX, INSERT … ON CONFLICT, etc.). The
|
|
32
|
+
* in-memory default does it synchronously on the underlying Set so
|
|
33
|
+
* concurrent JS callers can't race.
|
|
34
|
+
*/
|
|
35
|
+
recordIfNew(messageId: string, opts?: {
|
|
36
|
+
ttlMs?: number;
|
|
37
|
+
}): boolean | Promise<boolean>;
|
|
25
38
|
}
|
|
26
39
|
export declare class InMemoryIdempotencyStore implements IdempotencyStore {
|
|
27
40
|
private readonly entries;
|
|
28
41
|
seen(messageId: string): boolean;
|
|
29
42
|
record(messageId: string): void;
|
|
43
|
+
/** Atomic on a single-threaded event loop: check + add in one synchronous step. */
|
|
44
|
+
recordIfNew(messageId: string): boolean;
|
|
30
45
|
/** Test seam — count of recorded ids. */
|
|
31
46
|
size(): number;
|
|
32
47
|
/** Test seam — drop all entries. */
|
|
@@ -20,6 +20,13 @@ export class InMemoryIdempotencyStore {
|
|
|
20
20
|
record(messageId) {
|
|
21
21
|
this.entries.add(messageId);
|
|
22
22
|
}
|
|
23
|
+
/** Atomic on a single-threaded event loop: check + add in one synchronous step. */
|
|
24
|
+
recordIfNew(messageId) {
|
|
25
|
+
if (this.entries.has(messageId))
|
|
26
|
+
return false;
|
|
27
|
+
this.entries.add(messageId);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
23
30
|
/** Test seam — count of recorded ids. */
|
|
24
31
|
size() {
|
|
25
32
|
return this.entries.size;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Nwire — the framework's core primitives. defineAction, defineEvent, defineHandler, defineActor, defineProjection, defineQuery, defineWorkflow, defineModule, defineApp, definePlugin, createApp. MessageEnvelope with correlation/causation. The runtime is the bus.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"actions",
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"emittery": "1.0.1",
|
|
35
35
|
"koa": "^2.16.4",
|
|
36
36
|
"zod": "^4.0.0",
|
|
37
|
-
"@nwire/app": "0.
|
|
38
|
-
"@nwire/
|
|
39
|
-
"@nwire/dead-letter": "0.
|
|
40
|
-
"@nwire/
|
|
41
|
-
"@nwire/
|
|
42
|
-
"@nwire/
|
|
43
|
-
"@nwire/
|
|
44
|
-
"@nwire/logger": "0.
|
|
45
|
-
"@nwire/
|
|
46
|
-
"@nwire/
|
|
37
|
+
"@nwire/app": "0.11.0",
|
|
38
|
+
"@nwire/bus": "0.11.0",
|
|
39
|
+
"@nwire/dead-letter": "0.11.0",
|
|
40
|
+
"@nwire/handler": "0.11.0",
|
|
41
|
+
"@nwire/container": "0.11.0",
|
|
42
|
+
"@nwire/messages": "0.11.0",
|
|
43
|
+
"@nwire/wires": "0.11.0",
|
|
44
|
+
"@nwire/logger": "0.11.0",
|
|
45
|
+
"@nwire/envelope": "0.11.0",
|
|
46
|
+
"@nwire/hooks": "0.11.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/koa": "^2.15.0",
|