@sincpro/client-framework 0.6.0 → 1.1.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/AGENTS.md +95 -25
- package/MANIFESTO.md +103 -40
- package/README.md +608 -333
- package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
- package/dist/circuit_breaker/domain/circuit.js +37 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
- package/dist/circuit_breaker/index.d.ts +9 -0
- package/dist/circuit_breaker/index.js +9 -0
- package/dist/client/adapters/json-rpc.d.ts +40 -0
- package/dist/client/adapters/json-rpc.js +97 -0
- package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
- package/dist/client/{memory.js → adapters/memory.js} +2 -2
- package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
- package/dist/client/adapters/rest-operations.d.ts +19 -0
- package/dist/client/adapters/rest-operations.js +66 -0
- package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
- package/dist/client/{rest.js → adapters/rest.js} +2 -2
- package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
- package/dist/client/domain/operation.d.ts +73 -0
- package/dist/client/domain/operation.js +19 -0
- package/dist/client/index.d.ts +20 -0
- package/dist/client/index.js +20 -0
- package/dist/client/services/guarded.d.ts +17 -0
- package/dist/client/services/guarded.js +19 -0
- package/dist/client/services/propagation.d.ts +45 -0
- package/dist/{services/execution.js → client/services/propagation.js} +49 -36
- package/dist/client/{retry.js → services/retry.js} +1 -1
- package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
- package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
- package/dist/codegen/{names.js → domain/names.js} +3 -3
- package/dist/codegen/{cli.js → entrypoint/cli.js} +15 -14
- package/dist/codegen/index.d.ts +7 -7
- package/dist/codegen/index.js +5 -10
- package/dist/codegen/services/client.d.ts +15 -0
- package/dist/codegen/services/client.js +111 -0
- package/dist/codegen/services/domain.d.ts +20 -0
- package/dist/codegen/{domain.js → services/domain.js} +82 -25
- package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
- package/dist/codegen/{entities.js → services/entities.js} +18 -4
- package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
- package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +8 -5
- package/dist/codegen/{scaffold.js → services/scaffold.js} +39 -43
- package/dist/common/failures.d.ts +83 -0
- package/dist/common/failures.js +212 -0
- package/dist/common/index.d.ts +11 -0
- package/dist/common/index.js +11 -0
- package/dist/context/adapters/memory.d.ts +6 -0
- package/dist/context/adapters/memory.js +34 -0
- package/dist/context/adapters/plain-codec.d.ts +3 -0
- package/dist/context/adapters/plain-codec.js +16 -0
- package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
- package/dist/context/{codec.js → domain/codec.js} +0 -14
- package/dist/context/domain/execution.d.ts +45 -0
- package/dist/context/domain/execution.js +11 -0
- package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
- package/dist/context/{store.js → domain/store.js} +1 -34
- package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
- package/dist/context/index.d.ts +17 -0
- package/dist/context/index.js +15 -0
- package/dist/context/infrastructure/process.d.ts +13 -0
- package/dist/context/infrastructure/process.js +35 -0
- package/dist/core/domain/bus.d.ts +27 -0
- package/dist/core/domain/bus.js +26 -0
- package/dist/core/domain/dto.d.ts +62 -0
- package/dist/core/domain/dto.js +43 -0
- package/dist/core/domain/errors.d.ts +18 -0
- package/dist/core/domain/errors.js +1 -0
- package/dist/core/domain/pipeline.d.ts +89 -0
- package/dist/core/domain/pipeline.js +1 -0
- package/dist/core/domain/services.d.ts +47 -0
- package/dist/core/domain/services.js +31 -0
- package/dist/core/entrypoint/bounded-context.d.ts +139 -0
- package/dist/core/entrypoint/bounded-context.js +426 -0
- package/dist/core/entrypoint/screens.d.ts +6 -0
- package/dist/core/entrypoint/screens.js +136 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +17 -0
- package/dist/core/infrastructure/listeners.d.ts +28 -0
- package/dist/core/infrastructure/listeners.js +34 -0
- package/dist/core/infrastructure/process.d.ts +56 -0
- package/dist/core/infrastructure/process.js +35 -0
- package/dist/core/services/binding.d.ts +6 -0
- package/dist/core/services/binding.js +16 -0
- package/dist/core/services/error-handlers.d.ts +11 -0
- package/dist/core/services/error-handlers.js +21 -0
- package/dist/core/services/execution.d.ts +66 -0
- package/dist/core/services/execution.js +216 -0
- package/dist/core/services/interceptors.d.ts +15 -0
- package/dist/core/services/interceptors.js +25 -0
- package/dist/core/services/listening.d.ts +17 -0
- package/dist/core/services/listening.js +25 -0
- package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
- package/dist/event_driven/adapters/memory-event-store.js +23 -0
- package/dist/event_driven/adapters/stored-queue.d.ts +67 -0
- package/dist/event_driven/adapters/stored-queue.js +192 -0
- package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
- package/dist/event_driven/adapters/sync-queue.js +19 -0
- package/dist/event_driven/domain/envelope.d.ts +28 -0
- package/dist/event_driven/domain/envelope.js +19 -0
- package/dist/event_driven/domain/queue.d.ts +40 -0
- package/dist/event_driven/domain/queue.js +1 -0
- package/dist/event_driven/domain/store.d.ts +56 -0
- package/dist/event_driven/domain/store.js +1 -0
- package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
- package/dist/event_driven/entrypoint/publisher.js +35 -0
- package/dist/event_driven/entrypoint/subscriber.d.ts +34 -0
- package/dist/event_driven/entrypoint/subscriber.js +51 -0
- package/dist/event_driven/index.d.ts +19 -0
- package/dist/event_driven/index.js +17 -0
- package/dist/index.d.ts +21 -38
- package/dist/index.js +21 -38
- package/dist/model/index.d.ts +10 -0
- package/dist/model/index.js +10 -0
- package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
- package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
- package/dist/model/{schema.js → services/schema.js} +1 -1
- package/dist/observability/adapters/console.d.ts +12 -0
- package/dist/observability/adapters/console.js +35 -0
- package/dist/observability/adapters/memory.d.ts +41 -0
- package/dist/observability/adapters/memory.js +49 -0
- package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
- package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
- package/dist/observability/domain/errors.d.ts +23 -0
- package/dist/observability/domain/errors.js +8 -0
- package/dist/observability/domain/logging.d.ts +44 -0
- package/dist/observability/domain/logging.js +1 -0
- package/dist/observability/domain/tracing.d.ts +49 -0
- package/dist/observability/domain/tracing.js +20 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +4 -0
- package/dist/observability/infrastructure/registry.d.ts +21 -0
- package/dist/observability/infrastructure/registry.js +18 -0
- package/dist/observability/services/logging.d.ts +27 -0
- package/dist/observability/services/logging.js +106 -0
- package/dist/observability/services/outcome.d.ts +12 -0
- package/dist/observability/services/outcome.js +17 -0
- package/dist/{services/report.js → observability/services/reporting.js} +4 -3
- package/dist/testing/index.d.ts +3 -33
- package/dist/testing/index.js +5 -37
- package/package.json +16 -8
- package/dist/codegen/client.d.ts +0 -14
- package/dist/codegen/client.js +0 -201
- package/dist/codegen/domain.d.ts +0 -15
- package/dist/observability/ports.d.ts +0 -65
- package/dist/observability/ports.js +0 -21
- package/dist/services/bounded-context.d.ts +0 -79
- package/dist/services/bounded-context.js +0 -261
- package/dist/services/execution.d.ts +0 -63
- package/dist/services/feature.d.ts +0 -85
- package/dist/services/feature.js +0 -86
- package/dist/services/process.d.ts +0 -60
- package/dist/services/process.js +0 -57
- package/dist/services/screens.d.ts +0 -2
- package/dist/services/screens.js +0 -76
- /package/dist/{adapters → client/adapters}/parquet.js +0 -0
- /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
- /package/dist/client/{contract.js → domain/contract.js} +0 -0
- /package/dist/client/{entity.js → domain/entity.js} +0 -0
- /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
- /package/dist/client/{transport.js → domain/transport.js} +0 -0
- /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
- /package/dist/client/{discovery.js → services/discovery.js} +0 -0
- /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
- /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
- /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
- /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
- /package/dist/codegen/{check.js → services/check.js} +0 -0
- /package/dist/codegen/{rest.js → services/rest.js} +0 -0
- /package/dist/context/{context.js → entrypoint/context.js} +0 -0
- /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
- /package/dist/model/{meta.js → domain/meta.js} +0 -0
- /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
- /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
- /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
- /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ContextStore } from "../../context/domain/store.js";
|
|
2
|
+
import { type ConsoleLoggerOptions } from "../../observability/adapters/console.js";
|
|
3
|
+
import type { ErrorReporter } from "../../observability/domain/errors.js";
|
|
4
|
+
import type { Logger } from "../../observability/domain/logging.js";
|
|
5
|
+
import { type Outcome, type Tracer } from "../../observability/domain/tracing.js";
|
|
6
|
+
/**
|
|
7
|
+
* Where logs go: the console by default, any `Logger` otherwise, or nowhere.
|
|
8
|
+
*
|
|
9
|
+
* { mode: "development", level: "info", layers: { adapter: false } } // the console, filtered
|
|
10
|
+
* { mode: "production" } // the console, silent
|
|
11
|
+
* { engine: combineLoggers(consoleLogger({ mode }), lokiLogger) } // the platform's sinks
|
|
12
|
+
* { engine: false } // no logs
|
|
13
|
+
*
|
|
14
|
+
* Context: one shape for both doors — `configureProcess({ logger })`, which replaces everything,
|
|
15
|
+
* and `configureLogger(...)`, which replaces only logging (a debug switch on a phone). `mode`,
|
|
16
|
+
* `level` and `layers` configure the console engine; a supplied `engine` owns its own policy.
|
|
17
|
+
*/
|
|
18
|
+
export type LoggerConfiguration = Partial<ConsoleLoggerOptions> & {
|
|
19
|
+
engine?: Logger | false;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* What every bounded context of this process runs with — set once, in `main`, the way
|
|
23
|
+
* sincpro_framework's process is: who traces, who reports, where logs go, where the context lives.
|
|
24
|
+
*
|
|
25
|
+
* // entrypoints/main.tsx — the platform's pieces, built from the application's settings
|
|
26
|
+
* configureProcess({
|
|
27
|
+
* tracer: browserTracer({ endpoint: settings.otlp, service: "forge-web" }),
|
|
28
|
+
* reporter: glitchtipReporter({ dsn: settings.glitchtip }),
|
|
29
|
+
* logger: { mode: settings.production ? "production" : "development" },
|
|
30
|
+
* context: memoryContexts(),
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* Context: everything is optional, and nothing breaks without it. A process that never configures
|
|
34
|
+
* runs untraced and unreported, with memory context and development console logging — so a bounded
|
|
35
|
+
* context is usable the moment it is imported, and `main` adds what it watches with. The bounded
|
|
36
|
+
* contexts read the process on every call, never at import: `project` may be defined before `main`
|
|
37
|
+
* runs.
|
|
38
|
+
*
|
|
39
|
+
* It replaces everything at once: what is not given goes back to its default — a test calls it
|
|
40
|
+
* with its own tracer and starts from a clean context.
|
|
41
|
+
*/
|
|
42
|
+
export type ProcessOptions = {
|
|
43
|
+
/** The platform's (OpenTelemetry in a browser, a queue on a phone, memory in a test). */
|
|
44
|
+
tracer?: Tracer;
|
|
45
|
+
/** The platform's (GlitchTip, Sentry, memory in a test). Only what nobody expected reaches it. */
|
|
46
|
+
reporter?: ErrorReporter;
|
|
47
|
+
/** Where logs go (`LoggerConfiguration`); the console in development mode by default. */
|
|
48
|
+
logger?: LoggerConfiguration;
|
|
49
|
+
/** Where the context lives (`memoryContexts()` by default). */
|
|
50
|
+
context?: ContextStore;
|
|
51
|
+
/** How an error ended a use case (`outcomeOf` by default). Only `failed` is reported. */
|
|
52
|
+
outcomeOf?: (error: unknown) => Outcome;
|
|
53
|
+
};
|
|
54
|
+
export declare function configureProcess(options?: ProcessOptions): void;
|
|
55
|
+
/** Replace logging only; tracing, reporting and the context stay as they are. */
|
|
56
|
+
export declare function configureLogger(options?: LoggerConfiguration): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { memoryContexts } from "../../context/adapters/memory.js";
|
|
2
|
+
import { replaceProcessStore } from "../../context/infrastructure/process.js";
|
|
3
|
+
import { consoleLogger, } from "../../observability/adapters/console.js";
|
|
4
|
+
import { NO_TRACER, } from "../../observability/domain/tracing.js";
|
|
5
|
+
import { replaceObservability } from "../../observability/infrastructure/registry.js";
|
|
6
|
+
import { outcomeOf } from "../../observability/services/outcome.js";
|
|
7
|
+
function loggingEngine(options) {
|
|
8
|
+
if (options.engine === false)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (options.engine !== undefined) {
|
|
11
|
+
if (!options.engine || typeof options.engine.log !== "function") {
|
|
12
|
+
throw new TypeError("Logger engine must implement log(record)");
|
|
13
|
+
}
|
|
14
|
+
return options.engine;
|
|
15
|
+
}
|
|
16
|
+
return consoleLogger({
|
|
17
|
+
mode: options.mode ?? "development",
|
|
18
|
+
...(options.level ? { level: options.level } : {}),
|
|
19
|
+
...(options.layers ? { layers: options.layers } : {}),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function configureProcess(options = {}) {
|
|
23
|
+
const logger = loggingEngine(options.logger ?? {});
|
|
24
|
+
replaceObservability({
|
|
25
|
+
tracer: options.tracer ?? NO_TRACER,
|
|
26
|
+
reporter: options.reporter,
|
|
27
|
+
logger,
|
|
28
|
+
outcomeOf: options.outcomeOf ?? outcomeOf,
|
|
29
|
+
});
|
|
30
|
+
replaceProcessStore(options.context ?? memoryContexts());
|
|
31
|
+
}
|
|
32
|
+
/** Replace logging only; tracing, reporting and the context stay as they are. */
|
|
33
|
+
export function configureLogger(options = {}) {
|
|
34
|
+
replaceObservability({ logger: loggingEngine(options) });
|
|
35
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Execution } from "../../context/domain/execution.js";
|
|
2
|
+
/**
|
|
3
|
+
* A dependency as the execution should see it: bound when it can be (`BIND`, a `Transport`, an
|
|
4
|
+
* `OperationTransport`), as it is otherwise.
|
|
5
|
+
*/
|
|
6
|
+
export declare function bindDependency<T>(value: T, execution: Execution): T;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isOperationTransport } from "../../client/domain/operation.js";
|
|
2
|
+
import { bindOperations, bindTransport, isTransport, } from "../../client/services/propagation.js";
|
|
3
|
+
import { BIND, isBindable, } from "../../context/domain/execution.js";
|
|
4
|
+
/**
|
|
5
|
+
* A dependency as the execution should see it: bound when it can be (`BIND`, a `Transport`, an
|
|
6
|
+
* `OperationTransport`), as it is otherwise.
|
|
7
|
+
*/
|
|
8
|
+
export function bindDependency(value, execution) {
|
|
9
|
+
if (isBindable(value))
|
|
10
|
+
return value[BIND](execution);
|
|
11
|
+
if (isTransport(value))
|
|
12
|
+
return bindTransport(value, execution);
|
|
13
|
+
if (isOperationTransport(value))
|
|
14
|
+
return bindOperations(value, execution);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ErrorHandler } from "../domain/errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* The answer of a chain of error handlers to `error`, or what its last handler threw.
|
|
4
|
+
*
|
|
5
|
+
* await handled([toEmpty, logAndRethrow], new IssueNotFound()) -> toEmpty's answer
|
|
6
|
+
*
|
|
7
|
+
* 1. Each handler, first-registered-first, receives what the one before it threw.
|
|
8
|
+
* Final: the first answer; when every handler threw, the last thrown — the original error when
|
|
9
|
+
* there is no handler at all.
|
|
10
|
+
*/
|
|
11
|
+
export declare function handled(handlers: readonly ErrorHandler[], error: unknown): Promise<unknown>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The answer of a chain of error handlers to `error`, or what its last handler threw.
|
|
3
|
+
*
|
|
4
|
+
* await handled([toEmpty, logAndRethrow], new IssueNotFound()) -> toEmpty's answer
|
|
5
|
+
*
|
|
6
|
+
* 1. Each handler, first-registered-first, receives what the one before it threw.
|
|
7
|
+
* Final: the first answer; when every handler threw, the last thrown — the original error when
|
|
8
|
+
* there is no handler at all.
|
|
9
|
+
*/
|
|
10
|
+
export async function handled(handlers, error) {
|
|
11
|
+
let current = error;
|
|
12
|
+
for (const handler of handlers) {
|
|
13
|
+
try {
|
|
14
|
+
return await handler(current);
|
|
15
|
+
}
|
|
16
|
+
catch (thrown) {
|
|
17
|
+
current = thrown;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
throw current;
|
|
21
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Execution } from "../../context/domain/execution.js";
|
|
2
|
+
import type { ContextValues } from "../../context/domain/store.js";
|
|
3
|
+
import type { Executable } from "../domain/dto.js";
|
|
4
|
+
import type { ErrorHandler } from "../domain/errors.js";
|
|
5
|
+
import type { Cause, ExecutionCompleted, ExecutionStarted, Interceptor, PipelineLayer } from "../domain/pipeline.js";
|
|
6
|
+
import { type Attributes, type Outcome } from "../../observability/domain/tracing.js";
|
|
7
|
+
export interface RuntimeService {
|
|
8
|
+
execute(command: object): unknown;
|
|
9
|
+
}
|
|
10
|
+
type Layer = PipelineLayer;
|
|
11
|
+
/** Where a failure first happened: the run, its executable, its layer and how it ended. */
|
|
12
|
+
export type FailedAt = {
|
|
13
|
+
readonly execution: Execution;
|
|
14
|
+
readonly executable: Executable;
|
|
15
|
+
readonly layer: Layer;
|
|
16
|
+
readonly outcome: Outcome;
|
|
17
|
+
};
|
|
18
|
+
/** Where `error` first failed, when it is an object a run threw; `undefined` otherwise. */
|
|
19
|
+
export declare function failedAt(error: unknown): FailedAt | undefined;
|
|
20
|
+
/** Who hears the moments of a run: read when they happen, so a listener added later hears. */
|
|
21
|
+
export interface Announcer {
|
|
22
|
+
started(heard: ExecutionStarted): void;
|
|
23
|
+
completed(heard: ExecutionCompleted): void;
|
|
24
|
+
}
|
|
25
|
+
export interface ExecutionOptions {
|
|
26
|
+
readonly boundedContext: string;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly layer: Layer;
|
|
29
|
+
/** What else its span says: the event it executes, the service it replaced. */
|
|
30
|
+
readonly attributes?: Attributes;
|
|
31
|
+
readonly dependencies: ReadonlyMap<string, unknown>;
|
|
32
|
+
readonly context: () => ContextValues;
|
|
33
|
+
readonly parent?: Execution;
|
|
34
|
+
/** What caused a root run from outside: a delivered event's envelope continues its chain. */
|
|
35
|
+
readonly cause?: Cause;
|
|
36
|
+
/** What cancels a root run; a child inherits its parent's. */
|
|
37
|
+
readonly signal?: AbortSignal;
|
|
38
|
+
readonly featureBus: (execution: Execution) => object;
|
|
39
|
+
/** What runs around the service, outermost first. */
|
|
40
|
+
readonly interceptors: readonly Interceptor[];
|
|
41
|
+
/** Who hears it start and answer. */
|
|
42
|
+
readonly announce: Announcer;
|
|
43
|
+
/** The bounded context's expected errors: they end `refused`, never reported. */
|
|
44
|
+
readonly expected: (error: unknown) => boolean;
|
|
45
|
+
/** The bounded context's handlers for this layer: what answers a failure instead of raising it. */
|
|
46
|
+
readonly handlers: readonly ErrorHandler[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One pipeline for every registered Feature and ApplicationService.
|
|
50
|
+
*
|
|
51
|
+
* 1. A span `<bounded context>/<serviceName>`, a child of the parent run when there is one, and
|
|
52
|
+
* the execution every dependency, `this.logger` and child Feature are bound to.
|
|
53
|
+
* Its chain: the parent's correlation (or the cause's, or its own id for a root), caused by
|
|
54
|
+
* the parent run or the delivered event.
|
|
55
|
+
* 2. Its start is announced; the interceptors run around the service, which sees the
|
|
56
|
+
* execution through its own view of the instance.
|
|
57
|
+
* 3. A failure is observed where it happened: its outcome (expected by the bounded context →
|
|
58
|
+
* `refused`, else the process's `outcomeOf`), the span closed with it, its log line,
|
|
59
|
+
* where it first failed and — when `failed`, and only once per error along the run — its
|
|
60
|
+
* report.
|
|
61
|
+
* 4. Then the layer's error handlers answer it, in order; with none, or when the last throws,
|
|
62
|
+
* the error reaches the caller.
|
|
63
|
+
* Final: the service's answer — announced —, or its handlers'.
|
|
64
|
+
*/
|
|
65
|
+
export declare function executeService(target: RuntimeService, command: Executable, options: ExecutionOptions): Promise<unknown>;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { newEntityId } from "../../common/ids.js";
|
|
2
|
+
import { bindDependency } from "./binding.js";
|
|
3
|
+
import { handled } from "./error-handlers.js";
|
|
4
|
+
import { runAround } from "./interceptors.js";
|
|
5
|
+
import { NO_TRACER, } from "../../observability/domain/tracing.js";
|
|
6
|
+
import { observabilityNow } from "../../observability/infrastructure/registry.js";
|
|
7
|
+
import { executionLogger, logExecution, logHandled, } from "../../observability/services/logging.js";
|
|
8
|
+
import { firstReport } from "../../observability/services/reporting.js";
|
|
9
|
+
const FAILED_AT = new WeakMap();
|
|
10
|
+
/** Where `error` first failed, when it is an object a run threw; `undefined` otherwise. */
|
|
11
|
+
export function failedAt(error) {
|
|
12
|
+
return error !== null && typeof error === "object" ? FAILED_AT.get(error) : undefined;
|
|
13
|
+
}
|
|
14
|
+
const REPORTED_BY_EXECUTION = new WeakMap();
|
|
15
|
+
/** What a cancelled run fails with: an `AbortError`, whatever the signal was aborted with. */
|
|
16
|
+
function abortedBy(signal) {
|
|
17
|
+
const reason = signal.reason;
|
|
18
|
+
if (reason instanceof Error && reason.name === "AbortError")
|
|
19
|
+
return reason;
|
|
20
|
+
return Object.assign(new Error("The run was cancelled"), {
|
|
21
|
+
name: "AbortError",
|
|
22
|
+
cause: reason,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What `running` answers — or the cancellation, the moment `signal` aborts: the caller stops
|
|
27
|
+
* waiting even when the service does not look at its signal.
|
|
28
|
+
*/
|
|
29
|
+
function untilAborted(running, signal) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const abort = () => reject(abortedBy(signal));
|
|
32
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
33
|
+
running.then((answer) => {
|
|
34
|
+
signal.removeEventListener("abort", abort);
|
|
35
|
+
resolve(answer);
|
|
36
|
+
}, (error) => {
|
|
37
|
+
signal.removeEventListener("abort", abort);
|
|
38
|
+
reject(error);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Observability adapters are best-effort; their failures must not replace a business result. */
|
|
43
|
+
function observe(work, fallback) {
|
|
44
|
+
try {
|
|
45
|
+
return work();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function serviceView(target, options, execution) {
|
|
52
|
+
const dependencies = new Map();
|
|
53
|
+
const logger = executionLogger(options.layer, execution);
|
|
54
|
+
let features;
|
|
55
|
+
return new Proxy(target, {
|
|
56
|
+
get(held, property, receiver) {
|
|
57
|
+
if (property === "context")
|
|
58
|
+
return execution.context;
|
|
59
|
+
if (property === "span")
|
|
60
|
+
return execution.span;
|
|
61
|
+
if (property === "logger")
|
|
62
|
+
return logger;
|
|
63
|
+
if (property === "signal")
|
|
64
|
+
return execution.signal;
|
|
65
|
+
if (property === "execution")
|
|
66
|
+
return execution;
|
|
67
|
+
if (property === "featureBus" && options.layer === "application_service") {
|
|
68
|
+
return (features ??= options.featureBus(execution));
|
|
69
|
+
}
|
|
70
|
+
if (typeof property === "string" && options.dependencies.has(property)) {
|
|
71
|
+
if (!dependencies.has(property)) {
|
|
72
|
+
dependencies.set(property, bindDependency(options.dependencies.get(property), execution));
|
|
73
|
+
}
|
|
74
|
+
return dependencies.get(property);
|
|
75
|
+
}
|
|
76
|
+
if (Reflect.has(held, property))
|
|
77
|
+
return Reflect.get(held, property, receiver);
|
|
78
|
+
if (typeof property === "string") {
|
|
79
|
+
throw new Error(`${options.boundedContext}: dependency or member is not registered: ${property}`);
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
},
|
|
83
|
+
set(held, property, value) {
|
|
84
|
+
if (property === "context" ||
|
|
85
|
+
property === "span" ||
|
|
86
|
+
property === "logger" ||
|
|
87
|
+
property === "signal" ||
|
|
88
|
+
property === "execution" ||
|
|
89
|
+
property === "featureBus" ||
|
|
90
|
+
(typeof property === "string" && options.dependencies.has(property))) {
|
|
91
|
+
throw new Error(`Execution dependencies and context are read-only: ${String(property)}`);
|
|
92
|
+
}
|
|
93
|
+
return Reflect.set(held, property, value);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* One pipeline for every registered Feature and ApplicationService.
|
|
99
|
+
*
|
|
100
|
+
* 1. A span `<bounded context>/<serviceName>`, a child of the parent run when there is one, and
|
|
101
|
+
* the execution every dependency, `this.logger` and child Feature are bound to.
|
|
102
|
+
* Its chain: the parent's correlation (or the cause's, or its own id for a root), caused by
|
|
103
|
+
* the parent run or the delivered event.
|
|
104
|
+
* 2. Its start is announced; the interceptors run around the service, which sees the
|
|
105
|
+
* execution through its own view of the instance.
|
|
106
|
+
* 3. A failure is observed where it happened: its outcome (expected by the bounded context →
|
|
107
|
+
* `refused`, else the process's `outcomeOf`), the span closed with it, its log line,
|
|
108
|
+
* where it first failed and — when `failed`, and only once per error along the run — its
|
|
109
|
+
* report.
|
|
110
|
+
* 4. Then the layer's error handlers answer it, in order; with none, or when the last throws,
|
|
111
|
+
* the error reaches the caller.
|
|
112
|
+
* Final: the service's answer — announced —, or its handlers'.
|
|
113
|
+
*/
|
|
114
|
+
export async function executeService(target, command, options) {
|
|
115
|
+
const ports = observabilityNow();
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
const useCase = `${options.boundedContext}.${options.name}`;
|
|
118
|
+
const context = options.parent?.context ?? options.context();
|
|
119
|
+
const id = newEntityId();
|
|
120
|
+
const correlationId = options.parent?.correlationId ?? options.cause?.correlationId ?? id;
|
|
121
|
+
const causationId = options.parent?.id ?? options.cause?.id;
|
|
122
|
+
const attributes = {
|
|
123
|
+
"sincpro.use_case": useCase,
|
|
124
|
+
"sincpro.layer": options.layer,
|
|
125
|
+
"sincpro.context": options.boundedContext,
|
|
126
|
+
"sincpro.execution_id": id,
|
|
127
|
+
"sincpro.correlation_id": correlationId,
|
|
128
|
+
...(causationId ? { "sincpro.causation_id": causationId } : {}),
|
|
129
|
+
...options.attributes,
|
|
130
|
+
};
|
|
131
|
+
const span = observe(() => ports.tracer.start(`${options.boundedContext}/${options.name}`, {
|
|
132
|
+
...(options.parent ? { parent: options.parent.span } : {}),
|
|
133
|
+
attributes,
|
|
134
|
+
}), NO_TRACER.start(useCase, {}));
|
|
135
|
+
const execution = {
|
|
136
|
+
id,
|
|
137
|
+
useCase,
|
|
138
|
+
boundedContext: options.boundedContext,
|
|
139
|
+
parent: options.parent,
|
|
140
|
+
correlationId,
|
|
141
|
+
causationId,
|
|
142
|
+
signal: options.signal ?? options.parent?.signal,
|
|
143
|
+
span,
|
|
144
|
+
context,
|
|
145
|
+
};
|
|
146
|
+
const reported = options.parent
|
|
147
|
+
? (REPORTED_BY_EXECUTION.get(options.parent) ?? new Set())
|
|
148
|
+
: new Set();
|
|
149
|
+
REPORTED_BY_EXECUTION.set(execution, reported);
|
|
150
|
+
logExecution(options.layer, execution);
|
|
151
|
+
observe(() => options.announce.started({ executable: command, execution, layer: options.layer }), undefined);
|
|
152
|
+
const finish = (error) => {
|
|
153
|
+
const outcome = error
|
|
154
|
+
? observe(() => (options.expected(error.value) ? "refused" : ports.outcomeOf(error.value)), "failed")
|
|
155
|
+
: "ok";
|
|
156
|
+
observe(() => span.end(outcome, error?.value), undefined);
|
|
157
|
+
logExecution(options.layer, execution, {
|
|
158
|
+
outcome,
|
|
159
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
160
|
+
});
|
|
161
|
+
if (outcome === "failed" && error && ports.reporter && !reported.has(error.value)) {
|
|
162
|
+
reported.add(error.value);
|
|
163
|
+
// Mark for reportError outside the call; a different root may report the same Error.
|
|
164
|
+
firstReport(error.value);
|
|
165
|
+
observe(() => ports.reporter?.report(error.value, {
|
|
166
|
+
useCase,
|
|
167
|
+
traceId: span.traceId,
|
|
168
|
+
executionId: id,
|
|
169
|
+
context,
|
|
170
|
+
}), undefined);
|
|
171
|
+
}
|
|
172
|
+
if (error?.value !== null &&
|
|
173
|
+
typeof error?.value === "object" &&
|
|
174
|
+
!FAILED_AT.has(error.value))
|
|
175
|
+
FAILED_AT.set(error.value, {
|
|
176
|
+
execution,
|
|
177
|
+
executable: command,
|
|
178
|
+
layer: options.layer,
|
|
179
|
+
outcome,
|
|
180
|
+
});
|
|
181
|
+
return outcome;
|
|
182
|
+
};
|
|
183
|
+
const view = serviceView(target, options, execution);
|
|
184
|
+
const service = (current) => Promise.resolve(Reflect.apply(target.execute, view, [current]));
|
|
185
|
+
let answer;
|
|
186
|
+
try {
|
|
187
|
+
const signal = execution.signal;
|
|
188
|
+
if (signal?.aborted)
|
|
189
|
+
throw abortedBy(signal);
|
|
190
|
+
const running = runAround(options.interceptors, service, command, {
|
|
191
|
+
boundedContext: options.boundedContext,
|
|
192
|
+
useCase,
|
|
193
|
+
layer: options.layer,
|
|
194
|
+
execution,
|
|
195
|
+
logger: executionLogger(options.layer, execution),
|
|
196
|
+
});
|
|
197
|
+
answer = signal === undefined ? await running : await untilAborted(running, signal);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
finish({ value: error });
|
|
201
|
+
if (options.handlers.length === 0)
|
|
202
|
+
throw error;
|
|
203
|
+
const recovered = await handled(options.handlers, error);
|
|
204
|
+
logHandled(options.layer, { execution });
|
|
205
|
+
return recovered;
|
|
206
|
+
}
|
|
207
|
+
finish();
|
|
208
|
+
observe(() => options.announce.completed({
|
|
209
|
+
executable: command,
|
|
210
|
+
execution,
|
|
211
|
+
layer: options.layer,
|
|
212
|
+
answer,
|
|
213
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
214
|
+
}), undefined);
|
|
215
|
+
return answer;
|
|
216
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Executable } from "../domain/dto.js";
|
|
2
|
+
import type { InterceptedRun, Interceptor } from "../domain/pipeline.js";
|
|
3
|
+
/**
|
|
4
|
+
* `handler` run on `executable`, wrapped by `interceptors` — the first the outermost.
|
|
5
|
+
*
|
|
6
|
+
* runAround([], service, command, run) -> the service's answer
|
|
7
|
+
* runAround([audit, credit], service, command, run) -> audit(credit(service))
|
|
8
|
+
* runAround([<passes another class on>], …) -> TypeError, naming both classes
|
|
9
|
+
*
|
|
10
|
+
* 1. Each interceptor gets a `next` bound to the position after it.
|
|
11
|
+
* 2. What reaches `next` must be of the class the call started with: the bus routes by class,
|
|
12
|
+
* so another class would be another use case under this one's name.
|
|
13
|
+
* Final: the outermost interceptor's answer — the service's, when none answers by itself.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runAround(interceptors: readonly Interceptor[], handler: (executable: Executable) => Promise<unknown>, executable: Executable, run: InterceptedRun): Promise<unknown>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `handler` run on `executable`, wrapped by `interceptors` — the first the outermost.
|
|
3
|
+
*
|
|
4
|
+
* runAround([], service, command, run) -> the service's answer
|
|
5
|
+
* runAround([audit, credit], service, command, run) -> audit(credit(service))
|
|
6
|
+
* runAround([<passes another class on>], …) -> TypeError, naming both classes
|
|
7
|
+
*
|
|
8
|
+
* 1. Each interceptor gets a `next` bound to the position after it.
|
|
9
|
+
* 2. What reaches `next` must be of the class the call started with: the bus routes by class,
|
|
10
|
+
* so another class would be another use case under this one's name.
|
|
11
|
+
* Final: the outermost interceptor's answer — the service's, when none answers by itself.
|
|
12
|
+
*/
|
|
13
|
+
export function runAround(interceptors, handler, executable, run) {
|
|
14
|
+
const started = Object.getPrototypeOf(executable)?.constructor;
|
|
15
|
+
const at = async (position, current) => {
|
|
16
|
+
if (Object.getPrototypeOf(current)?.constructor !== started) {
|
|
17
|
+
throw new TypeError(`An interceptor of ${run.useCase} passed on a ${current?.constructor?.name} where it received a ${started?.name}`);
|
|
18
|
+
}
|
|
19
|
+
const interceptor = interceptors[position];
|
|
20
|
+
if (interceptor === undefined)
|
|
21
|
+
return handler(current);
|
|
22
|
+
return interceptor(current, (next) => at(position + 1, next), run);
|
|
23
|
+
};
|
|
24
|
+
return at(0, executable);
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Executable } from "../domain/dto.js";
|
|
2
|
+
import type { Listener } from "../domain/pipeline.js";
|
|
3
|
+
/** A class a service, an interceptor or a listener is for. */
|
|
4
|
+
export type ExecutableClass = abstract new (...args: never[]) => Executable;
|
|
5
|
+
/** An interceptor or a listener, and the classes it is for — every class when it names none. */
|
|
6
|
+
export type Hearing<F> = {
|
|
7
|
+
readonly what: F;
|
|
8
|
+
readonly dtos: readonly ExecutableClass[];
|
|
9
|
+
};
|
|
10
|
+
/** Whether something registered for `dtos` concerns `executable`: by its exact class. */
|
|
11
|
+
export declare function concerns(dtos: readonly ExecutableClass[], executable: Executable): boolean;
|
|
12
|
+
/** Each listener that hears it, in turn; one that fails is ignored. */
|
|
13
|
+
export declare function announceTo<E extends {
|
|
14
|
+
executable: Executable;
|
|
15
|
+
}>(hearing: ReadonlySet<Hearing<Listener<E>>>, heard: E): void;
|
|
16
|
+
/** `listener` added to `hearing` for `dtos`; answers how to stop. */
|
|
17
|
+
export declare function listenIn<E>(hearing: Set<Hearing<Listener<E>>>, listener: Listener<E>, dtos: readonly ExecutableClass[]): () => void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Whether something registered for `dtos` concerns `executable`: by its exact class. */
|
|
2
|
+
export function concerns(dtos, executable) {
|
|
3
|
+
return dtos.length === 0 || dtos.includes(Object.getPrototypeOf(executable)?.constructor);
|
|
4
|
+
}
|
|
5
|
+
/** Each listener that hears it, in turn; one that fails is ignored. */
|
|
6
|
+
export function announceTo(hearing, heard) {
|
|
7
|
+
for (const one of [...hearing]) {
|
|
8
|
+
if (!concerns(one.dtos, heard.executable))
|
|
9
|
+
continue;
|
|
10
|
+
try {
|
|
11
|
+
one.what(heard);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Hearing never changes the answer, as tracing, reporting and logging never do.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** `listener` added to `hearing` for `dtos`; answers how to stop. */
|
|
19
|
+
export function listenIn(hearing, listener, dtos) {
|
|
20
|
+
if (typeof listener !== "function")
|
|
21
|
+
throw new TypeError("A listener is a function");
|
|
22
|
+
const one = { what: listener, dtos };
|
|
23
|
+
hearing.add(one);
|
|
24
|
+
return () => void hearing.delete(one);
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EventStore, StoredEvent } from "../domain/store.js";
|
|
2
|
+
/** A memory store, and what it holds — in the order added — for whoever inspects it. */
|
|
3
|
+
export type MemoryEventStore = EventStore & {
|
|
4
|
+
readonly events: readonly StoredEvent[];
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The events of a stored queue in this process's memory: what a browser tab or a test keeps.
|
|
8
|
+
* Gone with the process — a phone's store is a table.
|
|
9
|
+
*/
|
|
10
|
+
export declare function memoryEventStore(): MemoryEventStore;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The events of a stored queue in this process's memory: what a browser tab or a test keeps.
|
|
3
|
+
* Gone with the process — a phone's store is a table.
|
|
4
|
+
*/
|
|
5
|
+
export function memoryEventStore() {
|
|
6
|
+
const held = [];
|
|
7
|
+
return {
|
|
8
|
+
get events() {
|
|
9
|
+
return [...held];
|
|
10
|
+
},
|
|
11
|
+
add(stored) {
|
|
12
|
+
held.push(stored);
|
|
13
|
+
},
|
|
14
|
+
next(skip) {
|
|
15
|
+
return held.find((one) => one.state === "pending" && !skip.has(one.envelope.id));
|
|
16
|
+
},
|
|
17
|
+
save(stored) {
|
|
18
|
+
const at = held.findIndex((one) => one.envelope.id === stored.envelope.id);
|
|
19
|
+
if (at >= 0)
|
|
20
|
+
held[at] = stored;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type JsonValue } from "../../common/serialization.js";
|
|
2
|
+
import type { DomainEvent, EventClass } from "../../core/domain/dto.js";
|
|
3
|
+
import type { EventQueue, Subscribes } from "../domain/queue.js";
|
|
4
|
+
import type { EventStore, StoredEvent } from "../domain/store.js";
|
|
5
|
+
export type StoredQueueOptions = {
|
|
6
|
+
/** Where the events wait: memory, a phone's SQLite, a browser's IndexedDB. */
|
|
7
|
+
readonly store: EventStore;
|
|
8
|
+
/** Who executes them: every bounded context that hears each one. */
|
|
9
|
+
readonly subscriber: Subscribes;
|
|
10
|
+
/**
|
|
11
|
+
* How a stored payload becomes its class again. By default, `static fromJSON` when the class
|
|
12
|
+
* has one, else its values set on an instance of it — as JSON gave them: an event that carries
|
|
13
|
+
* a `Money`, a `Decimal` or a date declares `fromJSON` to read them back as such.
|
|
14
|
+
*/
|
|
15
|
+
readonly rebuild?: (type: EventClass<DomainEvent>, payload: JsonValue) => DomainEvent;
|
|
16
|
+
/**
|
|
17
|
+
* Whether an event may be delivered now — a phone asks for a connection, or a session. One
|
|
18
|
+
* that is not waits while the rest go on; `drain()` again when what it waits for comes back.
|
|
19
|
+
*/
|
|
20
|
+
readonly ready?: (stored: StoredEvent) => boolean | Promise<boolean>;
|
|
21
|
+
/** The first wait after a failed attempt, in ms (1 s); doubled each time, never past `longestWait`. */
|
|
22
|
+
readonly wait?: number;
|
|
23
|
+
/** The longest wait between attempts, in ms (one minute). */
|
|
24
|
+
readonly longestWait?: number;
|
|
25
|
+
/**
|
|
26
|
+
* How many attempts an event gets before it is set aside although a retry might still fix it
|
|
27
|
+
* (10): a bug of this process (`internal`) or a dependency gone for good must not hold the line
|
|
28
|
+
* forever. `Infinity` keeps trying.
|
|
29
|
+
*/
|
|
30
|
+
readonly maxAttempts?: number;
|
|
31
|
+
/** Told each time an event settles — delivered, set aside, or waiting to retry —: a screen's activity. */
|
|
32
|
+
readonly onSettled?: (stored: StoredEvent) => void;
|
|
33
|
+
};
|
|
34
|
+
/** A stored queue, and the door to deliver what waits — on a timer, back online, in the foreground. */
|
|
35
|
+
export type StoredQueue = EventQueue & {
|
|
36
|
+
drain(): Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The queue that stores an event before delivering it, and delivers it until every bounded
|
|
40
|
+
* context that hears it has it — the backend's store-and-forward (`Forwarding`), over a store
|
|
41
|
+
* each platform plugs.
|
|
42
|
+
*
|
|
43
|
+
* const queue = storedQueue({ store: sqliteEventStore(db), subscriber, ready: online });
|
|
44
|
+
* shop.addDependency("publisher", new Publisher(queue));
|
|
45
|
+
* connectivity.onBack(() => queue.drain());
|
|
46
|
+
*
|
|
47
|
+
* Context: what a phone did by hand in its event bus, the same on every platform. `publish`
|
|
48
|
+
* returns once the event is stored — nobody waits for the delivery. Then, one at a time and in
|
|
49
|
+
* the order stored:
|
|
50
|
+
*
|
|
51
|
+
* 1. An event not ready (`ready`) waits while the rest go on.
|
|
52
|
+
* 2. Nobody hears its name: delivered — a fact recorded, nobody's to act on.
|
|
53
|
+
* 3. Rebuilt as its class; one that cannot be is set aside.
|
|
54
|
+
* 4. Executed in every bounded context that hears it and does not have it yet (`reached`).
|
|
55
|
+
* 5. A failure every attempt would repeat (`PERMANENT_KINDS`: a refusal, invalid, not found) sets
|
|
56
|
+
* that context aside; any other is tried again after the backoff — or what the failure asks
|
|
57
|
+
* for (`retryAfter`, an open circuit's time left) —, and what comes after waits, so the
|
|
58
|
+
* order holds; after `maxAttempts`, it is set aside too.
|
|
59
|
+
* Final: delivered when every context has it, set aside — the dead-letter queue — when one
|
|
60
|
+
* refused it for good or ran out of attempts; `onSettled` hears either.
|
|
61
|
+
*
|
|
62
|
+
* Delivery is at least once: a process that stops after a context executed an event and before
|
|
63
|
+
* the store saved it executes it there again on the next drain. A subscriber that must not act
|
|
64
|
+
* twice keys what it does by the envelope's id — `this.execution.causationId` in its run. And one
|
|
65
|
+
* process drains a store: two tabs over one IndexedDB claim it first (the Web Locks API).
|
|
66
|
+
*/
|
|
67
|
+
export declare function storedQueue(options: StoredQueueOptions): StoredQueue;
|