@nifrajs/core 3.1.0 → 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 +21 -0
- 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 +2 -2
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +2 -2
- package/dist/edge-kit.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 +6 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +2 -17
- 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/server/bare-error-lane.d.ts +1 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +3 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- 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/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/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/registry.d.ts +40 -12
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +4 -28
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +10 -185
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +39 -2
- package/dist/server/respond.js.map +1 -1
- package/dist/server/runtime-core.d.ts +18 -2
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +0 -23
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +3 -0
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +113 -84
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +506 -639
- 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/ws-bun.d.ts +11 -3
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +42 -12
- 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 +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +3 -1
- package/dist/single-copy.js.map +1 -1
- package/package.json +6 -3
- 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 +2 -2
- 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 +34 -84
- package/src/internal/route-lanes.ts +221 -0
- package/src/internal/route-program.ts +404 -0
- package/src/server/bare-error-lane.ts +3 -2
- package/src/server/body-lane.ts +203 -0
- package/src/server/contract.ts +70 -20
- package/src/server/cookies.ts +16 -2
- package/src/server/plugin.ts +11 -5
- package/src/server/registry.ts +71 -16
- package/src/server/request-context.ts +7 -262
- package/src/server/respond.ts +45 -2
- package/src/server/runtime-core.ts +28 -2
- package/src/server/server-types.ts +3 -0
- package/src/server/server.ts +1152 -1260
- package/src/server/validation.ts +19 -0
- package/src/server/ws-bun.ts +44 -11
- package/src/server/ws-hook.ts +3 -2
- package/src/server.ts +7 -2
- package/src/single-copy.ts +3 -1
package/src/edge-kit.ts
CHANGED
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
* or app state. It is the interface, not the framework.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
export { readBodyFramed } from "./server/body-lane.ts"
|
|
19
20
|
export { plainError } from "./server/http.ts"
|
|
20
21
|
export type { ProtoPoisoning } from "./server/proto-guard.ts"
|
|
21
22
|
export { type QueryValue, queryObjectOf, searchOf } from "./server/query.ts"
|
|
22
|
-
export { readBodyFramed } from "./server/request-context.ts"
|
|
23
23
|
export { toResponse } from "./server/respond.ts"
|
|
24
24
|
export type { ResponseResult } from "./server/runtime-core.ts"
|
|
25
25
|
export { EMPTY_RESPONSE_CONTROLS } from "./server/runtime-core.ts"
|
|
26
26
|
export type { CtxSet, MaybePromise, RequestSource } from "./server/server.ts"
|
|
27
|
-
export { plainValidationError } from "./server/
|
|
27
|
+
export { plainValidationError } from "./server/validation.ts"
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Nifra 2.0 keeps the package root intentionally lean. Optional systems live at documented
|
|
5
5
|
* subpaths so importing the root cannot activate unrelated runtimes.
|
|
6
|
+
*
|
|
7
|
+
* Every named export below is re-exported in a SINGLE hop from the module that defines it -
|
|
8
|
+
* deliberately not `export * from "./server.ts"`. A root binding that travels through a chained
|
|
9
|
+
* barrel can be dropped by a bundler that rewrites module loads (the single-copy plugin does
|
|
10
|
+
* exactly that), which surfaces as a runtime `ReferenceError` only in bundled output. The list
|
|
11
|
+
* must stay identical to `./server.ts`; `server-entry.test.ts` enforces the parity.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
/**
|
|
@@ -10,8 +16,129 @@
|
|
|
10
16
|
* read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
|
|
11
17
|
* `check:publish` asserts it equals `@nifrajs/core`'s package version.
|
|
12
18
|
*/
|
|
13
|
-
export const VERSION = "3.
|
|
19
|
+
export const VERSION = "3.2.0" as const
|
|
14
20
|
|
|
15
21
|
export type Version = typeof VERSION
|
|
16
22
|
|
|
17
|
-
export
|
|
23
|
+
export { FrameworkError, RouteConfigError, type RouteConfigErrorCode } from "./errors.ts"
|
|
24
|
+
export { FRAMEWORK_NAME, type FrameworkName } from "./internal/brand.ts"
|
|
25
|
+
export { isSameOriginRequest } from "./internal/same-origin.ts"
|
|
26
|
+
export { METHODS, type Method, Router, type RouterMatch } from "./router/router.ts"
|
|
27
|
+
export type {
|
|
28
|
+
InferInput,
|
|
29
|
+
InferOutput,
|
|
30
|
+
StandardIssue,
|
|
31
|
+
StandardResult,
|
|
32
|
+
StandardSchemaV1,
|
|
33
|
+
StandardTypes,
|
|
34
|
+
ValidationOutcome,
|
|
35
|
+
} from "./schema/standard.ts"
|
|
36
|
+
export type {
|
|
37
|
+
Context,
|
|
38
|
+
Params,
|
|
39
|
+
Platform,
|
|
40
|
+
Prettify,
|
|
41
|
+
ResponseControls,
|
|
42
|
+
RouteSchema,
|
|
43
|
+
} from "./server/context.ts"
|
|
44
|
+
export {
|
|
45
|
+
type CookieOptions,
|
|
46
|
+
type CookieSecret,
|
|
47
|
+
cookieNamePrefix,
|
|
48
|
+
parseCookies,
|
|
49
|
+
serializeCookie,
|
|
50
|
+
signValue,
|
|
51
|
+
unsignValue,
|
|
52
|
+
} from "./server/cookies.ts"
|
|
53
|
+
export {
|
|
54
|
+
type DurableObjectNamespaceLike,
|
|
55
|
+
type ExecutionContext,
|
|
56
|
+
type ScheduledController,
|
|
57
|
+
type ScheduledHandler,
|
|
58
|
+
toFetchHandler,
|
|
59
|
+
} from "./server/edge.ts"
|
|
60
|
+
export { isSameOriginPath, pathnameOf, type UrlParts, urlPartsOf } from "./server/http.ts"
|
|
61
|
+
export {
|
|
62
|
+
commonSecretPatterns,
|
|
63
|
+
jsonLogger,
|
|
64
|
+
type LogFields,
|
|
65
|
+
type Logger,
|
|
66
|
+
type RedactOptions,
|
|
67
|
+
redactLogFields,
|
|
68
|
+
silentLogger,
|
|
69
|
+
} from "./server/logger.ts"
|
|
70
|
+
export {
|
|
71
|
+
type FetchApp,
|
|
72
|
+
type LambdaEvent,
|
|
73
|
+
type LambdaHandler,
|
|
74
|
+
type LambdaResponse,
|
|
75
|
+
type LambdaV1Event,
|
|
76
|
+
type LambdaV2Event,
|
|
77
|
+
type NetlifyEvent,
|
|
78
|
+
type NetlifyHandler,
|
|
79
|
+
type PlatformResponse,
|
|
80
|
+
toLambdaHandler,
|
|
81
|
+
toNetlifyHandler,
|
|
82
|
+
toVercelHandler,
|
|
83
|
+
type VercelHandler,
|
|
84
|
+
} from "./server/platform-adapters.ts"
|
|
85
|
+
export type { Registry, ResponseMapFor, RouteInfo, RouteInfoFor } from "./server/registry.ts"
|
|
86
|
+
export {
|
|
87
|
+
type PlainRender,
|
|
88
|
+
type ResponseResult,
|
|
89
|
+
type StatusResponse,
|
|
90
|
+
status,
|
|
91
|
+
} from "./server/runtime-core.ts"
|
|
92
|
+
export {
|
|
93
|
+
type AdmissionController,
|
|
94
|
+
type AdmissionDecision,
|
|
95
|
+
type AnyServer,
|
|
96
|
+
type ContextPlugin,
|
|
97
|
+
type DefinePluginResult,
|
|
98
|
+
defineContextPlugin,
|
|
99
|
+
defineIdentityPlugin,
|
|
100
|
+
definePlugin,
|
|
101
|
+
defineRouterPlugin,
|
|
102
|
+
type Handler,
|
|
103
|
+
type IdentityPlugin,
|
|
104
|
+
type McpPromptDescriptor,
|
|
105
|
+
type McpResourceDescriptor,
|
|
106
|
+
type Middleware,
|
|
107
|
+
type NifraFeatureVersion,
|
|
108
|
+
type NifraPlugin,
|
|
109
|
+
type NodeRequestContext,
|
|
110
|
+
type NodeRequestHook,
|
|
111
|
+
type NodeResponseContext,
|
|
112
|
+
type NodeResponseHook,
|
|
113
|
+
type NodeServeOutcome,
|
|
114
|
+
type OnRequestResult,
|
|
115
|
+
type PluginTypeCollapsed,
|
|
116
|
+
type PromptArgument,
|
|
117
|
+
type PromptMessage,
|
|
118
|
+
type ResponseBodyHook,
|
|
119
|
+
type ResponseBodyReplacement,
|
|
120
|
+
type ResponseFinalization,
|
|
121
|
+
type ResponseHeadersHook,
|
|
122
|
+
type ResponseHeadersView,
|
|
123
|
+
type RouteDescriptor,
|
|
124
|
+
type RunningServer,
|
|
125
|
+
Server,
|
|
126
|
+
type ServerOptions,
|
|
127
|
+
server,
|
|
128
|
+
type ToolAnnotations,
|
|
129
|
+
} from "./server/server.ts"
|
|
130
|
+
export type {
|
|
131
|
+
SSEContext,
|
|
132
|
+
SSEInit,
|
|
133
|
+
SSEMessage,
|
|
134
|
+
SSEStream,
|
|
135
|
+
TypedSSEStream,
|
|
136
|
+
} from "./server/sse.ts"
|
|
137
|
+
export type {
|
|
138
|
+
NifraWebSocket,
|
|
139
|
+
StandardWebSocket,
|
|
140
|
+
WebSocketContext,
|
|
141
|
+
WebSocketData,
|
|
142
|
+
WebSocketHandler,
|
|
143
|
+
WebSocketUpgradeOutcome,
|
|
144
|
+
} from "./server/websocket.ts"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApprovalConsumeResult, ApprovalRecord, ApprovalStore } from "
|
|
1
|
+
import type { ApprovalConsumeResult, ApprovalRecord, ApprovalStore } from "./durable-types.ts"
|
|
2
2
|
import { safeEqual } from "./safe-equal.ts"
|
|
3
3
|
|
|
4
4
|
export type DurableApprovalDecisionInput = Parameters<ApprovalStore["decide"]>[0]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SagaRecord, SagaState, SagaStepRecord, SagaStore } from "
|
|
1
|
+
import type { SagaRecord, SagaState, SagaStepRecord, SagaStore } from "./durable-types.ts"
|
|
2
2
|
|
|
3
3
|
export type DurableSagaTransitionInput = Parameters<SagaStore["compareAndSet"]>[0]
|
|
4
4
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ReconciliationScanOptions, ReconciliationScanPage } from "./durable-types.ts"
|
|
2
|
+
|
|
3
|
+
export const TOKEN = /^[!-~]+$/
|
|
4
|
+
export const ERROR_CODE = /^[a-z][a-z0-9_.-]{0,63}$/
|
|
5
|
+
export const encoder = new TextEncoder()
|
|
6
|
+
|
|
7
|
+
export function assertToken(value: string, label: string, maxLength = 255): void {
|
|
8
|
+
if (
|
|
9
|
+
typeof value !== "string" ||
|
|
10
|
+
value.length === 0 ||
|
|
11
|
+
value.length > maxLength ||
|
|
12
|
+
!TOKEN.test(value)
|
|
13
|
+
) {
|
|
14
|
+
throw new TypeError(`${label} must be a bounded printable token`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function assertPositiveMs(value: number, label: string): void {
|
|
19
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
20
|
+
throw new RangeError(`${label} must be a positive safe integer`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function assertTimestamp(value: number, label: string): void {
|
|
24
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
25
|
+
throw new RangeError(`${label} must be a non-negative safe integer timestamp`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readClock(clock: () => number, label: string): number {
|
|
29
|
+
const value = clock()
|
|
30
|
+
assertTimestamp(value, label)
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function addDuration(timestamp: number, duration: number, label: string): number {
|
|
35
|
+
const result = timestamp + duration
|
|
36
|
+
assertTimestamp(result, label)
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function bytesToBase64Url(bytes: Uint8Array): string {
|
|
41
|
+
let binary = ""
|
|
42
|
+
for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
43
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function sha256(value: string): Promise<string> {
|
|
47
|
+
return bytesToBase64Url(
|
|
48
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value))),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function cloneValue<T>(value: T): T {
|
|
53
|
+
return structuredClone(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_RECONCILIATION_LIMIT = 100
|
|
57
|
+
export const MAX_RECONCILIATION_LIMIT = 1_000
|
|
58
|
+
|
|
59
|
+
export function reconciliationLimit(value = DEFAULT_RECONCILIATION_LIMIT): number {
|
|
60
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > MAX_RECONCILIATION_LIMIT) {
|
|
61
|
+
throw new RangeError(`reconciliation limit must be between 1 and ${MAX_RECONCILIATION_LIMIT}`)
|
|
62
|
+
}
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function cursorOffset(cursor?: string): number {
|
|
67
|
+
if (cursor === undefined) return 0
|
|
68
|
+
if (!/^(?:0|[1-9][0-9]*)$/u.test(cursor)) throw new TypeError("invalid reconciliation cursor")
|
|
69
|
+
const offset = Number(cursor)
|
|
70
|
+
if (!Number.isSafeInteger(offset)) throw new TypeError("invalid reconciliation cursor")
|
|
71
|
+
return offset
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function memoryScan<
|
|
75
|
+
Record extends { readonly state: State; readonly updatedAt: number },
|
|
76
|
+
State extends string,
|
|
77
|
+
>(
|
|
78
|
+
source: Iterable<Record>,
|
|
79
|
+
input: ReconciliationScanOptions<State>,
|
|
80
|
+
): ReconciliationScanPage<Record> {
|
|
81
|
+
const states = new Set(input.states)
|
|
82
|
+
const offset = cursorOffset(input.cursor)
|
|
83
|
+
const end = offset + input.limit
|
|
84
|
+
const records: Record[] = []
|
|
85
|
+
let seen = 0
|
|
86
|
+
let more = false
|
|
87
|
+
for (const record of source) {
|
|
88
|
+
if (!states.has(record.state)) continue
|
|
89
|
+
if (input.updatedBefore !== undefined && record.updatedAt > input.updatedBefore) continue
|
|
90
|
+
if (seen >= end) {
|
|
91
|
+
more = true
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
if (seen >= offset) records.push(cloneValue(record))
|
|
95
|
+
seen++
|
|
96
|
+
}
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
records: Object.freeze(records),
|
|
99
|
+
...(more ? { cursor: String(offset + records.length) } : {}),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function bucketAdd<State extends string>(
|
|
104
|
+
byState: Map<State, Set<string>>,
|
|
105
|
+
state: State,
|
|
106
|
+
id: string,
|
|
107
|
+
): void {
|
|
108
|
+
let bucket = byState.get(state)
|
|
109
|
+
if (bucket === undefined) {
|
|
110
|
+
bucket = new Set()
|
|
111
|
+
byState.set(state, bucket)
|
|
112
|
+
}
|
|
113
|
+
bucket.add(id)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function bucketMove<State extends string>(
|
|
117
|
+
byState: Map<State, Set<string>>,
|
|
118
|
+
from: State,
|
|
119
|
+
to: State,
|
|
120
|
+
id: string,
|
|
121
|
+
): void {
|
|
122
|
+
if (from === to) return
|
|
123
|
+
byState.get(from)?.delete(id)
|
|
124
|
+
bucketAdd(byState, to, id)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function indexedScan<Record extends { readonly updatedAt: number }, State extends string>(
|
|
128
|
+
byState: Map<State, Set<string>>,
|
|
129
|
+
resolve: (id: string) => Record | undefined,
|
|
130
|
+
input: ReconciliationScanOptions<State>,
|
|
131
|
+
): ReconciliationScanPage<Record> {
|
|
132
|
+
const offset = cursorOffset(input.cursor)
|
|
133
|
+
const end = offset + input.limit
|
|
134
|
+
const records: Record[] = []
|
|
135
|
+
let seen = 0
|
|
136
|
+
let more = false
|
|
137
|
+
for (const state of new Set(input.states)) {
|
|
138
|
+
if (more) break
|
|
139
|
+
const bucket = byState.get(state)
|
|
140
|
+
if (bucket === undefined) continue
|
|
141
|
+
for (const id of bucket) {
|
|
142
|
+
const record = resolve(id)
|
|
143
|
+
if (record === undefined) continue
|
|
144
|
+
if (input.updatedBefore !== undefined && record.updatedAt > input.updatedBefore) continue
|
|
145
|
+
if (seen >= end) {
|
|
146
|
+
more = true
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
if (seen >= offset) records.push(cloneValue(record))
|
|
150
|
+
seen++
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
records: Object.freeze(records),
|
|
155
|
+
...(more ? { cursor: String(offset + records.length) } : {}),
|
|
156
|
+
})
|
|
157
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/** Type-only records and transition contracts shared by durable execution domains. */
|
|
2
|
+
export type DurableEffectState = "admission" | "executing" | "committed" | "failed" | "unknown"
|
|
3
|
+
|
|
4
|
+
export interface ReconciliationScanOptions<State extends string> {
|
|
5
|
+
readonly states: readonly State[]
|
|
6
|
+
readonly updatedBefore?: number
|
|
7
|
+
readonly cursor?: string
|
|
8
|
+
readonly limit: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ReconciliationScanPage<Record> {
|
|
12
|
+
readonly records: readonly Record[]
|
|
13
|
+
readonly cursor?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ReconciliationPage<Finding> {
|
|
17
|
+
readonly findings: readonly Finding[]
|
|
18
|
+
readonly cursor?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DurableEffectRecord {
|
|
22
|
+
readonly effectId: string
|
|
23
|
+
readonly capability: string
|
|
24
|
+
readonly target?: string
|
|
25
|
+
readonly digest?: string
|
|
26
|
+
readonly tenantId?: string
|
|
27
|
+
readonly principalId?: string
|
|
28
|
+
readonly state: DurableEffectState
|
|
29
|
+
readonly createdAt: number
|
|
30
|
+
readonly updatedAt: number
|
|
31
|
+
readonly errorCode?: string
|
|
32
|
+
readonly version: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DurableEffectStore {
|
|
36
|
+
readonly durability?: "memory" | "durable"
|
|
37
|
+
create(record: DurableEffectRecord): boolean | Promise<boolean>
|
|
38
|
+
get(effectId: string): DurableEffectRecord | undefined | Promise<DurableEffectRecord | undefined>
|
|
39
|
+
/** Atomic compare-and-set; exactly one caller may advance the supplied `version` and `from` state. */
|
|
40
|
+
transition(input: {
|
|
41
|
+
readonly effectId: string
|
|
42
|
+
readonly version: number
|
|
43
|
+
readonly from: DurableEffectState
|
|
44
|
+
readonly to: DurableEffectState
|
|
45
|
+
readonly updatedAt: number
|
|
46
|
+
readonly errorCode?: string
|
|
47
|
+
}): boolean | Promise<boolean>
|
|
48
|
+
/** Bounded operational scan. Transition-only stores do not need to implement reconciliation. */
|
|
49
|
+
scan?(
|
|
50
|
+
input: ReconciliationScanOptions<DurableEffectState>,
|
|
51
|
+
):
|
|
52
|
+
| ReconciliationScanPage<DurableEffectRecord>
|
|
53
|
+
| Promise<ReconciliationScanPage<DurableEffectRecord>>
|
|
54
|
+
/** @deprecated Compatibility fallback; production reconciliation should implement `scan`. */
|
|
55
|
+
list?(): readonly DurableEffectRecord[] | Promise<readonly DurableEffectRecord[]>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ApprovalState = "pending" | "approved" | "denied" | "consumed" | "expired"
|
|
59
|
+
|
|
60
|
+
export interface ApprovalRecord {
|
|
61
|
+
readonly approvalId: string
|
|
62
|
+
readonly effectId: string
|
|
63
|
+
readonly capability: string
|
|
64
|
+
readonly target?: string
|
|
65
|
+
readonly digest?: string
|
|
66
|
+
readonly tenantId: string
|
|
67
|
+
readonly principalId: string
|
|
68
|
+
readonly tokenHash: string
|
|
69
|
+
readonly state: ApprovalState
|
|
70
|
+
readonly createdAt: number
|
|
71
|
+
readonly expiresAt: number
|
|
72
|
+
readonly updatedAt: number
|
|
73
|
+
readonly decidedBy?: string
|
|
74
|
+
readonly version: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type ApprovalConsumeResult =
|
|
78
|
+
| { readonly state: "consumed" }
|
|
79
|
+
| {
|
|
80
|
+
readonly state: "missing" | "pending" | "denied" | "expired" | "replay" | "binding" | "token"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ApprovalStore {
|
|
84
|
+
readonly durability?: "memory" | "durable"
|
|
85
|
+
create(record: ApprovalRecord): boolean | Promise<boolean>
|
|
86
|
+
get(approvalId: string): ApprovalRecord | undefined | Promise<ApprovalRecord | undefined>
|
|
87
|
+
decide(input: {
|
|
88
|
+
readonly approvalId: string
|
|
89
|
+
readonly tenantId: string
|
|
90
|
+
readonly decision: "approved" | "denied"
|
|
91
|
+
readonly decidedBy: string
|
|
92
|
+
readonly now: number
|
|
93
|
+
}): boolean | Promise<boolean>
|
|
94
|
+
/**
|
|
95
|
+
* Atomically validate binding/token/state and change `approved` to `consumed`. Two concurrent
|
|
96
|
+
* callers with one token must yield exactly one `consumed` and one `replay` result.
|
|
97
|
+
*/
|
|
98
|
+
consume(input: {
|
|
99
|
+
readonly approvalId: string
|
|
100
|
+
readonly tenantId: string
|
|
101
|
+
readonly principalId: string
|
|
102
|
+
readonly capability: string
|
|
103
|
+
readonly target?: string
|
|
104
|
+
readonly digest?: string
|
|
105
|
+
readonly tokenHash: string
|
|
106
|
+
readonly now: number
|
|
107
|
+
}): ApprovalConsumeResult | Promise<ApprovalConsumeResult>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type SagaState = "running" | "compensating" | "completed" | "compensated" | "manual-review"
|
|
111
|
+
export type SagaStepState =
|
|
112
|
+
| "executing"
|
|
113
|
+
| "committed"
|
|
114
|
+
| "failed"
|
|
115
|
+
| "ambiguous"
|
|
116
|
+
| "compensating"
|
|
117
|
+
| "compensation-failed"
|
|
118
|
+
| "compensated"
|
|
119
|
+
|
|
120
|
+
export interface SagaStepRecord {
|
|
121
|
+
readonly name: string
|
|
122
|
+
readonly effectId: string
|
|
123
|
+
readonly compensationEffectId: string
|
|
124
|
+
readonly state: SagaStepState
|
|
125
|
+
readonly compensationArgs: unknown
|
|
126
|
+
readonly result?: unknown
|
|
127
|
+
readonly attempts: number
|
|
128
|
+
readonly nextAttemptAt?: number
|
|
129
|
+
readonly errorCode?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface SagaRecord {
|
|
133
|
+
readonly sagaId: string
|
|
134
|
+
readonly definition: string
|
|
135
|
+
readonly state: SagaState
|
|
136
|
+
readonly input: unknown
|
|
137
|
+
readonly steps: readonly SagaStepRecord[]
|
|
138
|
+
readonly createdAt: number
|
|
139
|
+
readonly updatedAt: number
|
|
140
|
+
readonly version: number
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface SagaStore {
|
|
144
|
+
readonly durability?: "memory" | "durable"
|
|
145
|
+
create(record: SagaRecord): boolean | Promise<boolean>
|
|
146
|
+
get(sagaId: string): SagaRecord | undefined | Promise<SagaRecord | undefined>
|
|
147
|
+
/** Atomic version compare-and-set; this is the saga engine's crash/concurrency boundary. */
|
|
148
|
+
compareAndSet(input: {
|
|
149
|
+
readonly sagaId: string
|
|
150
|
+
readonly version: number
|
|
151
|
+
readonly record: SagaRecord
|
|
152
|
+
}): boolean | Promise<boolean>
|
|
153
|
+
/** Bounded operational scan. Transition-only stores do not need to implement reconciliation. */
|
|
154
|
+
scan?(
|
|
155
|
+
input: ReconciliationScanOptions<SagaState>,
|
|
156
|
+
): ReconciliationScanPage<SagaRecord> | Promise<ReconciliationScanPage<SagaRecord>>
|
|
157
|
+
/** @deprecated Compatibility fallback; production reconciliation should implement `scan`. */
|
|
158
|
+
list?(): readonly SagaRecord[] | Promise<readonly SagaRecord[]>
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface SagaStepExecutionContext {
|
|
162
|
+
readonly effectId: string
|
|
163
|
+
readonly signal: AbortSignal
|
|
164
|
+
}
|
|
165
|
+
export interface SagaCompensationContext {
|
|
166
|
+
readonly effectId: string
|
|
167
|
+
readonly sagaId: string
|
|
168
|
+
readonly attempt: number
|
|
169
|
+
readonly signal: AbortSignal
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface SagaRunContext<C extends Record<string, unknown>> {
|
|
173
|
+
step<K extends keyof C & string, T>(
|
|
174
|
+
name: K,
|
|
175
|
+
compensationArgs: C[K],
|
|
176
|
+
execute: (context: SagaStepExecutionContext) => T | PromiseLike<T>,
|
|
177
|
+
): Promise<T>
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface SagaDefinition<I, C extends Record<string, unknown>> {
|
|
181
|
+
readonly name: string
|
|
182
|
+
readonly capability?: string
|
|
183
|
+
readonly run: (context: SagaRunContext<C>, input: I) => void | PromiseLike<void>
|
|
184
|
+
readonly compensators: {
|
|
185
|
+
readonly [K in keyof C]: (
|
|
186
|
+
args: C[K],
|
|
187
|
+
context: SagaCompensationContext,
|
|
188
|
+
) => void | PromiseLike<void>
|
|
189
|
+
}
|
|
190
|
+
readonly retry?: {
|
|
191
|
+
readonly maxAttempts?: number
|
|
192
|
+
readonly backoffMs?: (attempt: number) => number
|
|
193
|
+
}
|
|
194
|
+
}
|