@nifrajs/core 2.14.1 → 3.2.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 +25 -0
- package/dist/assurance.d.ts +16 -8
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +33 -0
- package/dist/assurance.js.map +1 -1
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +26 -3
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-approval.d.ts +56 -0
- package/dist/durable-approval.d.ts.map +1 -0
- package/dist/durable-approval.js +233 -0
- package/dist/durable-approval.js.map +1 -0
- package/dist/durable-effect.d.ts +42 -0
- package/dist/durable-effect.d.ts.map +1 -0
- package/dist/durable-effect.js +234 -0
- package/dist/durable-effect.js.map +1 -0
- package/dist/durable-execution.d.ts +8 -346
- package/dist/durable-execution.d.ts.map +1 -1
- package/dist/durable-execution.js +7 -1150
- package/dist/durable-execution.js.map +1 -1
- package/dist/durable-saga.d.ts +94 -0
- package/dist/durable-saga.d.ts.map +1 -0
- package/dist/durable-saga.js +550 -0
- package/dist/durable-saga.js.map +1 -0
- package/dist/edge-kit.d.ts +27 -0
- package/dist/edge-kit.d.ts.map +1 -0
- package/dist/edge-kit.js +24 -0
- package/dist/edge-kit.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +23 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-approval-protocol.d.ts +1 -1
- package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
- package/dist/internal/durable-effect-protocol.d.ts +1 -1
- package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
- package/dist/internal/durable-saga-protocol.d.ts +1 -1
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
- package/dist/internal/durable-shared.d.ts +26 -0
- package/dist/internal/durable-shared.d.ts.map +1 -0
- package/dist/internal/durable-shared.js +131 -0
- package/dist/internal/durable-shared.js.map +1 -0
- package/dist/internal/durable-types.d.ts +159 -0
- package/dist/internal/durable-types.d.ts.map +1 -0
- package/dist/internal/durable-types.js +2 -0
- package/dist/internal/durable-types.js.map +1 -0
- package/dist/internal/route-compiler.d.ts +61 -0
- package/dist/internal/route-compiler.d.ts.map +1 -0
- package/dist/internal/route-compiler.js +121 -0
- package/dist/internal/route-compiler.js.map +1 -0
- package/dist/internal/route-execution.d.ts +17 -9
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +4 -18
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +35 -0
- package/dist/internal/route-lanes.d.ts.map +1 -0
- package/dist/internal/route-lanes.js +150 -0
- package/dist/internal/route-lanes.js.map +1 -0
- package/dist/internal/route-program.d.ts +67 -0
- package/dist/internal/route-program.d.ts.map +1 -0
- package/dist/internal/route-program.js +199 -0
- package/dist/internal/route-program.js.map +1 -0
- package/dist/response-observer.d.ts +25 -0
- package/dist/response-observer.d.ts.map +1 -0
- package/dist/response-observer.js +40 -0
- package/dist/response-observer.js.map +1 -0
- package/dist/security/baseline.d.ts +68 -0
- package/dist/security/baseline.d.ts.map +1 -0
- package/dist/security/baseline.js +118 -0
- package/dist/security/baseline.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +26 -0
- package/dist/server/bare-error-lane.d.ts.map +1 -0
- package/dist/server/bare-error-lane.js +60 -0
- package/dist/server/bare-error-lane.js.map +1 -0
- package/dist/server/body-lane.d.ts +13 -0
- package/dist/server/body-lane.d.ts.map +1 -0
- package/dist/server/body-lane.js +134 -0
- package/dist/server/body-lane.js.map +1 -0
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +7 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +25 -0
- package/dist/server/bridge-markers.d.ts.map +1 -0
- package/dist/server/bridge-markers.js +25 -0
- package/dist/server/bridge-markers.js.map +1 -0
- package/dist/server/context.d.ts +6 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +40 -14
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +7 -0
- package/dist/server/contract.js.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +16 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/header-case.d.ts.map +1 -1
- package/dist/server/header-case.js +2 -1
- package/dist/server/header-case.js.map +1 -1
- package/dist/server/http.d.ts +36 -3
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +49 -3
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +6 -8
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +7 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/node-outcome-hook.d.ts +2 -1
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/node-outcome.d.ts +17 -2
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +37 -3
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/plugin.d.ts +2 -2
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js +6 -1
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/query.d.ts +7 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -3
- package/dist/server/query.js.map +1 -1
- package/dist/server/registry.d.ts +40 -12
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +2 -13
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +18 -159
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +58 -4
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +5 -2
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +20 -0
- package/dist/server/response-observer-runtime.d.ts.map +1 -0
- package/dist/server/response-observer-runtime.js +95 -0
- package/dist/server/response-observer-runtime.js.map +1 -0
- package/dist/server/runtime-core.d.ts +76 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +42 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +6 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +158 -116
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +707 -844
- package/dist/server/server.js.map +1 -1
- package/dist/server/validation.d.ts +5 -0
- package/dist/server/validation.d.ts.map +1 -0
- package/dist/server/validation.js +13 -0
- package/dist/server/validation.js.map +1 -0
- package/dist/server/websocket.d.ts +20 -4
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +81 -11
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +12 -4
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +50 -16
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +3 -2
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.d.ts +2 -0
- package/dist/single-copy-register.d.ts.map +1 -0
- package/dist/single-copy-register.js +24 -0
- package/dist/single-copy-register.js.map +1 -0
- package/dist/single-copy.d.ts +156 -0
- package/dist/single-copy.d.ts.map +1 -0
- package/dist/single-copy.js +472 -0
- package/dist/single-copy.js.map +1 -0
- package/package.json +28 -3
- package/src/assurance.ts +58 -1
- package/src/durable-adapters.ts +20 -0
- package/src/durable-approval.ts +281 -0
- package/src/durable-effect.ts +300 -0
- package/src/durable-execution.ts +8 -1627
- package/src/durable-saga.ts +744 -0
- package/src/edge-kit.ts +27 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +129 -2
- package/src/internal/durable-approval-protocol.ts +1 -1
- package/src/internal/durable-effect-protocol.ts +1 -1
- package/src/internal/durable-saga-protocol.ts +1 -1
- package/src/internal/durable-shared.ts +157 -0
- package/src/internal/durable-types.ts +194 -0
- package/src/internal/route-compiler.ts +234 -0
- package/src/internal/route-execution.ts +53 -96
- package/src/internal/route-lanes.ts +221 -0
- package/src/internal/route-program.ts +404 -0
- package/src/response-observer.ts +64 -0
- package/src/security/baseline.ts +172 -0
- package/src/server/bare-error-lane.ts +77 -0
- package/src/server/body-lane.ts +203 -0
- package/src/server/body.ts +8 -0
- package/src/server/bridge-markers.ts +24 -0
- package/src/server/context.ts +6 -1
- package/src/server/contract.ts +70 -20
- package/src/server/cookies.ts +16 -2
- package/src/server/header-case.ts +3 -1
- package/src/server/http.ts +53 -3
- package/src/server/install.ts +14 -0
- package/src/server/node-outcome-hook.ts +2 -1
- package/src/server/node-outcome.ts +44 -3
- package/src/server/plugin.ts +11 -5
- package/src/server/query.ts +10 -4
- package/src/server/registry.ts +71 -16
- package/src/server/request-context.ts +13 -216
- package/src/server/respond.ts +71 -3
- package/src/server/response-contract-lane.ts +6 -2
- package/src/server/response-observer-runtime.ts +135 -0
- package/src/server/runtime-core.ts +124 -1
- package/src/server/server-types.ts +6 -1
- package/src/server/server.ts +1446 -1485
- package/src/server/validation.ts +19 -0
- package/src/server/websocket.ts +127 -17
- package/src/server/ws-bun.ts +67 -15
- package/src/server/ws-hook.ts +3 -2
- package/src/server.ts +8 -2
- package/src/single-copy-register.ts +24 -0
- package/src/single-copy.ts +526 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { type EffectLifecycleObserver, emitEffectLifecycle } from "./effect-lifecycle.ts"
|
|
2
|
+
import type { CapabilityExecutionJournal } from "./internal/capability-runtime.ts"
|
|
3
|
+
import { validCapabilityId } from "./internal/capability-runtime.ts"
|
|
4
|
+
import { durableEffectProtocol } from "./internal/durable-effect-protocol.ts"
|
|
5
|
+
import {
|
|
6
|
+
assertTimestamp,
|
|
7
|
+
assertToken,
|
|
8
|
+
bucketAdd,
|
|
9
|
+
bucketMove,
|
|
10
|
+
cloneValue,
|
|
11
|
+
ERROR_CODE,
|
|
12
|
+
indexedScan,
|
|
13
|
+
MAX_RECONCILIATION_LIMIT,
|
|
14
|
+
memoryScan,
|
|
15
|
+
readClock,
|
|
16
|
+
reconciliationLimit,
|
|
17
|
+
} from "./internal/durable-shared.ts"
|
|
18
|
+
import type {
|
|
19
|
+
DurableEffectRecord,
|
|
20
|
+
DurableEffectState,
|
|
21
|
+
DurableEffectStore,
|
|
22
|
+
ReconciliationPage,
|
|
23
|
+
ReconciliationScanOptions,
|
|
24
|
+
ReconciliationScanPage,
|
|
25
|
+
} from "./internal/durable-types.ts"
|
|
26
|
+
// Durable effect journal + reconciliation
|
|
27
|
+
|
|
28
|
+
export class DurableEffectTransitionError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
public readonly effectId: string,
|
|
31
|
+
public readonly transition: string,
|
|
32
|
+
) {
|
|
33
|
+
super(`durable effect ${effectId}: ${transition} transition was rejected`)
|
|
34
|
+
this.name = "DurableEffectTransitionError"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DurableEffectJournalOptions {
|
|
39
|
+
readonly store: DurableEffectStore
|
|
40
|
+
readonly now?: () => number
|
|
41
|
+
/** Tests/local development only. Production journals require `store.durability === "durable"`. */
|
|
42
|
+
readonly allowMemoryStore?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createDurableEffectJournal(
|
|
46
|
+
options: DurableEffectJournalOptions,
|
|
47
|
+
): CapabilityExecutionJournal {
|
|
48
|
+
if (options.store.durability !== "durable" && options.allowMemoryStore !== true) {
|
|
49
|
+
throw new TypeError('durable effect journal requires store.durability === "durable"')
|
|
50
|
+
}
|
|
51
|
+
const clock = options.now ?? Date.now
|
|
52
|
+
const now = (): number => readClock(clock, "durable effect clock")
|
|
53
|
+
|
|
54
|
+
const current = async (effectId: string): Promise<DurableEffectRecord> => {
|
|
55
|
+
assertToken(effectId, "durable effect id", 64)
|
|
56
|
+
const record = await options.store.get(effectId)
|
|
57
|
+
if (record === undefined) throw new DurableEffectTransitionError(effectId, "missing")
|
|
58
|
+
if (
|
|
59
|
+
record.effectId !== effectId ||
|
|
60
|
+
!validCapabilityId(record.capability) ||
|
|
61
|
+
!["admission", "executing", "committed", "failed", "unknown"].includes(record.state) ||
|
|
62
|
+
!Number.isSafeInteger(record.version) ||
|
|
63
|
+
record.version < 1
|
|
64
|
+
) {
|
|
65
|
+
throw new TypeError(`durable effect ${effectId}: store returned an invalid record`)
|
|
66
|
+
}
|
|
67
|
+
assertTimestamp(record.createdAt, "durable effect createdAt")
|
|
68
|
+
assertTimestamp(record.updatedAt, "durable effect updatedAt")
|
|
69
|
+
if (record.createdAt > record.updatedAt)
|
|
70
|
+
throw new TypeError(`durable effect ${effectId}: updatedAt precedes createdAt`)
|
|
71
|
+
if (record.target !== undefined) assertToken(record.target, "stored durable effect target", 128)
|
|
72
|
+
if (record.digest !== undefined && !/^[0-9a-f]{64}$/u.test(record.digest))
|
|
73
|
+
throw new TypeError(`durable effect ${effectId}: store returned an invalid digest`)
|
|
74
|
+
if (record.tenantId !== undefined)
|
|
75
|
+
assertToken(record.tenantId, "stored durable effect tenantId")
|
|
76
|
+
if (record.principalId !== undefined)
|
|
77
|
+
assertToken(record.principalId, "stored durable effect principalId")
|
|
78
|
+
if ((record.tenantId === undefined) !== (record.principalId === undefined))
|
|
79
|
+
throw new TypeError(`durable effect ${effectId}: store returned a partial identity`)
|
|
80
|
+
if (record.errorCode !== undefined && !ERROR_CODE.test(record.errorCode))
|
|
81
|
+
throw new TypeError(`durable effect ${effectId}: store returned an invalid error code`)
|
|
82
|
+
return record
|
|
83
|
+
}
|
|
84
|
+
const transition = async (
|
|
85
|
+
effectId: string,
|
|
86
|
+
from: DurableEffectState,
|
|
87
|
+
to: DurableEffectState,
|
|
88
|
+
errorCode?: string,
|
|
89
|
+
): Promise<void> => {
|
|
90
|
+
const record = await current(effectId)
|
|
91
|
+
const next = durableEffectProtocol.transition(record, {
|
|
92
|
+
effectId,
|
|
93
|
+
version: record.version,
|
|
94
|
+
from,
|
|
95
|
+
to,
|
|
96
|
+
updatedAt: now(),
|
|
97
|
+
...(errorCode === undefined ? {} : { errorCode }),
|
|
98
|
+
})
|
|
99
|
+
if (next === undefined) throw new DurableEffectTransitionError(effectId, `${from}->${to}`)
|
|
100
|
+
const accepted = await options.store.transition({
|
|
101
|
+
effectId,
|
|
102
|
+
version: record.version,
|
|
103
|
+
from,
|
|
104
|
+
to,
|
|
105
|
+
updatedAt: next.updatedAt,
|
|
106
|
+
...(errorCode === undefined ? {} : { errorCode }),
|
|
107
|
+
})
|
|
108
|
+
if (!accepted) throw new DurableEffectTransitionError(effectId, `${from}->${to}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const journal: CapabilityExecutionJournal = {
|
|
112
|
+
async intent(input) {
|
|
113
|
+
assertToken(input.effectId, "durable effect id", 64)
|
|
114
|
+
if (!validCapabilityId(input.capability))
|
|
115
|
+
throw new TypeError("durable effect capability is invalid")
|
|
116
|
+
if (input.target !== undefined) assertToken(input.target, "durable effect target", 128)
|
|
117
|
+
if (input.digest !== undefined && !/^[0-9a-f]{64}$/u.test(input.digest))
|
|
118
|
+
throw new TypeError("durable effect digest is invalid")
|
|
119
|
+
if (input.identity !== undefined) {
|
|
120
|
+
assertToken(input.identity.tenantId, "durable effect tenantId")
|
|
121
|
+
assertToken(input.identity.principalId, "durable effect principalId")
|
|
122
|
+
}
|
|
123
|
+
const at = now()
|
|
124
|
+
const accepted = await options.store.create(
|
|
125
|
+
Object.freeze({
|
|
126
|
+
effectId: input.effectId,
|
|
127
|
+
capability: input.capability,
|
|
128
|
+
...(input.target === undefined ? {} : { target: input.target }),
|
|
129
|
+
...(input.digest === undefined ? {} : { digest: input.digest }),
|
|
130
|
+
...(input.identity === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: { tenantId: input.identity.tenantId, principalId: input.identity.principalId }),
|
|
133
|
+
state: "admission" as const,
|
|
134
|
+
createdAt: at,
|
|
135
|
+
updatedAt: at,
|
|
136
|
+
version: 1,
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
139
|
+
if (!accepted) throw new DurableEffectTransitionError(input.effectId, "create")
|
|
140
|
+
},
|
|
141
|
+
async executing(effectId) {
|
|
142
|
+
await transition(effectId, "admission", "executing")
|
|
143
|
+
},
|
|
144
|
+
async committed(effectId) {
|
|
145
|
+
await transition(effectId, "executing", "committed")
|
|
146
|
+
},
|
|
147
|
+
async failed(effectId, input) {
|
|
148
|
+
if (!ERROR_CODE.test(input.errorCode))
|
|
149
|
+
throw new TypeError("durable effect errorCode is invalid")
|
|
150
|
+
const record = await current(effectId)
|
|
151
|
+
const to = input.began ? "unknown" : "failed"
|
|
152
|
+
const next = durableEffectProtocol.transition(record, {
|
|
153
|
+
effectId,
|
|
154
|
+
version: record.version,
|
|
155
|
+
from: record.state,
|
|
156
|
+
to,
|
|
157
|
+
updatedAt: now(),
|
|
158
|
+
errorCode: input.errorCode,
|
|
159
|
+
})
|
|
160
|
+
if (next === undefined)
|
|
161
|
+
throw new DurableEffectTransitionError(effectId, `${record.state}->${to}`)
|
|
162
|
+
const accepted = await options.store.transition({
|
|
163
|
+
effectId,
|
|
164
|
+
version: record.version,
|
|
165
|
+
from: record.state,
|
|
166
|
+
to,
|
|
167
|
+
updatedAt: next.updatedAt,
|
|
168
|
+
errorCode: input.errorCode,
|
|
169
|
+
})
|
|
170
|
+
if (!accepted) throw new DurableEffectTransitionError(effectId, `${record.state}->${to}`)
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
return Object.freeze(journal)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class MemoryDurableEffectStore implements DurableEffectStore {
|
|
177
|
+
readonly durability = "memory" as const
|
|
178
|
+
private readonly records = new Map<string, DurableEffectRecord>()
|
|
179
|
+
// Secondary index: effectIds grouped by state, so reconciliation scans only the requested (few,
|
|
180
|
+
// non-terminal) states instead of walking every retained record. See {@link indexedScan}.
|
|
181
|
+
private readonly byState = new Map<DurableEffectState, Set<string>>()
|
|
182
|
+
|
|
183
|
+
create(record: DurableEffectRecord): boolean {
|
|
184
|
+
if (this.records.has(record.effectId)) return false
|
|
185
|
+
this.records.set(record.effectId, Object.freeze(cloneValue(record)))
|
|
186
|
+
bucketAdd(this.byState, record.state, record.effectId)
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get(effectId: string): DurableEffectRecord | undefined {
|
|
191
|
+
const record = this.records.get(effectId)
|
|
192
|
+
return record === undefined ? undefined : Object.freeze(cloneValue(record))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
transition(input: Parameters<DurableEffectStore["transition"]>[0]): boolean {
|
|
196
|
+
const current = this.records.get(input.effectId)
|
|
197
|
+
const next = durableEffectProtocol.transition(current, input)
|
|
198
|
+
if (next === undefined) return false
|
|
199
|
+
if (current === undefined) return false
|
|
200
|
+
this.records.set(input.effectId, Object.freeze(next))
|
|
201
|
+
bucketMove(this.byState, current.state, input.to, input.effectId)
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
list(): readonly DurableEffectRecord[] {
|
|
206
|
+
return Object.freeze(
|
|
207
|
+
[...this.records.values()].map((record) => Object.freeze(cloneValue(record))),
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
scan(
|
|
212
|
+
input: ReconciliationScanOptions<DurableEffectState>,
|
|
213
|
+
): ReconciliationScanPage<DurableEffectRecord> {
|
|
214
|
+
return indexedScan(this.byState, (id) => this.records.get(id), input)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface EffectReconciliationFinding {
|
|
219
|
+
readonly effectId: string
|
|
220
|
+
readonly capability: string
|
|
221
|
+
readonly state: "incomplete" | "ambiguous"
|
|
222
|
+
readonly updatedAt: number
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function scanEffectRecords(
|
|
226
|
+
store: DurableEffectStore,
|
|
227
|
+
input: ReconciliationScanOptions<DurableEffectState>,
|
|
228
|
+
): Promise<ReconciliationScanPage<DurableEffectRecord>> {
|
|
229
|
+
if (store.scan !== undefined) return await store.scan(input)
|
|
230
|
+
if (store.list === undefined) throw new TypeError("effect store does not support reconciliation")
|
|
231
|
+
return memoryScan(await store.list(), input)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function reconcileEffectsPage(
|
|
235
|
+
store: DurableEffectStore,
|
|
236
|
+
options: {
|
|
237
|
+
readonly staleBefore: number
|
|
238
|
+
readonly observer?: EffectLifecycleObserver
|
|
239
|
+
readonly cursor?: string
|
|
240
|
+
readonly limit?: number
|
|
241
|
+
},
|
|
242
|
+
): Promise<ReconciliationPage<EffectReconciliationFinding>> {
|
|
243
|
+
const findings: EffectReconciliationFinding[] = []
|
|
244
|
+
const page = await scanEffectRecords(store, {
|
|
245
|
+
states: ["admission", "executing", "unknown"],
|
|
246
|
+
updatedBefore: options.staleBefore,
|
|
247
|
+
...(options.cursor === undefined ? {} : { cursor: options.cursor }),
|
|
248
|
+
limit: reconciliationLimit(options.limit),
|
|
249
|
+
})
|
|
250
|
+
for (const record of page.records) {
|
|
251
|
+
if (!validCapabilityId(record.capability) || !/^[!-~]{1,64}$/u.test(record.effectId)) continue
|
|
252
|
+
if (record.updatedAt > options.staleBefore) continue
|
|
253
|
+
const state =
|
|
254
|
+
record.state === "admission"
|
|
255
|
+
? "incomplete"
|
|
256
|
+
: record.state === "executing" || record.state === "unknown"
|
|
257
|
+
? "ambiguous"
|
|
258
|
+
: undefined
|
|
259
|
+
if (state === undefined) continue
|
|
260
|
+
findings.push(
|
|
261
|
+
Object.freeze({
|
|
262
|
+
effectId: record.effectId,
|
|
263
|
+
capability: record.capability,
|
|
264
|
+
state,
|
|
265
|
+
updatedAt: record.updatedAt,
|
|
266
|
+
}),
|
|
267
|
+
)
|
|
268
|
+
if (options.observer !== undefined) {
|
|
269
|
+
emitEffectLifecycle([options.observer], {
|
|
270
|
+
effectId: record.effectId,
|
|
271
|
+
capability: record.capability,
|
|
272
|
+
stage: "reconciliation",
|
|
273
|
+
phase: state === "ambiguous" ? "ambiguous" : "failed",
|
|
274
|
+
errorCode: state === "ambiguous" ? "effect_ambiguous" : "effect_incomplete",
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return Object.freeze({
|
|
279
|
+
findings: Object.freeze(findings),
|
|
280
|
+
...(page.cursor === undefined ? {} : { cursor: page.cursor }),
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function reconcileEffects(
|
|
285
|
+
store: DurableEffectStore,
|
|
286
|
+
options: { readonly staleBefore: number; readonly observer?: EffectLifecycleObserver },
|
|
287
|
+
): Promise<readonly EffectReconciliationFinding[]> {
|
|
288
|
+
const page = await reconcileEffectsPage(store, {
|
|
289
|
+
...options,
|
|
290
|
+
limit: MAX_RECONCILIATION_LIMIT,
|
|
291
|
+
})
|
|
292
|
+
if (page.cursor !== undefined) {
|
|
293
|
+
throw new RangeError(
|
|
294
|
+
`effect reconciliation exceeds ${MAX_RECONCILIATION_LIMIT} records; use reconcileEffectsPage()`,
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
return page.findings
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// -------------------------------------------------------------------------------------------------
|