@nifrajs/core 4.0.2 → 5.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 +6 -2
- package/dist/assurance.d.ts +86 -6
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +186 -95
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +8 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +53 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-effect.js +1 -1
- package/dist/durable-effect.js.map +1 -1
- package/dist/durable-saga.d.ts +6 -0
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +83 -10
- package/dist/durable-saga.js.map +1 -1
- package/dist/edge-kit.d.ts +8 -3
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +7 -2
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +16 -5
- package/dist/effect-scope.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +9 -1
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-shared.d.ts +1 -2
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +25 -44
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/durable-types.d.ts +3 -0
- package/dist/internal/durable-types.d.ts.map +1 -1
- package/dist/internal/hook-origins.d.ts +67 -0
- package/dist/internal/hook-origins.d.ts.map +1 -0
- package/dist/internal/hook-origins.js +86 -0
- package/dist/internal/hook-origins.js.map +1 -0
- package/dist/internal/route-assurance.d.ts +6 -3
- package/dist/internal/route-assurance.d.ts.map +1 -1
- package/dist/internal/route-assurance.js +29 -14
- package/dist/internal/route-assurance.js.map +1 -1
- package/dist/internal/route-compiler.d.ts +9 -0
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +95 -13
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-program.d.ts +1 -1
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +59 -16
- package/dist/internal/route-program.js.map +1 -1
- package/dist/internal/same-origin.d.ts +11 -1
- package/dist/internal/same-origin.d.ts.map +1 -1
- package/dist/internal/same-origin.js +8 -0
- package/dist/internal/same-origin.js.map +1 -1
- package/dist/reconciliation-worker.d.ts.map +1 -1
- package/dist/reconciliation-worker.js +12 -2
- package/dist/reconciliation-worker.js.map +1 -1
- package/dist/reflection.d.ts +53 -2
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +352 -5
- package/dist/reflection.js.map +1 -1
- package/dist/router/pattern.d.ts +1 -1
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +6 -3
- package/dist/router/pattern.js.map +1 -1
- package/dist/security/baseline.d.ts +3 -0
- package/dist/security/baseline.d.ts.map +1 -1
- package/dist/security/baseline.js +9 -5
- package/dist/security/baseline.js.map +1 -1
- package/dist/server/auth.d.ts +32 -3
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +23 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/body-lane.d.ts +8 -2
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +27 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +34 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +3 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js +6 -5
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +2 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +13 -1
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +2 -1
- package/dist/server/edge.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +5 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts +6 -3
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +38 -17
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +3 -1
- package/dist/server/logger.js.map +1 -1
- package/dist/server/methods.d.ts +1 -0
- package/dist/server/methods.d.ts.map +1 -1
- package/dist/server/methods.js +1 -0
- package/dist/server/methods.js.map +1 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +29 -11
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/platform-adapters.d.ts.map +1 -1
- package/dist/server/platform-adapters.js +3 -2
- package/dist/server/platform-adapters.js.map +1 -1
- package/dist/server/request-context.d.ts +2 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +4 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +44 -11
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +192 -79
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +21 -6
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +139 -14
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -41
- package/dist/server/response-observer-runtime.js.map +1 -1
- package/dist/server/runtime-core.d.ts +2 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +2 -0
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +45 -10
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +67 -32
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +481 -334
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +11 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +18 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/server/transport-lane.d.ts +7 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.d.ts +5 -2
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +17 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +33 -14
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +47 -12
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +7 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +42 -2
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +15 -3
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +4 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +6 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +55 -6
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +71 -40
- package/dist/tool-contract.js.map +1 -1
- package/dist/wire.d.ts +5 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +5 -0
- package/dist/wire.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +4 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/assurance.ts +299 -111
- package/src/capabilities.ts +62 -1
- package/src/durable-effect.ts +1 -1
- package/src/durable-saga.ts +94 -10
- package/src/edge-kit.ts +8 -3
- package/src/effect-scope.ts +15 -5
- package/src/errors.ts +1 -0
- package/src/evidence.ts +12 -3
- package/src/index.ts +7 -2
- package/src/internal/durable-shared.ts +25 -37
- package/src/internal/durable-types.ts +3 -0
- package/src/internal/hook-origins.ts +153 -0
- package/src/internal/route-assurance.ts +32 -18
- package/src/internal/route-compiler.ts +107 -16
- package/src/internal/route-program.ts +98 -44
- package/src/internal/same-origin.ts +14 -1
- package/src/reconciliation-worker.ts +10 -2
- package/src/reflection.ts +455 -4
- package/src/router/pattern.ts +6 -3
- package/src/security/baseline.ts +10 -5
- package/src/server/auth.ts +45 -3
- package/src/server/body-lane.ts +36 -1
- package/src/server/body.ts +34 -1
- package/src/server/client-ip.ts +8 -5
- package/src/server/context.ts +2 -1
- package/src/server/cookies.ts +12 -1
- package/src/server/edge.ts +2 -1
- package/src/server/http.ts +7 -1
- package/src/server/idempotency-lane.ts +64 -22
- package/src/server/install.ts +3 -0
- package/src/server/logger.ts +3 -1
- package/src/server/methods.ts +1 -0
- package/src/server/node-outcome.ts +34 -12
- package/src/server/platform-adapters.ts +3 -2
- package/src/server/request-context.ts +5 -0
- package/src/server/respond.ts +222 -69
- package/src/server/response-contract-lane.ts +162 -10
- package/src/server/response-observer-runtime.ts +16 -38
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +45 -10
- package/src/server/server.ts +707 -539
- package/src/server/sse.ts +84 -55
- package/src/server/transport-lane.ts +7 -1
- package/src/server/webhook.ts +22 -3
- package/src/server/websocket.ts +83 -18
- package/src/server/ws-bun.ts +48 -1
- package/src/server/ws-hook.ts +14 -2
- package/src/server.ts +6 -1
- package/src/single-copy.ts +55 -5
- package/src/tool-contract.ts +67 -39
- package/src/wire.ts +14 -0
- package/src/ws.ts +6 -0
package/src/capabilities.ts
CHANGED
|
@@ -154,6 +154,8 @@ export interface RouteCapabilityEvidence {
|
|
|
154
154
|
/** True only when the route's reachable module graph was actually scanned. */
|
|
155
155
|
readonly covered: boolean
|
|
156
156
|
readonly evidence: readonly CapabilityEvidence[]
|
|
157
|
+
/** The probable cause of an uncovered route, when the collector can tell. */
|
|
158
|
+
readonly uncoveredReason?: string
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
export interface CapabilityEvidenceSet {
|
|
@@ -195,6 +197,9 @@ export interface AssuredCapabilityRoute {
|
|
|
195
197
|
readonly covered: boolean
|
|
196
198
|
/** Highest data-sensitivity the response carries, when the route declares it. */
|
|
197
199
|
readonly classification?: DataClassification
|
|
200
|
+
/** `"declared"` for a route an opaque mount states it serves: listed on the mount's word, never
|
|
201
|
+
* analyzed. Absent on an analyzed route. */
|
|
202
|
+
readonly analysis?: "declared"
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
/**
|
|
@@ -225,6 +230,7 @@ export interface CapabilitySnapshotRoute {
|
|
|
225
230
|
readonly unproven: readonly string[]
|
|
226
231
|
/** Recorded so a route that starts returning `pii`/`secret` flips the lockfile and forces a review. */
|
|
227
232
|
readonly classification?: DataClassification
|
|
233
|
+
readonly analysis?: "declared"
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
export interface CapabilitySnapshot {
|
|
@@ -412,7 +418,7 @@ export function evaluateCapabilityAssurance(
|
|
|
412
418
|
code: "provenance-uncovered",
|
|
413
419
|
method: route.method,
|
|
414
420
|
path: route.path,
|
|
415
|
-
message: `${route.method} ${route.path} has no static provenance coverage`,
|
|
421
|
+
message: `${route.method} ${route.path} has no static provenance coverage${supplied?.uncoveredReason === undefined ? "" : ` - ${supplied.uncoveredReason}`}`,
|
|
416
422
|
})
|
|
417
423
|
}
|
|
418
424
|
// A safe-method route that REACHES a domain write without declaring it used to draw two findings
|
|
@@ -513,6 +519,45 @@ export function evaluateCapabilityAssurance(
|
|
|
513
519
|
for (const mount of evidenceSet.mounts ?? reflectMounts(source)) {
|
|
514
520
|
if (mount.opaque !== undefined) {
|
|
515
521
|
gaps.push(Object.freeze({ kind: "opaque-mount", path: mount.path, reason: mount.opaque }))
|
|
522
|
+
for (const route of mount.routes ?? []) {
|
|
523
|
+
// Only what a declaration alone can contradict: an id the policy lacks, or a write a safe
|
|
524
|
+
// method can never carry. Evidence-backed checks have nothing to read here.
|
|
525
|
+
for (const id of route.capabilities) {
|
|
526
|
+
const definition = definitions.get(id)
|
|
527
|
+
if (definition === undefined) {
|
|
528
|
+
findings.push({
|
|
529
|
+
code: "unknown-capability",
|
|
530
|
+
method: route.method,
|
|
531
|
+
path: route.path,
|
|
532
|
+
capability: id,
|
|
533
|
+
message: `${route.method} ${route.path} (declared by mount ${mount.path}) declares unknown capability ${id}`,
|
|
534
|
+
})
|
|
535
|
+
} else if (
|
|
536
|
+
SAFE_METHODS.has(route.method) &&
|
|
537
|
+
definition.zone === "domain" &&
|
|
538
|
+
definition.access === "write"
|
|
539
|
+
) {
|
|
540
|
+
findings.push({
|
|
541
|
+
code: "safe-method-domain-write",
|
|
542
|
+
method: route.method,
|
|
543
|
+
path: route.path,
|
|
544
|
+
capability: id,
|
|
545
|
+
message: `${route.method} ${route.path} (declared by mount ${mount.path}) cannot carry domain write capability ${id}`,
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
routes.push(
|
|
550
|
+
Object.freeze({
|
|
551
|
+
method: route.method,
|
|
552
|
+
path: route.path,
|
|
553
|
+
declared: route.capabilities,
|
|
554
|
+
evidence: Object.freeze([]),
|
|
555
|
+
unproven: route.capabilities,
|
|
556
|
+
covered: false,
|
|
557
|
+
analysis: "declared",
|
|
558
|
+
}),
|
|
559
|
+
)
|
|
560
|
+
}
|
|
516
561
|
continue
|
|
517
562
|
}
|
|
518
563
|
findings.push({
|
|
@@ -722,6 +767,21 @@ async function runCapabilityInterceptors(
|
|
|
722
767
|
await nextPromise
|
|
723
768
|
}
|
|
724
769
|
|
|
770
|
+
/** Preserve the request's effect/admission scope while restricting a nested execution's privileges. */
|
|
771
|
+
export function narrowCapabilityContext(context: object, allowed: readonly string[]): object {
|
|
772
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The private symbol stores only the capability guard installed by owner admission.
|
|
773
|
+
const guard = (context as { readonly [CAPABILITY_GUARD]?: CapabilityGuard })[CAPABILITY_GUARD]
|
|
774
|
+
const capabilities = Object.freeze([...new Set(allowed)])
|
|
775
|
+
for (const capability of capabilities) guardFor(context, capability)
|
|
776
|
+
const child: object = Object.create(context)
|
|
777
|
+
if (guard !== undefined) {
|
|
778
|
+
Object.defineProperty(child, CAPABILITY_GUARD, {
|
|
779
|
+
value: Object.freeze({ ...guard, allowed: capabilities }),
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
return child
|
|
783
|
+
}
|
|
784
|
+
|
|
725
785
|
function guardFor(context: object, capability: string): CapabilityGuard {
|
|
726
786
|
if (!validCapabilityId(capability)) {
|
|
727
787
|
throw new Error(
|
|
@@ -914,6 +974,7 @@ export function snapshotCapabilities(report: CapabilityAssuranceReport): Capabil
|
|
|
914
974
|
evidenced: Object.freeze([...new Set(route.evidence.map((item) => item.id))].sort()),
|
|
915
975
|
unproven: Object.freeze([...route.unproven].sort()),
|
|
916
976
|
...(route.classification !== undefined ? { classification: route.classification } : {}),
|
|
977
|
+
...(route.analysis !== undefined ? { analysis: route.analysis } : {}),
|
|
917
978
|
}),
|
|
918
979
|
)
|
|
919
980
|
.sort((a, b) => codeUnitOrder(a.path, b.path) || codeUnitOrder(a.method, b.method))
|
package/src/durable-effect.ts
CHANGED
|
@@ -235,7 +235,7 @@ async function scanEffectRecords(
|
|
|
235
235
|
): Promise<ReconciliationScanPage<DurableEffectRecord>> {
|
|
236
236
|
if (store.scan !== undefined) return await store.scan(input)
|
|
237
237
|
if (store.list === undefined) throw new TypeError("effect store does not support reconciliation")
|
|
238
|
-
return memoryScan(await store.list(), input)
|
|
238
|
+
return memoryScan(await store.list(), (record) => record.effectId, input)
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
export async function reconcileEffectsPage(
|
package/src/durable-saga.ts
CHANGED
|
@@ -118,6 +118,12 @@ export interface SagaEngineOptions {
|
|
|
118
118
|
readonly observer?: EffectLifecycleObserver
|
|
119
119
|
/** Tests/local development only. Production sagas require a durable store. */
|
|
120
120
|
readonly allowMemoryStore?: boolean
|
|
121
|
+
/**
|
|
122
|
+
* How long one `resume` or `compensate` call owns a saga. A second call while the lease is live
|
|
123
|
+
* throws `SagaConcurrencyError` and changes nothing; every transition renews the lease, and a call
|
|
124
|
+
* that died is taken over once it lapses. Default 5 minutes: longer than any one step should run.
|
|
125
|
+
*/
|
|
126
|
+
readonly leaseMs?: number
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
export type SagaAmbiguityResolution =
|
|
@@ -191,6 +197,8 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
191
197
|
const clock = options.now ?? Date.now
|
|
192
198
|
const now = (): number => readClock(clock, "saga clock")
|
|
193
199
|
const observer = options.observer
|
|
200
|
+
const leaseMs = options.leaseMs ?? 300_000
|
|
201
|
+
assertPositiveMs(leaseMs, "saga leaseMs")
|
|
194
202
|
|
|
195
203
|
const load = async (sagaId: string): Promise<SagaRecord> => {
|
|
196
204
|
const record = await options.store.get(sagaId)
|
|
@@ -207,6 +215,12 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
207
215
|
throw new TypeError(`saga ${sagaId}: store returned an invalid record`)
|
|
208
216
|
}
|
|
209
217
|
assertToken(record.definition, "stored saga definition", 128)
|
|
218
|
+
if (record.owner !== undefined) {
|
|
219
|
+
assertToken(record.owner, "stored saga owner", 64)
|
|
220
|
+
if (record.leaseUntil === undefined)
|
|
221
|
+
throw new TypeError(`saga ${sagaId}: store returned an owner without a lease`)
|
|
222
|
+
assertTimestamp(record.leaseUntil, "stored saga leaseUntil")
|
|
223
|
+
}
|
|
210
224
|
assertTimestamp(record.createdAt, "stored saga createdAt")
|
|
211
225
|
assertTimestamp(record.updatedAt, "stored saga updatedAt")
|
|
212
226
|
if (record.createdAt > record.updatedAt)
|
|
@@ -252,7 +266,14 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
252
266
|
const next = durableSagaProtocol.transition(current, {
|
|
253
267
|
sagaId: current.sagaId,
|
|
254
268
|
version: current.version,
|
|
255
|
-
record: Object.freeze({
|
|
269
|
+
record: Object.freeze({
|
|
270
|
+
...update,
|
|
271
|
+
// Every transition the owner writes renews its lease.
|
|
272
|
+
...(update.owner === undefined
|
|
273
|
+
? {}
|
|
274
|
+
: { leaseUntil: addDuration(now(), leaseMs, "saga leaseUntil") }),
|
|
275
|
+
version: current.version + 1,
|
|
276
|
+
}),
|
|
256
277
|
})
|
|
257
278
|
if (next === undefined) throw new SagaConcurrencyError(current.sagaId)
|
|
258
279
|
if (
|
|
@@ -275,10 +296,51 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
275
296
|
...(observer === undefined ? {} : { observers: [observer] }),
|
|
276
297
|
})
|
|
277
298
|
|
|
299
|
+
/** Take the saga for `owner`, or throw `SagaConcurrencyError` while another call's lease is live. */
|
|
300
|
+
const claim = async (record: SagaRecord, owner: string): Promise<SagaRecord> => {
|
|
301
|
+
if (record.owner === owner) return record
|
|
302
|
+
if (record.owner !== undefined && record.leaseUntil !== undefined && record.leaseUntil > now())
|
|
303
|
+
throw new SagaConcurrencyError(record.sagaId)
|
|
304
|
+
return await save(record, { ...record, owner, updatedAt: now() })
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Give the saga up so the next call need not wait out the lease. */
|
|
308
|
+
const release = async (sagaId: string, owner: string): Promise<void> => {
|
|
309
|
+
try {
|
|
310
|
+
const record = await load(sagaId)
|
|
311
|
+
if (record.owner !== owner) return
|
|
312
|
+
const { owner: _owner, leaseUntil: _leaseUntil, ...released } = record
|
|
313
|
+
await save(record, { ...released, updatedAt: now() })
|
|
314
|
+
} catch {
|
|
315
|
+
// A release that fails leaves the lease to lapse; the next call then takes the saga over.
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The saga as `owner` holds it; once another call took it over, `owner` must stop writing. */
|
|
320
|
+
const loadOwned = async (sagaId: string, owner: string): Promise<SagaRecord> => {
|
|
321
|
+
const record = await load(sagaId)
|
|
322
|
+
if (record.owner !== owner) throw new SagaConcurrencyError(sagaId)
|
|
323
|
+
return record
|
|
324
|
+
}
|
|
325
|
+
|
|
278
326
|
const compensate = async <I, C extends Record<string, unknown>>(
|
|
279
327
|
definition: SagaDefinition<I, C>,
|
|
280
328
|
sagaId: string,
|
|
281
329
|
runOptions: { readonly signal?: AbortSignal } = {},
|
|
330
|
+
): Promise<SagaRecord> => {
|
|
331
|
+
const owner = crypto.randomUUID()
|
|
332
|
+
try {
|
|
333
|
+
return await compensateOwned(definition, sagaId, runOptions, owner)
|
|
334
|
+
} finally {
|
|
335
|
+
await release(sagaId, owner)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const compensateOwned = async <I, C extends Record<string, unknown>>(
|
|
340
|
+
definition: SagaDefinition<I, C>,
|
|
341
|
+
sagaId: string,
|
|
342
|
+
runOptions: { readonly signal?: AbortSignal },
|
|
343
|
+
owner: string,
|
|
282
344
|
): Promise<SagaRecord> => {
|
|
283
345
|
assertSagaDefinition(definition)
|
|
284
346
|
assertToken(sagaId, "saga id", 128)
|
|
@@ -287,6 +349,7 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
287
349
|
let record = await load(sagaId)
|
|
288
350
|
if (record.definition !== definition.name) throw new TypeError("saga definition mismatch")
|
|
289
351
|
if (record.state === "completed" || record.state === "compensated") return record
|
|
352
|
+
record = await claim(record, owner)
|
|
290
353
|
if (
|
|
291
354
|
record.steps.some(
|
|
292
355
|
(step) =>
|
|
@@ -402,6 +465,20 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
402
465
|
definition: SagaDefinition<I, C>,
|
|
403
466
|
sagaId: string,
|
|
404
467
|
runOptions: { readonly signal?: AbortSignal } = {},
|
|
468
|
+
): Promise<SagaRecord> => {
|
|
469
|
+
const owner = crypto.randomUUID()
|
|
470
|
+
try {
|
|
471
|
+
return await resumeOwned(definition, sagaId, runOptions, owner)
|
|
472
|
+
} finally {
|
|
473
|
+
await release(sagaId, owner)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const resumeOwned = async <I, C extends Record<string, unknown>>(
|
|
478
|
+
definition: SagaDefinition<I, C>,
|
|
479
|
+
sagaId: string,
|
|
480
|
+
runOptions: { readonly signal?: AbortSignal },
|
|
481
|
+
owner: string,
|
|
405
482
|
): Promise<SagaRecord> => {
|
|
406
483
|
assertSagaDefinition(definition)
|
|
407
484
|
assertToken(sagaId, "saga id", 128)
|
|
@@ -410,6 +487,7 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
410
487
|
let record = await load(sagaId)
|
|
411
488
|
if (record.definition !== definition.name) throw new TypeError("saga definition mismatch")
|
|
412
489
|
if (record.state !== "running") return record
|
|
490
|
+
record = await claim(record, owner)
|
|
413
491
|
const context: SagaRunContext<C> = {
|
|
414
492
|
async step(name, compensationArgs, execute) {
|
|
415
493
|
throwIfAborted(signal)
|
|
@@ -418,15 +496,15 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
418
496
|
throw new TypeError("saga step execute must be a function")
|
|
419
497
|
if (typeof definition.compensators[name] !== "function")
|
|
420
498
|
throw new TypeError(`saga step ${name} has no compensator`)
|
|
421
|
-
record = await
|
|
499
|
+
record = await loadOwned(sagaId, owner)
|
|
500
|
+
if (record.state !== "running") throw new SagaConcurrencyError(sagaId)
|
|
422
501
|
const existingIndex = record.steps.findIndex((step) => step.name === name)
|
|
423
502
|
if (existingIndex >= 0) {
|
|
424
503
|
const existing = record.steps[existingIndex] as SagaStepRecord
|
|
425
504
|
if (existing.state === "committed")
|
|
426
505
|
return cloneValue(existing.result) as Awaited<ReturnType<typeof execute>>
|
|
427
506
|
if (existing.state === "executing" || existing.state === "ambiguous") {
|
|
428
|
-
|
|
429
|
-
record = await save(record, { ...record, state: "manual-review", updatedAt: now() })
|
|
507
|
+
record = await save(record, { ...record, state: "manual-review", updatedAt: now() })
|
|
430
508
|
throw new SagaAmbiguousStepError(sagaId, name, existing.effectId)
|
|
431
509
|
}
|
|
432
510
|
throw new Error(`saga ${sagaId}: step ${name} cannot resume from ${existing.state}`)
|
|
@@ -463,8 +541,9 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
463
541
|
})
|
|
464
542
|
},
|
|
465
543
|
async committed(_owned, result) {
|
|
466
|
-
record = await
|
|
544
|
+
record = await loadOwned(sagaId, owner)
|
|
467
545
|
const index = record.steps.findIndex((candidate) => candidate.effectId === effectId)
|
|
546
|
+
if (index < 0) throw new SagaConcurrencyError(sagaId)
|
|
468
547
|
const committed = {
|
|
469
548
|
...(record.steps[index] as SagaStepRecord),
|
|
470
549
|
state: "committed" as const,
|
|
@@ -473,8 +552,10 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
473
552
|
record = await save(record, withStep(record, index, committed))
|
|
474
553
|
},
|
|
475
554
|
async failed(_owned, failure) {
|
|
476
|
-
record = await
|
|
555
|
+
record = await loadOwned(sagaId, owner)
|
|
477
556
|
const index = record.steps.findIndex((candidate) => candidate.effectId === effectId)
|
|
557
|
+
// The step was never recorded: its intent write lost a race, so it never ran.
|
|
558
|
+
if (index < 0) throw new SagaConcurrencyError(sagaId)
|
|
478
559
|
const known = failure.error instanceof SagaStepNotCommittedError || !failure.began
|
|
479
560
|
const failed = {
|
|
480
561
|
...(record.steps[index] as SagaStepRecord),
|
|
@@ -500,14 +581,17 @@ export function createSagaEngine(options: SagaEngineOptions): SagaEngine {
|
|
|
500
581
|
try {
|
|
501
582
|
throwIfAborted(signal)
|
|
502
583
|
await definition.run(context, cloneValue(record.input) as I)
|
|
503
|
-
record = await
|
|
584
|
+
record = await loadOwned(sagaId, owner)
|
|
504
585
|
if (record.state === "running")
|
|
505
586
|
record = await save(record, { ...record, state: "completed", updatedAt: now() })
|
|
506
587
|
return record
|
|
507
588
|
} catch (error) {
|
|
589
|
+
// Another call holds the saga, or won a write this one lost: what ran is that call's to
|
|
590
|
+
// finish or roll back, and compensating here would undo it.
|
|
591
|
+
if (error instanceof SagaConcurrencyError) throw error
|
|
508
592
|
record = await load(sagaId)
|
|
509
|
-
if (record.state === "manual-review") throw error
|
|
510
|
-
await
|
|
593
|
+
if (record.state === "manual-review" || record.owner !== owner) throw error
|
|
594
|
+
await compensateOwned(definition, sagaId, runOptions, owner)
|
|
511
595
|
throw error
|
|
512
596
|
}
|
|
513
597
|
}
|
|
@@ -676,7 +760,7 @@ async function scanSagaRecords(
|
|
|
676
760
|
): Promise<ReconciliationScanPage<SagaRecord>> {
|
|
677
761
|
if (store.scan !== undefined) return await store.scan(input)
|
|
678
762
|
if (store.list === undefined) throw new TypeError("saga store does not support reconciliation")
|
|
679
|
-
return memoryScan(await store.list(), input)
|
|
763
|
+
return memoryScan(await store.list(), (record) => record.sagaId, input)
|
|
680
764
|
}
|
|
681
765
|
|
|
682
766
|
export async function reconcileSagasPage(
|
package/src/edge-kit.ts
CHANGED
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
* exposes the one lane the fused body runner already calls, so a compact server reuses it verbatim:
|
|
9
9
|
*
|
|
10
10
|
* - {@link readBodyFramed} read + cap + proto-guard + content-type framing (JSON / urlencoded / 415)
|
|
11
|
-
* - {@link
|
|
11
|
+
* - {@link capBodyBytes} the same byte cap on a handler's own reads of the raw request
|
|
12
|
+
* - {@link renderResult} render a {@link ResponseResult} (e.g. a 413 / 415 rejection) to a `Response`
|
|
13
|
+
* - {@link toResponse} render any handler result, a handler's own `Response` included
|
|
12
14
|
* - {@link plainError} the structured rejection envelope the lane returns
|
|
13
15
|
* - {@link queryObjectOf} / {@link searchOf} query parse without a full WHATWG URL construction
|
|
14
16
|
* - {@link answerNotFound} run a not-found handler under the same rules (404 only, no 2xx, fail closed)
|
|
17
|
+
* - {@link emitRequestErrorLog} log an unhandled request error under the app's `errorLogDetail`
|
|
15
18
|
*
|
|
16
19
|
* Everything here carries only structure - bytes, guards, and envelopes - never routing, lifecycle,
|
|
17
20
|
* or app state. It is the interface, not the framework.
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
|
-
export {
|
|
23
|
+
export { type ErrorLogDetail, emitRequestErrorLog } from "./server/bare-error-lane.ts"
|
|
24
|
+
export { capBodyBytes } from "./server/body.ts"
|
|
25
|
+
export { type FormOrigins, readBodyFramed } from "./server/body-lane.ts"
|
|
21
26
|
export { plainError } from "./server/http.ts"
|
|
22
27
|
export {
|
|
23
28
|
answerNotFound,
|
|
@@ -27,7 +32,7 @@ export {
|
|
|
27
32
|
} from "./server/not-found-answer.ts"
|
|
28
33
|
export type { ProtoPoisoning } from "./server/proto-guard.ts"
|
|
29
34
|
export { type QueryValue, queryObjectOf, searchOf } from "./server/query.ts"
|
|
30
|
-
export { toResponse } from "./server/respond.ts"
|
|
35
|
+
export { renderResult, toResponse } from "./server/respond.ts"
|
|
31
36
|
export type { ResponseResult } from "./server/runtime-core.ts"
|
|
32
37
|
export { EMPTY_RESPONSE_CONTROLS } from "./server/runtime-core.ts"
|
|
33
38
|
export type { CtxSet, MaybePromise, RequestSource } from "./server/server.ts"
|
package/src/effect-scope.ts
CHANGED
|
@@ -234,10 +234,20 @@ export function createEffectScope(
|
|
|
234
234
|
})
|
|
235
235
|
return result
|
|
236
236
|
} catch (error) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
// A misconfigured `errorCode` or `failurePhase` is thrown only once the effect is settled
|
|
238
|
+
// with the defaults: thrown here, it left the durable record unfinished.
|
|
239
|
+
let misconfigured: { readonly error: unknown } | undefined
|
|
240
|
+
let errorCode = began ? "execution_failed" : "admission_failed"
|
|
241
|
+
let failurePhase: "failed" | "ambiguous" = began ? "ambiguous" : "failed"
|
|
242
|
+
try {
|
|
243
|
+
errorCode = codeFor(input.errorCode, error, began)
|
|
244
|
+
const phase = input.failurePhase?.(error, began) ?? failurePhase
|
|
245
|
+
if (phase !== "failed" && phase !== "ambiguous") {
|
|
246
|
+
throw new TypeError("owned effect failurePhase returned an invalid phase")
|
|
247
|
+
}
|
|
248
|
+
failurePhase = phase
|
|
249
|
+
} catch (invalid) {
|
|
250
|
+
misconfigured = { error: invalid }
|
|
241
251
|
}
|
|
242
252
|
try {
|
|
243
253
|
await input.transitions?.failed?.(context, { began, errorCode, error })
|
|
@@ -253,7 +263,7 @@ export function createEffectScope(
|
|
|
253
263
|
errorCode,
|
|
254
264
|
...(input.trace === undefined ? {} : { trace: input.trace }),
|
|
255
265
|
})
|
|
256
|
-
throw error
|
|
266
|
+
throw misconfigured === undefined ? error : misconfigured.error
|
|
257
267
|
}
|
|
258
268
|
},
|
|
259
269
|
evidence: () => evidenceScope.evidence(),
|
package/src/errors.ts
CHANGED
package/src/evidence.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface ProjectEvidenceSchemaPart {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface ProjectEvidenceSchema {
|
|
37
|
+
/** Set only when the route chose it; absent means the default for its auth stages. */
|
|
38
|
+
readonly validationOrder?: "validate-before-auth" | "auth-before-validation"
|
|
37
39
|
readonly bodyLimit?: number | "unlimited"
|
|
38
40
|
readonly bodyLimitReason?: string
|
|
39
41
|
readonly headers?: ProjectEvidenceSchemaPart
|
|
@@ -160,6 +162,7 @@ function schemaOf(route: ReflectedRoute): ProjectEvidenceSchema | undefined {
|
|
|
160
162
|
if (part !== undefined) errors[status] = part
|
|
161
163
|
}
|
|
162
164
|
const schema: ProjectEvidenceSchema = {
|
|
165
|
+
...(source.validationOrder !== undefined ? { validationOrder: source.validationOrder } : {}),
|
|
163
166
|
...(source.bodyLimit !== undefined ? { bodyLimit: source.bodyLimit } : {}),
|
|
164
167
|
...(source.bodyLimitReason !== undefined ? { bodyLimitReason: source.bodyLimitReason } : {}),
|
|
165
168
|
...(headers !== undefined ? { headers } : {}),
|
|
@@ -328,9 +331,14 @@ function remapEvidenceSnapshot(
|
|
|
328
331
|
const key = route(item.method, item.path)
|
|
329
332
|
return Object.freeze({ ...item, ...key })
|
|
330
333
|
})
|
|
331
|
-
const mounts = evidence.mounts?.map((item) =>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
+
const mounts = evidence.mounts?.map((item) => {
|
|
335
|
+
const path = composedPath(pathPrefix, item.path)
|
|
336
|
+
if (item.routes === undefined) return Object.freeze({ ...item, path })
|
|
337
|
+
const routes = item.routes.map((declared) =>
|
|
338
|
+
Object.freeze({ ...declared, path: composedPath(pathPrefix, declared.path) }),
|
|
339
|
+
)
|
|
340
|
+
return Object.freeze({ ...item, path, routes: Object.freeze(routes) })
|
|
341
|
+
})
|
|
334
342
|
const assurance =
|
|
335
343
|
evidence.assurance === undefined
|
|
336
344
|
? undefined
|
|
@@ -499,6 +507,7 @@ function schemaFromEvidence(schema: ProjectEvidenceSchema | undefined): Reflecte
|
|
|
499
507
|
const response = schemaPartToReflection(schema.response)
|
|
500
508
|
const sse = schemaPartToReflection(schema.sse)
|
|
501
509
|
return {
|
|
510
|
+
...(schema.validationOrder !== undefined ? { validationOrder: schema.validationOrder } : {}),
|
|
502
511
|
...(schema.bodyLimit !== undefined ? { bodyLimit: schema.bodyLimit } : {}),
|
|
503
512
|
...(schema.bodyLimitReason !== undefined ? { bodyLimitReason: schema.bodyLimitReason } : {}),
|
|
504
513
|
...(headers !== undefined ? { headers } : {}),
|
package/src/index.ts
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
|
|
17
17
|
* `check:publish` asserts it equals `@nifrajs/core`'s package version.
|
|
18
18
|
*/
|
|
19
|
-
export const VERSION = "
|
|
19
|
+
export const VERSION = "5.1.0" as const
|
|
20
20
|
|
|
21
21
|
export type Version = typeof VERSION
|
|
22
22
|
|
|
23
23
|
export { FrameworkError, RouteConfigError, type RouteConfigErrorCode } from "./errors.ts"
|
|
24
24
|
export { FRAMEWORK_NAME, type FrameworkName } from "./internal/brand.ts"
|
|
25
|
-
export { isSameOriginRequest } from "./internal/same-origin.ts"
|
|
25
|
+
export { isSameOriginForm, isSameOriginRequest } from "./internal/same-origin.ts"
|
|
26
26
|
export {
|
|
27
27
|
ROUTE_PATTERN_OVERLAP_MAX_STATES,
|
|
28
28
|
RoutePatternOverlapLimitError,
|
|
@@ -54,6 +54,8 @@ export {
|
|
|
54
54
|
type AuthHeaders,
|
|
55
55
|
type AuthMaybePromise,
|
|
56
56
|
authenticated,
|
|
57
|
+
isAuthenticatedRequest,
|
|
58
|
+
markAuthenticatedRequest,
|
|
57
59
|
rejected,
|
|
58
60
|
} from "./server/auth.ts"
|
|
59
61
|
export type {
|
|
@@ -116,6 +118,7 @@ export type {
|
|
|
116
118
|
RouteInfoFor,
|
|
117
119
|
} from "./server/registry.ts"
|
|
118
120
|
export { replacedRequestOf } from "./server/request-lineage.ts"
|
|
121
|
+
export { writableResponse } from "./server/respond.ts"
|
|
119
122
|
export {
|
|
120
123
|
type PlainRender,
|
|
121
124
|
type ResponseResult,
|
|
@@ -127,6 +130,7 @@ export {
|
|
|
127
130
|
type AdmissionDecision,
|
|
128
131
|
type AnyServer,
|
|
129
132
|
type ContextPlugin,
|
|
133
|
+
type DeclaredMountRoute,
|
|
130
134
|
type DefinePluginResult,
|
|
131
135
|
defineContextPlugin,
|
|
132
136
|
defineIdentityPlugin,
|
|
@@ -150,6 +154,7 @@ export {
|
|
|
150
154
|
type NodeResponseHook,
|
|
151
155
|
type NodeServeOutcome,
|
|
152
156
|
type OnRequestResult,
|
|
157
|
+
type OpaqueMountDeclaration,
|
|
153
158
|
type PluginTypeCollapsed,
|
|
154
159
|
type PromptArgument,
|
|
155
160
|
type PromptMessage,
|
|
@@ -63,12 +63,22 @@ export function reconciliationLimit(value = DEFAULT_RECONCILIATION_LIMIT): numbe
|
|
|
63
63
|
return value
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
/**
|
|
67
|
+
* One page of `matches` in id order, the cursor being the last id returned. Keyed rather than an
|
|
68
|
+
* offset into the matching set: a caller reconciles each page before asking for the next, so the
|
|
69
|
+
* set shrinks under it, and an offset then lands past records that were never returned.
|
|
70
|
+
*/
|
|
71
|
+
function keysetPage<Record>(
|
|
72
|
+
matches: [id: string, record: Record][],
|
|
73
|
+
limit: number,
|
|
74
|
+
): ReconciliationScanPage<Record> {
|
|
75
|
+
// Code-unit order, the order the `id > cursor` comparisons use.
|
|
76
|
+
matches.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
77
|
+
const page = matches.slice(0, limit)
|
|
78
|
+
const records = Object.freeze(page.map(([, record]) => cloneValue(record)))
|
|
79
|
+
const last = page.at(-1)
|
|
80
|
+
if (matches.length <= limit || last === undefined) return Object.freeze({ records })
|
|
81
|
+
return Object.freeze({ records, cursor: last[0] })
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
export function memoryScan<
|
|
@@ -76,28 +86,18 @@ export function memoryScan<
|
|
|
76
86
|
State extends string,
|
|
77
87
|
>(
|
|
78
88
|
source: Iterable<Record>,
|
|
89
|
+
idOf: (record: Record) => string,
|
|
79
90
|
input: ReconciliationScanOptions<State>,
|
|
80
91
|
): ReconciliationScanPage<Record> {
|
|
81
92
|
const states = new Set(input.states)
|
|
82
|
-
const
|
|
83
|
-
const end = offset + input.limit
|
|
84
|
-
const records: Record[] = []
|
|
85
|
-
let seen = 0
|
|
86
|
-
let more = false
|
|
93
|
+
const matches: [string, Record][] = []
|
|
87
94
|
for (const record of source) {
|
|
88
95
|
if (!states.has(record.state)) continue
|
|
89
96
|
if (input.updatedBefore !== undefined && record.updatedAt > input.updatedBefore) continue
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
break
|
|
93
|
-
}
|
|
94
|
-
if (seen >= offset) records.push(cloneValue(record))
|
|
95
|
-
seen++
|
|
97
|
+
const id = idOf(record)
|
|
98
|
+
if (input.cursor === undefined || id > input.cursor) matches.push([id, record])
|
|
96
99
|
}
|
|
97
|
-
return
|
|
98
|
-
records: Object.freeze(records),
|
|
99
|
-
...(more ? { cursor: String(offset + records.length) } : {}),
|
|
100
|
-
})
|
|
100
|
+
return keysetPage(matches, input.limit)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export function bucketAdd<State extends string>(
|
|
@@ -129,29 +129,17 @@ export function indexedScan<Record extends { readonly updatedAt: number }, State
|
|
|
129
129
|
resolve: (id: string) => Record | undefined,
|
|
130
130
|
input: ReconciliationScanOptions<State>,
|
|
131
131
|
): ReconciliationScanPage<Record> {
|
|
132
|
-
const
|
|
133
|
-
const end = offset + input.limit
|
|
134
|
-
const records: Record[] = []
|
|
135
|
-
let seen = 0
|
|
136
|
-
let more = false
|
|
132
|
+
const matches: [string, Record][] = []
|
|
137
133
|
for (const state of new Set(input.states)) {
|
|
138
|
-
if (more) break
|
|
139
134
|
const bucket = byState.get(state)
|
|
140
135
|
if (bucket === undefined) continue
|
|
141
136
|
for (const id of bucket) {
|
|
137
|
+
if (input.cursor !== undefined && id <= input.cursor) continue
|
|
142
138
|
const record = resolve(id)
|
|
143
139
|
if (record === undefined) continue
|
|
144
140
|
if (input.updatedBefore !== undefined && record.updatedAt > input.updatedBefore) continue
|
|
145
|
-
|
|
146
|
-
more = true
|
|
147
|
-
break
|
|
148
|
-
}
|
|
149
|
-
if (seen >= offset) records.push(cloneValue(record))
|
|
150
|
-
seen++
|
|
141
|
+
matches.push([id, record])
|
|
151
142
|
}
|
|
152
143
|
}
|
|
153
|
-
return
|
|
154
|
-
records: Object.freeze(records),
|
|
155
|
-
...(more ? { cursor: String(offset + records.length) } : {}),
|
|
156
|
-
})
|
|
144
|
+
return keysetPage(matches, input.limit)
|
|
157
145
|
}
|
|
@@ -138,6 +138,9 @@ export interface SagaRecord {
|
|
|
138
138
|
readonly createdAt: number
|
|
139
139
|
readonly updatedAt: number
|
|
140
140
|
readonly version: number
|
|
141
|
+
/** The `resume`/`compensate` call that owns the saga while `leaseUntil` is in the future. */
|
|
142
|
+
readonly owner?: string
|
|
143
|
+
readonly leaseUntil?: number
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
export interface SagaStore {
|