@nifrajs/core 1.13.0 → 2.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/README.md +37 -1
- package/dist/assurance.d.ts +11 -2
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +20 -0
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +54 -2
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +240 -5
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-adapters.d.ts +241 -0
- package/dist/durable-adapters.d.ts.map +1 -0
- package/dist/durable-adapters.js +697 -0
- package/dist/durable-adapters.js.map +1 -0
- package/dist/durable-execution.d.ts +349 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +1173 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/effect-ledger.d.ts +9 -0
- package/dist/effect-ledger.d.ts.map +1 -0
- package/dist/effect-ledger.js +8 -0
- package/dist/effect-ledger.js.map +1 -0
- package/dist/effect-lifecycle.d.ts +42 -0
- package/dist/effect-lifecycle.d.ts.map +1 -0
- package/dist/effect-lifecycle.js +66 -0
- package/dist/effect-lifecycle.js.map +1 -0
- package/dist/effect-scope.d.ts +75 -0
- package/dist/effect-scope.d.ts.map +1 -0
- package/dist/effect-scope.js +177 -0
- package/dist/effect-scope.js.map +1 -0
- package/dist/idempotency-plugin.d.ts +13 -0
- package/dist/idempotency-plugin.d.ts.map +1 -0
- package/dist/idempotency-plugin.js +16 -0
- package/dist/idempotency-plugin.js.map +1 -0
- package/dist/index.d.ts +6 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -31
- package/dist/index.js.map +1 -1
- package/dist/internal/capability-runtime.d.ts +62 -1
- package/dist/internal/capability-runtime.d.ts.map +1 -1
- package/dist/internal/capability-runtime.js +11 -2
- package/dist/internal/capability-runtime.js.map +1 -1
- package/dist/internal/effect-execution.d.ts +16 -0
- package/dist/internal/effect-execution.d.ts.map +1 -0
- package/dist/internal/effect-execution.js +50 -0
- package/dist/internal/effect-execution.js.map +1 -0
- package/dist/internal/safe-equal.d.ts +20 -0
- package/dist/internal/safe-equal.d.ts.map +1 -0
- package/dist/internal/safe-equal.js +31 -0
- package/dist/internal/safe-equal.js.map +1 -0
- package/dist/ledger.d.ts +14 -6
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +31 -5
- package/dist/ledger.js.map +1 -1
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +56 -0
- package/dist/mcp.js.map +1 -0
- package/dist/node-direct.d.ts +5 -0
- package/dist/node-direct.d.ts.map +1 -0
- package/dist/node-direct.js +27 -0
- package/dist/node-direct.js.map +1 -0
- package/dist/reconciliation-worker.d.ts +108 -0
- package/dist/reconciliation-worker.d.ts.map +1 -0
- package/dist/reconciliation-worker.js +239 -0
- package/dist/reconciliation-worker.js.map +1 -0
- package/dist/reflection.d.ts +5 -0
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +2 -0
- package/dist/reflection.js.map +1 -1
- package/dist/server/client-ip.d.ts +35 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/client-ip.js +42 -0
- package/dist/server/client-ip.js.map +1 -0
- package/dist/server/context.d.ts +47 -2
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +14 -4
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +8 -2
- package/dist/server/contract.js.map +1 -1
- package/dist/server/http.d.ts +14 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +45 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/idempotency-lane.d.ts +45 -0
- package/dist/server/idempotency-lane.d.ts.map +1 -0
- package/dist/server/idempotency-lane.js +205 -0
- package/dist/server/idempotency-lane.js.map +1 -0
- package/dist/server/install.d.ts +21 -0
- package/dist/server/install.d.ts.map +1 -0
- package/dist/server/install.js +21 -0
- package/dist/server/install.js.map +1 -0
- package/dist/server/ledger-lane.d.ts +53 -0
- package/dist/server/ledger-lane.d.ts.map +1 -0
- package/dist/server/ledger-lane.js +82 -0
- package/dist/server/ledger-lane.js.map +1 -0
- package/dist/server/mcp-hook.d.ts +28 -0
- package/dist/server/mcp-hook.d.ts.map +1 -0
- package/dist/server/mcp-hook.js +2 -0
- package/dist/server/mcp-hook.js.map +1 -0
- package/dist/server/node-outcome-hook.d.ts +9 -0
- package/dist/server/node-outcome-hook.d.ts.map +1 -0
- package/dist/server/node-outcome-hook.js +2 -0
- package/dist/server/node-outcome-hook.js.map +1 -0
- package/dist/server/node-outcome.d.ts +37 -0
- package/dist/server/node-outcome.d.ts.map +1 -0
- package/dist/server/node-outcome.js +75 -0
- package/dist/server/node-outcome.js.map +1 -0
- package/dist/server/plugin.d.ts +78 -0
- package/dist/server/plugin.d.ts.map +1 -0
- package/dist/server/plugin.js +57 -0
- package/dist/server/plugin.js.map +1 -0
- package/dist/server/query.d.ts +18 -0
- package/dist/server/query.d.ts.map +1 -0
- package/dist/server/query.js +102 -0
- package/dist/server/query.js.map +1 -0
- package/dist/server/registry.d.ts +31 -6
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +51 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +184 -0
- package/dist/server/request-context.js.map +1 -0
- package/dist/server/respond.d.ts +17 -0
- package/dist/server/respond.d.ts.map +1 -0
- package/dist/server/respond.js +81 -0
- package/dist/server/respond.js.map +1 -0
- package/dist/server/runtime-core.d.ts +36 -0
- package/dist/server/runtime-core.d.ts.map +1 -0
- package/dist/server/runtime-core.js +51 -0
- package/dist/server/runtime-core.js.map +1 -0
- package/dist/server/server-types.d.ts +203 -0
- package/dist/server/server-types.d.ts.map +1 -0
- package/dist/server/server-types.js +2 -0
- package/dist/server/server-types.js.map +1 -0
- package/dist/server/server.d.ts +119 -345
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +247 -833
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse-hook.d.ts +10 -0
- package/dist/server/sse-hook.d.ts.map +1 -0
- package/dist/server/sse-hook.js +2 -0
- package/dist/server/sse-hook.js.map +1 -0
- package/dist/server/sse.d.ts +7 -23
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +18 -0
- package/dist/server/sse.js.map +1 -1
- package/dist/server/transport-lane.d.ts +28 -0
- package/dist/server/transport-lane.d.ts.map +1 -0
- package/dist/server/transport-lane.js +106 -0
- package/dist/server/transport-lane.js.map +1 -0
- package/dist/server/websocket.d.ts +23 -1
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +10 -1
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-hook.d.ts +8 -11
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server/ws-hook.js +7 -14
- package/dist/server/ws-hook.js.map +1 -1
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +16 -0
- package/dist/server.js.map +1 -0
- package/dist/transport-codec-rich.d.ts +8 -0
- package/dist/transport-codec-rich.d.ts.map +1 -0
- package/dist/transport-codec-rich.js +14 -0
- package/dist/transport-codec-rich.js.map +1 -0
- package/dist/transport-codec.d.ts +31 -0
- package/dist/transport-codec.d.ts.map +1 -0
- package/dist/transport-codec.js +190 -0
- package/dist/transport-codec.js.map +1 -0
- package/dist/transport-plugin.d.ts +3 -0
- package/dist/transport-plugin.d.ts.map +1 -0
- package/dist/transport-plugin.js +3 -0
- package/dist/transport-plugin.js.map +1 -0
- package/dist/wire.d.ts +58 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +445 -0
- package/dist/wire.js.map +1 -0
- package/dist/ws.d.ts +16 -7
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +25 -10
- package/dist/ws.js.map +1 -1
- package/package.json +122 -6
- package/src/assurance.ts +36 -1
- package/src/capabilities.ts +306 -5
- package/src/durable-adapters.ts +1039 -0
- package/src/durable-execution.ts +1676 -0
- package/src/effect-ledger.ts +9 -0
- package/src/effect-lifecycle.ts +120 -0
- package/src/effect-scope.ts +267 -0
- package/src/idempotency-plugin.ts +17 -0
- package/src/index.ts +6 -133
- package/src/internal/capability-runtime.ts +87 -1
- package/src/internal/effect-execution.ts +70 -0
- package/src/internal/safe-equal.ts +27 -0
- package/src/ledger.ts +45 -11
- package/src/mcp.ts +65 -0
- package/src/node-direct.ts +37 -0
- package/src/reconciliation-worker.ts +372 -0
- package/src/reflection.ts +7 -0
- package/src/server/client-ip.ts +60 -0
- package/src/server/context.ts +50 -2
- package/src/server/contract.ts +26 -7
- package/src/server/http.ts +58 -0
- package/src/server/idempotency-lane.ts +321 -0
- package/src/server/install.ts +28 -0
- package/src/server/ledger-lane.ts +125 -0
- package/src/server/mcp-hook.ts +41 -0
- package/src/server/node-outcome-hook.ts +9 -0
- package/src/server/node-outcome.ts +112 -0
- package/src/server/plugin.ts +92 -0
- package/src/server/query.ts +119 -0
- package/src/server/registry.ts +36 -6
- package/src/server/request-context.ts +271 -0
- package/src/server/respond.ts +94 -0
- package/src/server/runtime-core.ts +74 -0
- package/src/server/server-types.ts +204 -0
- package/src/server/server.ts +448 -1314
- package/src/server/sse-hook.ts +14 -0
- package/src/server/sse.ts +26 -0
- package/src/server/transport-lane.ts +141 -0
- package/src/server/websocket.ts +35 -1
- package/src/server/ws-hook.ts +8 -19
- package/src/server.ts +92 -0
- package/src/transport-codec-rich.ts +20 -0
- package/src/transport-codec.ts +250 -0
- package/src/transport-plugin.ts +2 -0
- package/src/wire.ts +474 -0
- package/src/ws.ts +33 -10
- package/dist/invariants.d.ts +0 -70
- package/dist/invariants.d.ts.map +0 -1
- package/dist/invariants.js +0 -487
- package/dist/invariants.js.map +0 -1
- package/src/invariants.ts +0 -604
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in server integration for the per-request effect ledger.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from `@nifrajs/core/server` so a bare HTTP app does not evaluate or bundle ledger
|
|
5
|
+
* machinery. Import ledger primitives from `@nifrajs/core/ledger`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type { EffectLedgerOptions } from "./ledger.ts"
|
|
9
|
+
export { effectLedger } from "./server/ledger-lane.ts"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { validCapabilityId } from "./internal/capability-runtime.ts"
|
|
2
|
+
import { type EffectCost, type EffectMetadata, normalizeEffectMetadata } from "./ledger.ts"
|
|
3
|
+
|
|
4
|
+
/** A bounded, payload-free stage in an effect's lifecycle. */
|
|
5
|
+
export type EffectLifecycleStage = "admission" | "execution" | "compensation" | "reconciliation"
|
|
6
|
+
|
|
7
|
+
export type EffectLifecyclePhase = "started" | "succeeded" | "failed" | "ambiguous"
|
|
8
|
+
|
|
9
|
+
/** Trace-parent tokens copied structurally from an installed tracing plugin. */
|
|
10
|
+
export interface EffectTraceParent {
|
|
11
|
+
readonly traceId: string
|
|
12
|
+
readonly spanId: string
|
|
13
|
+
readonly sampled: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Token-only lifecycle evidence. There is deliberately no payload, argument, result, error message,
|
|
18
|
+
* request, or context field, so observation adapters cannot accidentally export business data.
|
|
19
|
+
*/
|
|
20
|
+
export interface EffectLifecycleEvent extends EffectMetadata {
|
|
21
|
+
readonly effectId: string
|
|
22
|
+
readonly capability: string
|
|
23
|
+
readonly stage: EffectLifecycleStage
|
|
24
|
+
readonly phase: EffectLifecyclePhase
|
|
25
|
+
readonly at: number
|
|
26
|
+
readonly durationMs?: number
|
|
27
|
+
readonly attempt?: number
|
|
28
|
+
readonly errorCode?: string
|
|
29
|
+
readonly trace?: EffectTraceParent
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Observation is fail-open: a broken sink must never change effect behavior. */
|
|
33
|
+
export type EffectLifecycleObserver = (event: EffectLifecycleEvent) => void
|
|
34
|
+
|
|
35
|
+
const ERROR_CODE = /^[a-z][a-z0-9_.-]{0,63}$/
|
|
36
|
+
|
|
37
|
+
export function effectTraceParentOf(context: object): EffectTraceParent | undefined {
|
|
38
|
+
const trace = (
|
|
39
|
+
context as {
|
|
40
|
+
readonly trace?: {
|
|
41
|
+
readonly traceId?: unknown
|
|
42
|
+
readonly spanId?: unknown
|
|
43
|
+
readonly sampled?: unknown
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
).trace
|
|
47
|
+
if (
|
|
48
|
+
trace === undefined ||
|
|
49
|
+
typeof trace.traceId !== "string" ||
|
|
50
|
+
!/^[0-9a-f]{32}$/.test(trace.traceId) ||
|
|
51
|
+
typeof trace.spanId !== "string" ||
|
|
52
|
+
!/^[0-9a-f]{16}$/.test(trace.spanId) ||
|
|
53
|
+
typeof trace.sampled !== "boolean"
|
|
54
|
+
) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({ traceId: trace.traceId, spanId: trace.spanId, sampled: trace.sampled })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface EmitEffectLifecycleInput extends EffectMetadata {
|
|
61
|
+
readonly effectId: string
|
|
62
|
+
readonly capability: string
|
|
63
|
+
readonly stage: EffectLifecycleStage
|
|
64
|
+
readonly phase: EffectLifecyclePhase
|
|
65
|
+
readonly at?: number
|
|
66
|
+
readonly durationMs?: number
|
|
67
|
+
readonly attempt?: number
|
|
68
|
+
readonly errorCode?: string
|
|
69
|
+
readonly trace?: EffectTraceParent
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @internal Validate, freeze, and fan out one token-only event. */
|
|
73
|
+
export function emitEffectLifecycle(
|
|
74
|
+
observers: readonly EffectLifecycleObserver[],
|
|
75
|
+
input: EmitEffectLifecycleInput,
|
|
76
|
+
): void {
|
|
77
|
+
if (observers.length === 0) return
|
|
78
|
+
if (!validCapabilityId(input.capability) || !/^[!-~]{1,64}$/u.test(input.effectId)) return
|
|
79
|
+
let metadata: EffectMetadata
|
|
80
|
+
try {
|
|
81
|
+
metadata = normalizeEffectMetadata(input)
|
|
82
|
+
} catch {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
const at = input.at ?? Date.now()
|
|
86
|
+
if (!Number.isFinite(at) || at < 0) return
|
|
87
|
+
if (
|
|
88
|
+
input.durationMs !== undefined &&
|
|
89
|
+
(!Number.isFinite(input.durationMs) || input.durationMs < 0)
|
|
90
|
+
) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if (input.attempt !== undefined && (!Number.isSafeInteger(input.attempt) || input.attempt < 1)) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (input.errorCode !== undefined && !ERROR_CODE.test(input.errorCode)) return
|
|
97
|
+
const event: EffectLifecycleEvent = Object.freeze({
|
|
98
|
+
effectId: input.effectId,
|
|
99
|
+
capability: input.capability,
|
|
100
|
+
stage: input.stage,
|
|
101
|
+
phase: input.phase,
|
|
102
|
+
at,
|
|
103
|
+
...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
|
|
104
|
+
...(input.attempt === undefined ? {} : { attempt: input.attempt }),
|
|
105
|
+
...(input.errorCode === undefined ? {} : { errorCode: input.errorCode }),
|
|
106
|
+
...(metadata.target === undefined ? {} : { target: metadata.target }),
|
|
107
|
+
...(metadata.cost === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { cost: Object.freeze({ ...metadata.cost }) as EffectCost }),
|
|
110
|
+
...(metadata.digest === undefined ? {} : { digest: metadata.digest }),
|
|
111
|
+
...(input.trace === undefined ? {} : { trace: input.trace }),
|
|
112
|
+
})
|
|
113
|
+
for (const observer of observers) {
|
|
114
|
+
try {
|
|
115
|
+
observer(event)
|
|
116
|
+
} catch {
|
|
117
|
+
// Observation is deliberately fail-open.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One cross-runtime boundary for owned external effects. It centralizes correlation, lifecycle
|
|
3
|
+
* evidence, cancellation, durable transition ordering, and token-only observation without relying
|
|
4
|
+
* on Node-only async-local state.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type EffectLifecycleObserver,
|
|
9
|
+
type EffectLifecycleStage,
|
|
10
|
+
type EffectTraceParent,
|
|
11
|
+
emitEffectLifecycle,
|
|
12
|
+
} from "./effect-lifecycle.ts"
|
|
13
|
+
import { validCapabilityId } from "./internal/capability-runtime.ts"
|
|
14
|
+
import { type EffectMetadata, normalizeEffectMetadata } from "./ledger.ts"
|
|
15
|
+
|
|
16
|
+
const ERROR_CODE = /^[a-z][a-z0-9_.-]{0,63}$/
|
|
17
|
+
|
|
18
|
+
export interface EffectScopeEvidence {
|
|
19
|
+
readonly began: boolean
|
|
20
|
+
readonly committed: boolean
|
|
21
|
+
readonly ambiguous: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OwnedEffectContext extends EffectMetadata {
|
|
25
|
+
readonly effectId: string
|
|
26
|
+
readonly capability: string
|
|
27
|
+
readonly stage: Exclude<EffectLifecycleStage, "admission" | "reconciliation">
|
|
28
|
+
readonly signal: AbortSignal
|
|
29
|
+
readonly attempt?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OwnedEffectTransitions<T> {
|
|
33
|
+
intent?(context: OwnedEffectContext): void | PromiseLike<void>
|
|
34
|
+
executing?(context: OwnedEffectContext): void | PromiseLike<void>
|
|
35
|
+
committed?(context: OwnedEffectContext, result: T): void | PromiseLike<void>
|
|
36
|
+
failed?(
|
|
37
|
+
context: OwnedEffectContext,
|
|
38
|
+
input: { readonly began: boolean; readonly errorCode: string; readonly error: unknown },
|
|
39
|
+
): void | PromiseLike<void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OwnedEffectRunOptions<T> extends EffectMetadata {
|
|
43
|
+
readonly effectId?: string
|
|
44
|
+
readonly capability: string
|
|
45
|
+
readonly stage?: Exclude<EffectLifecycleStage, "admission" | "reconciliation">
|
|
46
|
+
readonly signal?: AbortSignal
|
|
47
|
+
readonly attempt?: number
|
|
48
|
+
readonly trace?: EffectTraceParent
|
|
49
|
+
/** Per-run observers, combined with the scope's observers. */
|
|
50
|
+
readonly observers?: readonly EffectLifecycleObserver[]
|
|
51
|
+
/** Optional admission boundary executed before the effect is marked as begun. */
|
|
52
|
+
readonly admit?: (context: OwnedEffectContext) => void | PromiseLike<void>
|
|
53
|
+
readonly transitions?: OwnedEffectTransitions<T>
|
|
54
|
+
readonly errorCode?: string | ((error: unknown, began: boolean) => string)
|
|
55
|
+
/** Classify a caught execution failure when the caller can prove it is terminal. */
|
|
56
|
+
readonly failurePhase?: (error: unknown, began: boolean) => "failed" | "ambiguous"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface EffectScopeOptions {
|
|
60
|
+
readonly observers?: readonly EffectLifecycleObserver[]
|
|
61
|
+
readonly signal?: AbortSignal
|
|
62
|
+
readonly effectId?: () => string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface EffectEvidenceScope {
|
|
66
|
+
evidence(): EffectScopeEvidence
|
|
67
|
+
/** Legacy/manual effects cannot prove a terminal outcome and therefore become ambiguous. */
|
|
68
|
+
markBegan(): void
|
|
69
|
+
/** Pair a prior manual `markBegan()` with a proven commit. */
|
|
70
|
+
markCommitted(): void
|
|
71
|
+
/** Preserve uncertainty when a caller loses the terminal outcome. */
|
|
72
|
+
markAmbiguous(): void
|
|
73
|
+
/** Settle one begun effect as a proven terminal failure without claiming it committed. */
|
|
74
|
+
markFailed(): void
|
|
75
|
+
/**
|
|
76
|
+
* Declare that the enclosing request may release its idempotency reservation if it returns a
|
|
77
|
+
* server error before any owned effect begins.
|
|
78
|
+
*/
|
|
79
|
+
markSafeToRetry(): void
|
|
80
|
+
/** True only while the retry declaration remains backed by evidence that no effect began. */
|
|
81
|
+
safeToRetry(): boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface EffectScope extends EffectEvidenceScope {
|
|
85
|
+
run<T>(
|
|
86
|
+
options: OwnedEffectRunOptions<T>,
|
|
87
|
+
execute: (context: OwnedEffectContext) => T | PromiseLike<T>,
|
|
88
|
+
): Promise<T>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let neverSignal: AbortSignal | undefined
|
|
92
|
+
function signalOrNever(signal?: AbortSignal): AbortSignal {
|
|
93
|
+
neverSignal ??= new AbortController().signal
|
|
94
|
+
return signal ?? neverSignal
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertEffectId(value: string): void {
|
|
98
|
+
if (!/^[!-~]{1,64}$/u.test(value))
|
|
99
|
+
throw new TypeError("effect id must be a bounded printable token")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function codeFor(
|
|
103
|
+
configured: OwnedEffectRunOptions<unknown>["errorCode"],
|
|
104
|
+
error: unknown,
|
|
105
|
+
began: boolean,
|
|
106
|
+
): string {
|
|
107
|
+
const code =
|
|
108
|
+
typeof configured === "function"
|
|
109
|
+
? configured(error, began)
|
|
110
|
+
: (configured ?? (began ? "execution_failed" : "admission_failed"))
|
|
111
|
+
if (!ERROR_CODE.test(code)) throw new TypeError("effect errorCode is invalid")
|
|
112
|
+
return code
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Lightweight aggregate evidence shared by request idempotency and full owned-effect runners. */
|
|
116
|
+
export function createEffectEvidenceScope(): EffectEvidenceScope {
|
|
117
|
+
const aggregate = { began: false, committed: false }
|
|
118
|
+
let uncertain = 0
|
|
119
|
+
let retryDeclared = false
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
evidence() {
|
|
122
|
+
return Object.freeze({ ...aggregate, ambiguous: uncertain > 0 })
|
|
123
|
+
},
|
|
124
|
+
markBegan() {
|
|
125
|
+
aggregate.began = true
|
|
126
|
+
retryDeclared = false
|
|
127
|
+
uncertain++
|
|
128
|
+
},
|
|
129
|
+
markCommitted() {
|
|
130
|
+
aggregate.began = true
|
|
131
|
+
aggregate.committed = true
|
|
132
|
+
if (uncertain > 0) uncertain--
|
|
133
|
+
},
|
|
134
|
+
markAmbiguous() {
|
|
135
|
+
aggregate.began = true
|
|
136
|
+
retryDeclared = false
|
|
137
|
+
if (uncertain === 0) uncertain = 1
|
|
138
|
+
},
|
|
139
|
+
markFailed() {
|
|
140
|
+
aggregate.began = true
|
|
141
|
+
retryDeclared = false
|
|
142
|
+
if (uncertain > 0) uncertain--
|
|
143
|
+
},
|
|
144
|
+
markSafeToRetry() {
|
|
145
|
+
if (aggregate.began) {
|
|
146
|
+
throw new Error("cannot mark an idempotent request safe to retry after an effect began")
|
|
147
|
+
}
|
|
148
|
+
retryDeclared = true
|
|
149
|
+
},
|
|
150
|
+
safeToRetry() {
|
|
151
|
+
return retryDeclared && !aggregate.began
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function createEffectScope(
|
|
157
|
+
options: EffectScopeOptions = {},
|
|
158
|
+
evidenceScope: EffectEvidenceScope = createEffectEvidenceScope(),
|
|
159
|
+
): EffectScope {
|
|
160
|
+
const observers = Object.freeze([...(options.observers ?? [])])
|
|
161
|
+
const idFor = options.effectId ?? crypto.randomUUID.bind(crypto)
|
|
162
|
+
|
|
163
|
+
const scope: EffectScope = {
|
|
164
|
+
async run<T>(
|
|
165
|
+
input: OwnedEffectRunOptions<T>,
|
|
166
|
+
execute: (context: OwnedEffectContext) => T | PromiseLike<T>,
|
|
167
|
+
) {
|
|
168
|
+
if (typeof execute !== "function")
|
|
169
|
+
throw new TypeError("owned effect executor must be a function")
|
|
170
|
+
if (!validCapabilityId(input.capability))
|
|
171
|
+
throw new TypeError("owned effect capability is invalid")
|
|
172
|
+
const effectId = input.effectId ?? idFor()
|
|
173
|
+
assertEffectId(effectId)
|
|
174
|
+
const metadata = normalizeEffectMetadata(input)
|
|
175
|
+
const signal = input.signal ?? options.signal ?? signalOrNever()
|
|
176
|
+
const stage = input.stage ?? "execution"
|
|
177
|
+
if (
|
|
178
|
+
input.attempt !== undefined &&
|
|
179
|
+
(!Number.isSafeInteger(input.attempt) || input.attempt < 1)
|
|
180
|
+
)
|
|
181
|
+
throw new RangeError("owned effect attempt must be a positive safe integer")
|
|
182
|
+
const context: OwnedEffectContext = Object.freeze({
|
|
183
|
+
effectId,
|
|
184
|
+
capability: input.capability,
|
|
185
|
+
stage,
|
|
186
|
+
signal,
|
|
187
|
+
...metadata,
|
|
188
|
+
...(input.attempt === undefined ? {} : { attempt: input.attempt }),
|
|
189
|
+
})
|
|
190
|
+
const runObservers =
|
|
191
|
+
input.observers === undefined
|
|
192
|
+
? observers
|
|
193
|
+
: Object.freeze([...observers, ...input.observers])
|
|
194
|
+
let began = false
|
|
195
|
+
const startedAt = performance.now()
|
|
196
|
+
try {
|
|
197
|
+
if (signal.aborted) throw signal.reason
|
|
198
|
+
await input.transitions?.intent?.(context)
|
|
199
|
+
if (input.admit !== undefined) {
|
|
200
|
+
emitEffectLifecycle(runObservers, {
|
|
201
|
+
...context,
|
|
202
|
+
stage: "admission",
|
|
203
|
+
phase: "started",
|
|
204
|
+
...(input.trace === undefined ? {} : { trace: input.trace }),
|
|
205
|
+
})
|
|
206
|
+
await input.admit(context)
|
|
207
|
+
emitEffectLifecycle(runObservers, {
|
|
208
|
+
...context,
|
|
209
|
+
stage: "admission",
|
|
210
|
+
phase: "succeeded",
|
|
211
|
+
durationMs: Math.max(0, performance.now() - startedAt),
|
|
212
|
+
...(input.trace === undefined ? {} : { trace: input.trace }),
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
if (signal.aborted) throw signal.reason
|
|
216
|
+
await input.transitions?.executing?.(context)
|
|
217
|
+
began = true
|
|
218
|
+
evidenceScope.markBegan()
|
|
219
|
+
const executionStartedAt = performance.now()
|
|
220
|
+
emitEffectLifecycle(runObservers, {
|
|
221
|
+
...context,
|
|
222
|
+
phase: "started",
|
|
223
|
+
...(input.trace === undefined ? {} : { trace: input.trace }),
|
|
224
|
+
})
|
|
225
|
+
const result = await execute(context)
|
|
226
|
+
await input.transitions?.committed?.(context, result)
|
|
227
|
+
evidenceScope.markCommitted()
|
|
228
|
+
emitEffectLifecycle(runObservers, {
|
|
229
|
+
...context,
|
|
230
|
+
phase: "succeeded",
|
|
231
|
+
durationMs: Math.max(0, performance.now() - executionStartedAt),
|
|
232
|
+
...(input.trace === undefined ? {} : { trace: input.trace }),
|
|
233
|
+
})
|
|
234
|
+
return result
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const errorCode = codeFor(input.errorCode, error, began)
|
|
237
|
+
const failurePhase = input.failurePhase?.(error, began) ?? (began ? "ambiguous" : "failed")
|
|
238
|
+
if (failurePhase !== "failed" && failurePhase !== "ambiguous") {
|
|
239
|
+
throw new TypeError("owned effect failurePhase returned an invalid phase")
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
await input.transitions?.failed?.(context, { began, errorCode, error })
|
|
243
|
+
} catch {
|
|
244
|
+
// Preserve the owned effect's cause. Reconciliation sees the unfinished durable state.
|
|
245
|
+
}
|
|
246
|
+
if (began && failurePhase === "failed") evidenceScope.markFailed()
|
|
247
|
+
emitEffectLifecycle(runObservers, {
|
|
248
|
+
...context,
|
|
249
|
+
stage: began ? stage : input.admit === undefined ? stage : "admission",
|
|
250
|
+
phase: began ? failurePhase : "failed",
|
|
251
|
+
durationMs: Math.max(0, performance.now() - startedAt),
|
|
252
|
+
errorCode,
|
|
253
|
+
...(input.trace === undefined ? {} : { trace: input.trace }),
|
|
254
|
+
})
|
|
255
|
+
throw error
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
evidence: () => evidenceScope.evidence(),
|
|
259
|
+
markBegan: () => evidenceScope.markBegan(),
|
|
260
|
+
markCommitted: () => evidenceScope.markCommitted(),
|
|
261
|
+
markAmbiguous: () => evidenceScope.markAmbiguous(),
|
|
262
|
+
markFailed: () => evidenceScope.markFailed(),
|
|
263
|
+
markSafeToRetry: () => evidenceScope.markSafeToRetry(),
|
|
264
|
+
safeToRetry: () => evidenceScope.safeToRetry(),
|
|
265
|
+
}
|
|
266
|
+
return Object.freeze(scope)
|
|
267
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in server integration for request idempotency.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from `@nifrajs/core/server` so a bare HTTP app does not evaluate or bundle the
|
|
5
|
+
* dedupe lane. Import primitives and stores from `@nifrajs/core/idempotency`.
|
|
6
|
+
*/
|
|
7
|
+
export { type IdempotencyPluginOptions, idempotency } from "./server/idempotency-lane.ts"
|
|
8
|
+
|
|
9
|
+
import { markRequestSafeToRetry } from "./internal/effect-execution.ts"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Opt a concrete 5xx response into releasing its idempotency reservation, but only while the
|
|
13
|
+
* request-local effect scope still proves that no owned effect began.
|
|
14
|
+
*/
|
|
15
|
+
export function markIdempotencySafeToRetry(context: object): void {
|
|
16
|
+
markRequestSafeToRetry(context)
|
|
17
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,144 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* `@nifrajs/core` — Nifra's lean HTTP server API.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
4
|
+
* Nifra 2.0 keeps the package root intentionally lean. Optional systems live at documented
|
|
5
|
+
* subpaths so importing the root cannot activate unrelated runtimes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Current package version. A hardcoded literal on purpose — core runs on the edge (no fs), so it can't
|
|
10
10
|
* read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
|
|
11
|
-
* `check:publish` asserts it equals `@nifrajs/core`'s package version
|
|
12
|
-
* (it shipped at "0.0.0" through 1.0.0 before those guards existed). Kept narrow (`as const`) so
|
|
13
|
-
* consumers can pin it at the type level — see `test/version.test-d.ts`.
|
|
11
|
+
* `check:publish` asserts it equals `@nifrajs/core`'s package version.
|
|
14
12
|
*/
|
|
15
|
-
export const VERSION = "1.
|
|
13
|
+
export const VERSION = "2.1.0" as const
|
|
16
14
|
|
|
17
15
|
export type Version = typeof VERSION
|
|
18
16
|
|
|
19
|
-
export * from "./
|
|
20
|
-
export * from "./capabilities.ts"
|
|
21
|
-
export * from "./causality.ts"
|
|
22
|
-
export * from "./classification.ts"
|
|
23
|
-
export * from "./diff.ts"
|
|
24
|
-
export { FrameworkError, RouteConfigError, type RouteConfigErrorCode } from "./errors.ts"
|
|
25
|
-
export * from "./idempotency.ts"
|
|
26
|
-
export { FRAMEWORK_NAME, type FrameworkName } from "./internal/brand.ts"
|
|
27
|
-
export * from "./invariants.ts"
|
|
28
|
-
export * from "./ledger.ts"
|
|
29
|
-
export * from "./manifest.ts"
|
|
30
|
-
export * from "./mount.ts"
|
|
31
|
-
export * from "./reflection.ts"
|
|
32
|
-
export * from "./router/pattern.ts"
|
|
33
|
-
export { METHODS, type Method, Router, type RouterMatch } from "./router/router.ts"
|
|
34
|
-
export type {
|
|
35
|
-
InferInput,
|
|
36
|
-
InferOutput,
|
|
37
|
-
StandardIssue,
|
|
38
|
-
StandardResult,
|
|
39
|
-
StandardSchemaV1,
|
|
40
|
-
StandardTypes,
|
|
41
|
-
ValidationOutcome,
|
|
42
|
-
} from "./schema/standard.ts"
|
|
43
|
-
export { validateStandard } from "./schema/standard.ts"
|
|
44
|
-
export {
|
|
45
|
-
type RobotsOptions,
|
|
46
|
-
type RobotsRule,
|
|
47
|
-
robots,
|
|
48
|
-
type SitemapChangeFreq,
|
|
49
|
-
type SitemapEntry,
|
|
50
|
-
type SitemapOptions,
|
|
51
|
-
sitemap,
|
|
52
|
-
} from "./seo.ts"
|
|
53
|
-
export type {
|
|
54
|
-
Context,
|
|
55
|
-
Params,
|
|
56
|
-
Platform,
|
|
57
|
-
Prettify,
|
|
58
|
-
ResponseControls,
|
|
59
|
-
RouteSchema,
|
|
60
|
-
} from "./server/context.ts"
|
|
61
|
-
export {
|
|
62
|
-
type ContextForOp,
|
|
63
|
-
type ContractShape,
|
|
64
|
-
defineContract,
|
|
65
|
-
type HandlersFor,
|
|
66
|
-
implement,
|
|
67
|
-
type OperationDef,
|
|
68
|
-
type RegistryFor,
|
|
69
|
-
type RegistryFromImpl,
|
|
70
|
-
type ResponseDef,
|
|
71
|
-
} from "./server/contract.ts"
|
|
72
|
-
export {
|
|
73
|
-
type CookieOptions,
|
|
74
|
-
parseCookies,
|
|
75
|
-
serializeCookie,
|
|
76
|
-
signValue,
|
|
77
|
-
unsignValue,
|
|
78
|
-
} from "./server/cookies.ts"
|
|
79
|
-
export {
|
|
80
|
-
commonSecretPatterns,
|
|
81
|
-
jsonLogger,
|
|
82
|
-
type LogFields,
|
|
83
|
-
type Logger,
|
|
84
|
-
type RedactOptions,
|
|
85
|
-
redactLogFields,
|
|
86
|
-
silentLogger,
|
|
87
|
-
} from "./server/logger.ts"
|
|
88
|
-
export type { Registry, RouteInfo } from "./server/registry.ts"
|
|
89
|
-
export {
|
|
90
|
-
type AdmissionController,
|
|
91
|
-
type AdmissionDecision,
|
|
92
|
-
type AnyServer,
|
|
93
|
-
type DurableObjectNamespaceLike,
|
|
94
|
-
defineIdentityPlugin,
|
|
95
|
-
definePlugin,
|
|
96
|
-
defineRouterPlugin,
|
|
97
|
-
type ExecutionContext,
|
|
98
|
-
type Handler,
|
|
99
|
-
type IdentityPlugin,
|
|
100
|
-
type McpPromptDescriptor,
|
|
101
|
-
type McpResourceDescriptor,
|
|
102
|
-
type Middleware,
|
|
103
|
-
type NifraPlugin,
|
|
104
|
-
type NodeServeOutcome,
|
|
105
|
-
type OnRequestResult,
|
|
106
|
-
type PromptArgument,
|
|
107
|
-
type PromptMessage,
|
|
108
|
-
type ResponseFinalization,
|
|
109
|
-
type RouteDescriptor,
|
|
110
|
-
type RunningServer,
|
|
111
|
-
type ScheduledController,
|
|
112
|
-
type ScheduledHandler,
|
|
113
|
-
Server,
|
|
114
|
-
type ServerOptions,
|
|
115
|
-
server,
|
|
116
|
-
type ToolAnnotations,
|
|
117
|
-
toFetchHandler,
|
|
118
|
-
} from "./server/server.ts"
|
|
119
|
-
export {
|
|
120
|
-
type SSEContext,
|
|
121
|
-
type SSEInit,
|
|
122
|
-
type SSEMessage,
|
|
123
|
-
type SSEStream,
|
|
124
|
-
sse,
|
|
125
|
-
type TypedSSEStream,
|
|
126
|
-
typedSSEStream,
|
|
127
|
-
} from "./server/sse.ts"
|
|
128
|
-
export {
|
|
129
|
-
type SignatureEncoding,
|
|
130
|
-
type VerifyWebhookOptions,
|
|
131
|
-
verifyWebhook,
|
|
132
|
-
type WebhookFailureReason,
|
|
133
|
-
type WebhookProvider,
|
|
134
|
-
type WebhookResult,
|
|
135
|
-
} from "./server/webhook.ts"
|
|
136
|
-
export type {
|
|
137
|
-
NifraWebSocket,
|
|
138
|
-
StandardWebSocket,
|
|
139
|
-
WebSocketContext,
|
|
140
|
-
WebSocketData,
|
|
141
|
-
WebSocketHandler,
|
|
142
|
-
WebSocketUpgradeOutcome,
|
|
143
|
-
} from "./server/websocket.ts"
|
|
144
|
-
export { attachWebSocket, TopicRegistry } from "./server/websocket.ts"
|
|
17
|
+
export * from "./server.ts"
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { EffectLifecycleObserver } from "../effect-lifecycle.ts"
|
|
2
|
+
import type { EffectCost } from "../ledger.ts"
|
|
3
|
+
|
|
1
4
|
export const CAPABILITY_GUARD = Symbol("nifra.capability.guard")
|
|
2
5
|
const CAPABILITY_ID = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/
|
|
3
6
|
|
|
@@ -11,11 +14,83 @@ export interface CapabilityUseEvent {
|
|
|
11
14
|
readonly path: string
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
/** Token-only metadata supplied to an asynchronous capability admission interceptor. */
|
|
18
|
+
export interface CapabilityInterceptorEvent extends CapabilityUseEvent {
|
|
19
|
+
readonly effectId: string
|
|
20
|
+
readonly signal: AbortSignal
|
|
21
|
+
readonly target?: string
|
|
22
|
+
readonly cost?: EffectCost
|
|
23
|
+
readonly digest?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Server-owned identity binding for durable admission. Values are opaque tokens, never payloads. */
|
|
27
|
+
export interface CapabilityExecutionIdentity {
|
|
28
|
+
readonly tenantId: string
|
|
29
|
+
readonly principalId: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CapabilityApprovalInput extends CapabilityExecutionIdentity {
|
|
33
|
+
/** Signed token returned by the approval coordinator after the first suspended attempt. */
|
|
34
|
+
readonly resumeToken?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CapabilityApprovalGate {
|
|
38
|
+
authorize(input: {
|
|
39
|
+
readonly effectId: string
|
|
40
|
+
readonly capability: string
|
|
41
|
+
readonly target?: string
|
|
42
|
+
readonly digest?: string
|
|
43
|
+
readonly identity: CapabilityExecutionIdentity
|
|
44
|
+
readonly resumeToken?: string
|
|
45
|
+
readonly signal: AbortSignal
|
|
46
|
+
}): void | PromiseLike<void>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Durable, token-only journal seam. Implementations must fail closed on transition failure. */
|
|
50
|
+
export interface CapabilityExecutionJournal {
|
|
51
|
+
intent(input: {
|
|
52
|
+
readonly effectId: string
|
|
53
|
+
readonly capability: string
|
|
54
|
+
readonly target?: string
|
|
55
|
+
readonly digest?: string
|
|
56
|
+
readonly identity?: CapabilityExecutionIdentity
|
|
57
|
+
}): void | PromiseLike<void>
|
|
58
|
+
executing(effectId: string): void | PromiseLike<void>
|
|
59
|
+
committed(effectId: string): void | PromiseLike<void>
|
|
60
|
+
failed(
|
|
61
|
+
effectId: string,
|
|
62
|
+
input: { readonly began: boolean; readonly errorCode: string },
|
|
63
|
+
): void | PromiseLike<void>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Continue to the next admission policy. The owned effect runs only after the full chain admits. */
|
|
67
|
+
export type CapabilityInterceptorNext = () => Promise<void>
|
|
68
|
+
|
|
69
|
+
export type CapabilityInterceptor = (
|
|
70
|
+
event: CapabilityInterceptorEvent,
|
|
71
|
+
next: CapabilityInterceptorNext,
|
|
72
|
+
) => void | PromiseLike<void>
|
|
73
|
+
|
|
74
|
+
export interface AroundCapabilityOptions {
|
|
75
|
+
/** Maximum admission time in milliseconds. Default 30_000. */
|
|
76
|
+
readonly timeoutMs?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface RegisteredCapabilityInterceptor {
|
|
80
|
+
readonly interceptor: CapabilityInterceptor
|
|
81
|
+
readonly timeoutMs: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS = 30_000
|
|
85
|
+
|
|
14
86
|
export interface CapabilityGuard {
|
|
15
87
|
readonly allowed: readonly string[]
|
|
16
88
|
readonly method: string
|
|
17
89
|
readonly path: string
|
|
18
90
|
readonly onUse?: (event: CapabilityUseEvent) => void
|
|
91
|
+
readonly trackEffect?: (context: object, committed: boolean) => void
|
|
92
|
+
readonly interceptors: readonly RegisteredCapabilityInterceptor[]
|
|
93
|
+
readonly observers: readonly EffectLifecycleObserver[]
|
|
19
94
|
}
|
|
20
95
|
|
|
21
96
|
export function normalizeRouteCapabilities(
|
|
@@ -44,6 +119,17 @@ export function createCapabilityGuard(
|
|
|
44
119
|
method: string,
|
|
45
120
|
path: string,
|
|
46
121
|
onUse: ((event: CapabilityUseEvent) => void) | undefined,
|
|
122
|
+
trackEffect: ((context: object, committed: boolean) => void) | undefined,
|
|
123
|
+
interceptors: readonly RegisteredCapabilityInterceptor[],
|
|
124
|
+
observers: readonly EffectLifecycleObserver[],
|
|
47
125
|
): CapabilityGuard {
|
|
48
|
-
return Object.freeze({
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
allowed,
|
|
128
|
+
method,
|
|
129
|
+
path,
|
|
130
|
+
...(onUse !== undefined ? { onUse } : {}),
|
|
131
|
+
...(trackEffect !== undefined ? { trackEffect } : {}),
|
|
132
|
+
interceptors,
|
|
133
|
+
observers,
|
|
134
|
+
})
|
|
49
135
|
}
|