@bleedingdev/modern-js-bff-effect 3.8.3-ultramodern.2

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.
Files changed (230) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/cjs/data-platform/batch/protocol.js +130 -0
  4. package/dist/cjs/data-platform/batch/queue.js +623 -0
  5. package/dist/cjs/data-platform/batch/request.js +121 -0
  6. package/dist/cjs/data-platform/batch/response.js +65 -0
  7. package/dist/cjs/data-platform/batch/telemetry.js +71 -0
  8. package/dist/cjs/data-platform/batch/url.js +69 -0
  9. package/dist/cjs/data-platform/batch-transport.js +59 -0
  10. package/dist/cjs/data-platform/codec.js +155 -0
  11. package/dist/cjs/data-platform/index.js +124 -0
  12. package/dist/cjs/data-platform/invalidation.js +61 -0
  13. package/dist/cjs/data-platform/trace.js +73 -0
  14. package/dist/cjs/data-platform/types.js +48 -0
  15. package/dist/cjs/data-platform/validation.js +178 -0
  16. package/dist/cjs/effect/context.js +59 -0
  17. package/dist/cjs/effect/dispatch.js +101 -0
  18. package/dist/cjs/effect/edge-context.js +61 -0
  19. package/dist/cjs/effect/edge-dispatcher.js +354 -0
  20. package/dist/cjs/effect/edge.js +62 -0
  21. package/dist/cjs/effect/endpoint-contracts.js +121 -0
  22. package/dist/cjs/effect/entry-shape-registry.js +31 -0
  23. package/dist/cjs/effect/entry-shape.js +113 -0
  24. package/dist/cjs/effect/handler/batch-handler.js +270 -0
  25. package/dist/cjs/effect/handler/batch.js +170 -0
  26. package/dist/cjs/effect/handler/definition.js +99 -0
  27. package/dist/cjs/effect/handler/envelope.js +104 -0
  28. package/dist/cjs/effect/handler/http.js +107 -0
  29. package/dist/cjs/effect/handler/openapi.js +58 -0
  30. package/dist/cjs/effect/handler/routing.js +100 -0
  31. package/dist/cjs/effect/handler/rpc.js +63 -0
  32. package/dist/cjs/effect/handler/types.js +43 -0
  33. package/dist/cjs/effect/handler.js +173 -0
  34. package/dist/cjs/effect/index.js +137 -0
  35. package/dist/cjs/effect/module.js +118 -0
  36. package/dist/cjs/effect/operation-context.js +116 -0
  37. package/dist/cjs/effect/rpcSerialization.js +54 -0
  38. package/dist/cjs/effect-client/batch.js +73 -0
  39. package/dist/cjs/effect-client/envelope.js +145 -0
  40. package/dist/cjs/effect-client/guards.js +44 -0
  41. package/dist/cjs/effect-client/index.js +208 -0
  42. package/dist/cjs/effect-client/request-context.js +64 -0
  43. package/dist/cjs/effect-client/request.js +66 -0
  44. package/dist/cjs/effect-client/runtime.js +100 -0
  45. package/dist/cjs/effect-client/types.js +18 -0
  46. package/dist/cjs/headers.js +46 -0
  47. package/dist/cjs/index.js +83 -0
  48. package/dist/esm/data-platform/batch/protocol.mjs +71 -0
  49. package/dist/esm/data-platform/batch/queue.mjs +582 -0
  50. package/dist/esm/data-platform/batch/request.mjs +68 -0
  51. package/dist/esm/data-platform/batch/response.mjs +24 -0
  52. package/dist/esm/data-platform/batch/telemetry.mjs +27 -0
  53. package/dist/esm/data-platform/batch/url.mjs +25 -0
  54. package/dist/esm/data-platform/batch-transport.mjs +12 -0
  55. package/dist/esm/data-platform/codec.mjs +90 -0
  56. package/dist/esm/data-platform/index.mjs +7 -0
  57. package/dist/esm/data-platform/invalidation.mjs +20 -0
  58. package/dist/esm/data-platform/trace.mjs +29 -0
  59. package/dist/esm/data-platform/types.mjs +4 -0
  60. package/dist/esm/data-platform/validation.mjs +128 -0
  61. package/dist/esm/effect/context.mjs +12 -0
  62. package/dist/esm/effect/dispatch.mjs +63 -0
  63. package/dist/esm/effect/edge-context.mjs +14 -0
  64. package/dist/esm/effect/edge-dispatcher.mjs +115 -0
  65. package/dist/esm/effect/edge.mjs +1 -0
  66. package/dist/esm/effect/endpoint-contracts.mjs +68 -0
  67. package/dist/esm/effect/entry-shape-registry.mjs +17 -0
  68. package/dist/esm/effect/entry-shape.mjs +51 -0
  69. package/dist/esm/effect/handler/batch-handler.mjs +232 -0
  70. package/dist/esm/effect/handler/batch.mjs +105 -0
  71. package/dist/esm/effect/handler/definition.mjs +58 -0
  72. package/dist/esm/effect/handler/envelope.mjs +63 -0
  73. package/dist/esm/effect/handler/http.mjs +69 -0
  74. package/dist/esm/effect/handler/openapi.mjs +20 -0
  75. package/dist/esm/effect/handler/routing.mjs +50 -0
  76. package/dist/esm/effect/handler/rpc.mjs +22 -0
  77. package/dist/esm/effect/handler/types.mjs +5 -0
  78. package/dist/esm/effect/handler.mjs +13 -0
  79. package/dist/esm/effect/index.mjs +7 -0
  80. package/dist/esm/effect/module.mjs +80 -0
  81. package/dist/esm/effect/operation-context.mjs +75 -0
  82. package/dist/esm/effect/rpcSerialization.mjs +16 -0
  83. package/dist/esm/effect-client/batch.mjs +35 -0
  84. package/dist/esm/effect-client/envelope.mjs +104 -0
  85. package/dist/esm/effect-client/guards.mjs +3 -0
  86. package/dist/esm/effect-client/index.mjs +110 -0
  87. package/dist/esm/effect-client/request-context.mjs +23 -0
  88. package/dist/esm/effect-client/request.mjs +28 -0
  89. package/dist/esm/effect-client/runtime.mjs +62 -0
  90. package/dist/esm/effect-client/types.mjs +0 -0
  91. package/dist/esm/headers.mjs +8 -0
  92. package/dist/esm/index.mjs +4 -0
  93. package/dist/esm/rslib-runtime.mjs +18 -0
  94. package/dist/esm-node/data-platform/batch/protocol.mjs +72 -0
  95. package/dist/esm-node/data-platform/batch/queue.mjs +583 -0
  96. package/dist/esm-node/data-platform/batch/request.mjs +69 -0
  97. package/dist/esm-node/data-platform/batch/response.mjs +25 -0
  98. package/dist/esm-node/data-platform/batch/telemetry.mjs +28 -0
  99. package/dist/esm-node/data-platform/batch/url.mjs +26 -0
  100. package/dist/esm-node/data-platform/batch-transport.mjs +13 -0
  101. package/dist/esm-node/data-platform/codec.mjs +91 -0
  102. package/dist/esm-node/data-platform/index.mjs +8 -0
  103. package/dist/esm-node/data-platform/invalidation.mjs +21 -0
  104. package/dist/esm-node/data-platform/trace.mjs +30 -0
  105. package/dist/esm-node/data-platform/types.mjs +5 -0
  106. package/dist/esm-node/data-platform/validation.mjs +129 -0
  107. package/dist/esm-node/effect/context.mjs +13 -0
  108. package/dist/esm-node/effect/dispatch.mjs +64 -0
  109. package/dist/esm-node/effect/edge-context.mjs +15 -0
  110. package/dist/esm-node/effect/edge-dispatcher.mjs +116 -0
  111. package/dist/esm-node/effect/edge.mjs +2 -0
  112. package/dist/esm-node/effect/endpoint-contracts.mjs +69 -0
  113. package/dist/esm-node/effect/entry-shape-registry.mjs +18 -0
  114. package/dist/esm-node/effect/entry-shape.mjs +63 -0
  115. package/dist/esm-node/effect/handler/batch-handler.mjs +233 -0
  116. package/dist/esm-node/effect/handler/batch.mjs +106 -0
  117. package/dist/esm-node/effect/handler/definition.mjs +59 -0
  118. package/dist/esm-node/effect/handler/envelope.mjs +64 -0
  119. package/dist/esm-node/effect/handler/http.mjs +70 -0
  120. package/dist/esm-node/effect/handler/openapi.mjs +21 -0
  121. package/dist/esm-node/effect/handler/routing.mjs +51 -0
  122. package/dist/esm-node/effect/handler/rpc.mjs +23 -0
  123. package/dist/esm-node/effect/handler/types.mjs +6 -0
  124. package/dist/esm-node/effect/handler.mjs +14 -0
  125. package/dist/esm-node/effect/index.mjs +8 -0
  126. package/dist/esm-node/effect/module.mjs +81 -0
  127. package/dist/esm-node/effect/operation-context.mjs +76 -0
  128. package/dist/esm-node/effect/rpcSerialization.mjs +17 -0
  129. package/dist/esm-node/effect-client/batch.mjs +36 -0
  130. package/dist/esm-node/effect-client/envelope.mjs +105 -0
  131. package/dist/esm-node/effect-client/guards.mjs +4 -0
  132. package/dist/esm-node/effect-client/index.mjs +111 -0
  133. package/dist/esm-node/effect-client/request-context.mjs +24 -0
  134. package/dist/esm-node/effect-client/request.mjs +29 -0
  135. package/dist/esm-node/effect-client/runtime.mjs +63 -0
  136. package/dist/esm-node/effect-client/types.mjs +1 -0
  137. package/dist/esm-node/headers.mjs +9 -0
  138. package/dist/esm-node/index.mjs +5 -0
  139. package/dist/esm-node/rslib-runtime.mjs +19 -0
  140. package/dist/types/data-platform/batch/protocol.d.ts +9 -0
  141. package/dist/types/data-platform/batch/queue.d.ts +47 -0
  142. package/dist/types/data-platform/batch/request.d.ts +19 -0
  143. package/dist/types/data-platform/batch/response.d.ts +3 -0
  144. package/dist/types/data-platform/batch/telemetry.d.ts +4 -0
  145. package/dist/types/data-platform/batch/url.d.ts +4 -0
  146. package/dist/types/data-platform/batch-transport.d.ts +3 -0
  147. package/dist/types/data-platform/codec.d.ts +17 -0
  148. package/dist/types/data-platform/index.d.ts +8 -0
  149. package/dist/types/data-platform/invalidation.d.ts +8 -0
  150. package/dist/types/data-platform/trace.d.ts +4 -0
  151. package/dist/types/data-platform/types.d.ts +151 -0
  152. package/dist/types/data-platform/validation.d.ts +29 -0
  153. package/dist/types/effect/context.d.ts +5 -0
  154. package/dist/types/effect/dispatch.d.ts +13 -0
  155. package/dist/types/effect/edge-context.d.ts +5 -0
  156. package/dist/types/effect/edge-dispatcher.d.ts +37 -0
  157. package/dist/types/effect/edge.d.ts +1 -0
  158. package/dist/types/effect/endpoint-contracts.d.ts +67 -0
  159. package/dist/types/effect/entry-shape.d.ts +32 -0
  160. package/dist/types/effect/handler/batch-handler.d.ts +11 -0
  161. package/dist/types/effect/handler/batch.d.ts +15 -0
  162. package/dist/types/effect/handler/definition.d.ts +7 -0
  163. package/dist/types/effect/handler/envelope.d.ts +13 -0
  164. package/dist/types/effect/handler/http.d.ts +15 -0
  165. package/dist/types/effect/handler/openapi.d.ts +4 -0
  166. package/dist/types/effect/handler/routing.d.ts +5 -0
  167. package/dist/types/effect/handler/rpc.d.ts +7 -0
  168. package/dist/types/effect/handler/types.d.ts +153 -0
  169. package/dist/types/effect/handler.d.ts +13 -0
  170. package/dist/types/effect/index.d.ts +6 -0
  171. package/dist/types/effect/module.d.ts +36 -0
  172. package/dist/types/effect/operation-context.d.ts +15 -0
  173. package/dist/types/effect/rpcSerialization.d.ts +3 -0
  174. package/dist/types/effect-client/batch.d.ts +2 -0
  175. package/dist/types/effect-client/envelope.d.ts +15 -0
  176. package/dist/types/effect-client/guards.d.ts +2 -0
  177. package/dist/types/effect-client/index.d.ts +58 -0
  178. package/dist/types/effect-client/request-context.d.ts +3 -0
  179. package/dist/types/effect-client/request.d.ts +1 -0
  180. package/dist/types/effect-client/runtime.d.ts +5 -0
  181. package/dist/types/effect-client/types.d.ts +68 -0
  182. package/dist/types/headers.d.ts +1 -0
  183. package/dist/types/index.d.ts +3 -0
  184. package/package.json +164 -0
  185. package/src/data-platform/batch/protocol.ts +112 -0
  186. package/src/data-platform/batch/queue.ts +876 -0
  187. package/src/data-platform/batch/request.ts +130 -0
  188. package/src/data-platform/batch/response.ts +74 -0
  189. package/src/data-platform/batch/telemetry.ts +47 -0
  190. package/src/data-platform/batch/url.ts +54 -0
  191. package/src/data-platform/batch-transport.ts +22 -0
  192. package/src/data-platform/codec.ts +145 -0
  193. package/src/data-platform/index.ts +63 -0
  194. package/src/data-platform/invalidation.ts +57 -0
  195. package/src/data-platform/trace.ts +49 -0
  196. package/src/data-platform/types.ts +186 -0
  197. package/src/data-platform/validation.ts +303 -0
  198. package/src/effect/context.ts +37 -0
  199. package/src/effect/dispatch.ts +132 -0
  200. package/src/effect/edge-context.ts +51 -0
  201. package/src/effect/edge-dispatcher.ts +310 -0
  202. package/src/effect/edge.ts +1 -0
  203. package/src/effect/endpoint-contracts.ts +191 -0
  204. package/src/effect/entry-shape-registry.cjs +16 -0
  205. package/src/effect/entry-shape-registry.d.cts +6 -0
  206. package/src/effect/entry-shape.ts +155 -0
  207. package/src/effect/handler/batch-handler.ts +475 -0
  208. package/src/effect/handler/batch.ts +182 -0
  209. package/src/effect/handler/definition.ts +121 -0
  210. package/src/effect/handler/envelope.ts +133 -0
  211. package/src/effect/handler/http.ts +133 -0
  212. package/src/effect/handler/openapi.ts +45 -0
  213. package/src/effect/handler/routing.ts +91 -0
  214. package/src/effect/handler/rpc.ts +45 -0
  215. package/src/effect/handler/types.ts +255 -0
  216. package/src/effect/handler.ts +33 -0
  217. package/src/effect/index.ts +22 -0
  218. package/src/effect/module.ts +186 -0
  219. package/src/effect/operation-context.ts +144 -0
  220. package/src/effect/rpcSerialization.ts +20 -0
  221. package/src/effect-client/batch.ts +50 -0
  222. package/src/effect-client/envelope.ts +238 -0
  223. package/src/effect-client/guards.ts +6 -0
  224. package/src/effect-client/index.ts +325 -0
  225. package/src/effect-client/request-context.ts +54 -0
  226. package/src/effect-client/request.ts +51 -0
  227. package/src/effect-client/runtime.ts +122 -0
  228. package/src/effect-client/types.ts +84 -0
  229. package/src/headers.ts +7 -0
  230. package/src/index.ts +3 -0
@@ -0,0 +1,186 @@
1
+ // @effect-diagnostics asyncFunction:off globalDate:off globalRandom:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ /**
3
+ * Data-platform envelope (`x-modernjs-data-envelope`): the TRANSPORT/CACHE
4
+ * contract of the effect lane — protocol versioning, cache scope keys,
5
+ * selection plans, batching and hydration checksums.
6
+ *
7
+ * Boundary note (one envelope system per concern): cross-project
8
+ * AUTHORIZATION is the bff-core policy's job (`x-modernjs-bff-envelope` +
9
+ * operation contracts, evaluated server-side in both the hono and effect
10
+ * lanes). Nothing in this module is a trust boundary; treat every value
11
+ * here as client-asserted cache metadata.
12
+ */
13
+
14
+ export type DataRequestMode =
15
+ | 'cache-first'
16
+ | 'stale-while-revalidate'
17
+ | 'network-only';
18
+
19
+ export type DataMutationMode = 'optimistic' | 'pessimistic' | 'fire-and-forget';
20
+
21
+ export type SelectionPlan = {
22
+ [field: string]: true | SelectionPlan;
23
+ };
24
+
25
+ export interface OperationDescriptor {
26
+ appNamespace: string;
27
+ apiId: string;
28
+ group: string;
29
+ endpoint: string;
30
+ schemaHash?: string;
31
+ version?: number;
32
+ }
33
+
34
+ export interface CacheScope {
35
+ appNamespace: string;
36
+ origin: string;
37
+ tenantId?: string;
38
+ userId?: string;
39
+ sessionId?: string;
40
+ }
41
+
42
+ export interface TraceContext {
43
+ traceId: string;
44
+ spanId: string;
45
+ sampled: boolean;
46
+ parentSpanId?: string;
47
+ }
48
+
49
+ export interface RequestEnvelope<Input = unknown> {
50
+ protocolVersion: 1;
51
+ operationId: string;
52
+ appNamespace: string;
53
+ origin: string;
54
+ requestMode: DataRequestMode;
55
+ mutationMode?: DataMutationMode;
56
+ scopeKey: string;
57
+ input: Input;
58
+ inputHash: string;
59
+ selectionPlan?: SelectionPlan;
60
+ selectionHash?: string;
61
+ traceparent?: string;
62
+ timestamp: number;
63
+ }
64
+
65
+ export interface HydrationEnvelope<Payload = unknown> {
66
+ protocolVersion: 1;
67
+ runtimeVersion: string;
68
+ appNamespace: string;
69
+ origin: string;
70
+ createdAt: number;
71
+ payload: Payload;
72
+ checksum: string;
73
+ }
74
+
75
+ export interface InvalidationEvent {
76
+ sourceNamespace: string;
77
+ sourceOperationId: string;
78
+ scopeKey: string;
79
+ targetNamespaces?: string[];
80
+ targetOperationIds?: string[];
81
+ }
82
+
83
+ export interface InvalidationSubscriber {
84
+ namespace: string;
85
+ operationIds?: string[];
86
+ scopeKey?: string;
87
+ acceptCrossNamespace?: boolean;
88
+ }
89
+
90
+ export interface DataBatchRequestItem {
91
+ id: string;
92
+ path: string;
93
+ method: string;
94
+ headers?: Record<string, string>;
95
+ body?: DataBatchBody;
96
+ }
97
+
98
+ export interface DataBatchRequestPayload {
99
+ protocolVersion: 2;
100
+ batchId: string;
101
+ sentAt: number;
102
+ items: DataBatchRequestItem[];
103
+ }
104
+
105
+ export interface DataBatchResponseItem {
106
+ id: string;
107
+ status: number;
108
+ headers?: DataBatchHeader[];
109
+ body?: DataBatchBody;
110
+ }
111
+
112
+ export interface DataBatchBody {
113
+ encoding: 'base64';
114
+ data: string;
115
+ }
116
+
117
+ export type DataBatchHeader = [name: string, value: string];
118
+
119
+ export interface DataBatchResponsePayload {
120
+ protocolVersion: 2;
121
+ batchId: string;
122
+ receivedAt: number;
123
+ items: DataBatchResponseItem[];
124
+ }
125
+
126
+ export interface DataBatchTransportEvent {
127
+ type: 'enqueue' | 'flush' | 'fallback' | 'disable';
128
+ endpoint: string;
129
+ batchId?: string;
130
+ size?: number;
131
+ reason?: string;
132
+ }
133
+
134
+ export type DataBatchTransportTelemetryAttributes = Record<
135
+ string,
136
+ string | number | boolean
137
+ >;
138
+
139
+ export interface DataBatchTransportOptions {
140
+ endpoint?: string;
141
+ fetch?: (
142
+ input: DataTransportRequestInfo,
143
+ init?: RequestInit,
144
+ ) => Promise<Response>;
145
+ flushIntervalMs?: number;
146
+ maxBatchSize?: number;
147
+ maxBatchBytes?: number;
148
+ requestTimeoutMs?: number;
149
+ allowedMethods?: string[];
150
+ onEvent?: (event: DataBatchTransportEvent) => void;
151
+ }
152
+
153
+ export interface SelectionPlanValidationOptions {
154
+ maxDepth?: number;
155
+ maxFields?: number;
156
+ allowedLeafPaths?: ReadonlyArray<string>;
157
+ }
158
+
159
+ export interface SelectionPlanValidationResult {
160
+ ok: boolean;
161
+ errors: string[];
162
+ stats: {
163
+ maxDepth: number;
164
+ fieldCount: number;
165
+ };
166
+ }
167
+
168
+ export interface RequestEnvelopeValidationOptions {
169
+ expectedProtocolVersion?: number;
170
+ expectedNamespace?: string;
171
+ expectedOrigin?: string;
172
+ requireTraceContext?: boolean;
173
+ }
174
+
175
+ export interface HydrationEnvelopeValidationOptions {
176
+ expectedProtocolVersion?: number;
177
+ expectedNamespace?: string;
178
+ expectedOrigin?: string;
179
+ expectedRuntimeVersion?: string;
180
+ }
181
+
182
+ export const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
183
+ export const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
184
+ export const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
185
+
186
+ export type DataTransportRequestInfo = string | URL | Request;
@@ -0,0 +1,303 @@
1
+ // @effect-diagnostics asyncFunction:off globalDate:off globalRandom:off globalTimers:off newPromise:off strictBooleanExpressions:off
2
+ import {
3
+ buildScopeKey,
4
+ createOperationId,
5
+ hashString,
6
+ isPlainObject,
7
+ normalizeOrigin,
8
+ sanitizeSegment,
9
+ stableStringify,
10
+ } from './codec';
11
+ import { formatTraceparentHeader, parseTraceparentHeader } from './trace';
12
+ import type {
13
+ CacheScope,
14
+ DataMutationMode,
15
+ DataRequestMode,
16
+ HydrationEnvelope,
17
+ HydrationEnvelopeValidationOptions,
18
+ OperationDescriptor,
19
+ RequestEnvelope,
20
+ RequestEnvelopeValidationOptions,
21
+ SelectionPlan,
22
+ SelectionPlanValidationOptions,
23
+ SelectionPlanValidationResult,
24
+ TraceContext,
25
+ } from './types';
26
+
27
+ export function validateSelectionPlan(
28
+ plan: SelectionPlan,
29
+ options: SelectionPlanValidationOptions = {},
30
+ ): SelectionPlanValidationResult {
31
+ const maxDepthLimit = options.maxDepth ?? 8;
32
+ const maxFieldsLimit = options.maxFields ?? 256;
33
+ const allowedLeafPaths = options.allowedLeafPaths
34
+ ? new Set(options.allowedLeafPaths)
35
+ : null;
36
+
37
+ const errors: string[] = [];
38
+ let maxDepth = 0;
39
+ let fieldCount = 0;
40
+
41
+ const walk = (node: unknown, path: string[]) => {
42
+ if (!isPlainObject(node)) {
43
+ errors.push(
44
+ `Selection node at "${path.join('.') || '<root>'}" must be an object`,
45
+ );
46
+ return;
47
+ }
48
+
49
+ const keys = Object.keys(node);
50
+ if (keys.length === 0) {
51
+ errors.push(
52
+ `Selection node at "${path.join('.') || '<root>'}" cannot be empty`,
53
+ );
54
+ return;
55
+ }
56
+
57
+ for (const key of keys) {
58
+ fieldCount += 1;
59
+ if (fieldCount > maxFieldsLimit) {
60
+ errors.push(
61
+ `Selection has too many fields: ${String(fieldCount)} > ${String(maxFieldsLimit)}`,
62
+ );
63
+ return;
64
+ }
65
+
66
+ const nextPath = [...path, key];
67
+ const depth = nextPath.length;
68
+ if (depth > maxDepth) {
69
+ maxDepth = depth;
70
+ }
71
+ if (depth > maxDepthLimit) {
72
+ errors.push(
73
+ `Selection exceeds maxDepth at "${nextPath.join('.')}" (${String(depth)} > ${String(maxDepthLimit)})`,
74
+ );
75
+ return;
76
+ }
77
+
78
+ const value = (node as Record<string, unknown>)[key];
79
+ if (value === true) {
80
+ if (allowedLeafPaths && !allowedLeafPaths.has(nextPath.join('.'))) {
81
+ errors.push(`Unknown selected field "${nextPath.join('.')}"`);
82
+ }
83
+ continue;
84
+ }
85
+
86
+ if (!isPlainObject(value)) {
87
+ errors.push(
88
+ `Invalid selection value at "${nextPath.join('.')}"; expected true or nested object`,
89
+ );
90
+ continue;
91
+ }
92
+
93
+ walk(value, nextPath);
94
+ }
95
+ };
96
+
97
+ walk(plan, []);
98
+
99
+ return {
100
+ ok: errors.length === 0,
101
+ errors,
102
+ stats: {
103
+ maxDepth,
104
+ fieldCount,
105
+ },
106
+ };
107
+ }
108
+
109
+ export function createRequestEnvelope<Input>(input: {
110
+ operation: OperationDescriptor;
111
+ scope: CacheScope;
112
+ requestInput: Input;
113
+ selectionPlan?: SelectionPlan;
114
+ requestMode?: DataRequestMode;
115
+ mutationMode?: DataMutationMode;
116
+ traceContext?: TraceContext;
117
+ requireTraceContext?: boolean;
118
+ timestamp?: number;
119
+ protocolVersion?: 1;
120
+ }): RequestEnvelope<Input> {
121
+ if (input.requireTraceContext && !input.traceContext) {
122
+ throw new Error('Trace context is required for this request envelope');
123
+ }
124
+
125
+ const traceparent = input.traceContext
126
+ ? formatTraceparentHeader(input.traceContext)
127
+ : undefined;
128
+
129
+ const envelope: RequestEnvelope<Input> = {
130
+ protocolVersion: input.protocolVersion ?? 1,
131
+ operationId: createOperationId(input.operation),
132
+ appNamespace: sanitizeSegment(input.scope.appNamespace),
133
+ origin: normalizeOrigin(input.scope.origin),
134
+ requestMode: input.requestMode ?? 'cache-first',
135
+ mutationMode: input.mutationMode,
136
+ scopeKey: buildScopeKey(input.scope),
137
+ input: input.requestInput,
138
+ inputHash: hashString(stableStringify(input.requestInput ?? null)),
139
+ selectionPlan: input.selectionPlan,
140
+ selectionHash: input.selectionPlan
141
+ ? hashString(stableStringify(input.selectionPlan))
142
+ : undefined,
143
+ traceparent,
144
+ timestamp: input.timestamp ?? Date.now(),
145
+ };
146
+
147
+ return envelope;
148
+ }
149
+
150
+ export function validateRequestEnvelope(
151
+ envelope: RequestEnvelope,
152
+ options: RequestEnvelopeValidationOptions = {},
153
+ ): { ok: boolean; errors: string[] } {
154
+ const errors: string[] = [];
155
+
156
+ if (
157
+ options.expectedProtocolVersion !== undefined &&
158
+ envelope.protocolVersion !== options.expectedProtocolVersion
159
+ ) {
160
+ errors.push(
161
+ `Protocol mismatch: expected ${String(options.expectedProtocolVersion)} but received ${String(envelope.protocolVersion)}`,
162
+ );
163
+ }
164
+
165
+ if (
166
+ options.expectedNamespace &&
167
+ envelope.appNamespace !== options.expectedNamespace
168
+ ) {
169
+ errors.push(
170
+ `Namespace mismatch: expected ${options.expectedNamespace} but received ${envelope.appNamespace}`,
171
+ );
172
+ }
173
+
174
+ if (options.expectedOrigin) {
175
+ const expectedOrigin = normalizeOrigin(options.expectedOrigin);
176
+ if (envelope.origin !== expectedOrigin) {
177
+ errors.push(
178
+ `Origin mismatch: expected ${expectedOrigin} but received ${envelope.origin}`,
179
+ );
180
+ }
181
+ }
182
+
183
+ if (!envelope.operationId) {
184
+ errors.push('Missing operationId');
185
+ }
186
+
187
+ if (!envelope.scopeKey) {
188
+ errors.push('Missing scopeKey');
189
+ }
190
+
191
+ if (options.requireTraceContext && !envelope.traceparent) {
192
+ errors.push('Missing trace context');
193
+ }
194
+
195
+ if (envelope.traceparent && !parseTraceparentHeader(envelope.traceparent)) {
196
+ errors.push('Invalid traceparent header');
197
+ }
198
+
199
+ const computedInputHash = hashString(stableStringify(envelope.input ?? null));
200
+ if (computedInputHash !== envelope.inputHash) {
201
+ errors.push('Input hash mismatch');
202
+ }
203
+
204
+ if (envelope.selectionPlan) {
205
+ const computedSelectionHash = hashString(
206
+ stableStringify(envelope.selectionPlan),
207
+ );
208
+ if (computedSelectionHash !== envelope.selectionHash) {
209
+ errors.push('Selection hash mismatch');
210
+ }
211
+ }
212
+
213
+ return {
214
+ ok: errors.length === 0,
215
+ errors,
216
+ };
217
+ }
218
+
219
+ export function createHydrationEnvelope<Payload>(input: {
220
+ runtimeVersion: string;
221
+ scope: Pick<CacheScope, 'appNamespace' | 'origin'>;
222
+ payload: Payload;
223
+ createdAt?: number;
224
+ protocolVersion?: 1;
225
+ }): HydrationEnvelope<Payload> {
226
+ const envelopeWithoutChecksum = {
227
+ protocolVersion: input.protocolVersion ?? 1,
228
+ runtimeVersion: input.runtimeVersion,
229
+ appNamespace: sanitizeSegment(input.scope.appNamespace),
230
+ origin: normalizeOrigin(input.scope.origin),
231
+ createdAt: input.createdAt ?? Date.now(),
232
+ payload: input.payload,
233
+ };
234
+
235
+ const checksum = hashString(stableStringify(envelopeWithoutChecksum));
236
+
237
+ return {
238
+ ...envelopeWithoutChecksum,
239
+ checksum,
240
+ };
241
+ }
242
+
243
+ export function validateHydrationEnvelope(
244
+ envelope: HydrationEnvelope,
245
+ options: HydrationEnvelopeValidationOptions = {},
246
+ ): { ok: boolean; errors: string[] } {
247
+ const errors: string[] = [];
248
+
249
+ if (
250
+ options.expectedProtocolVersion !== undefined &&
251
+ envelope.protocolVersion !== options.expectedProtocolVersion
252
+ ) {
253
+ errors.push(
254
+ `Protocol mismatch: expected ${String(options.expectedProtocolVersion)} but received ${String(envelope.protocolVersion)}`,
255
+ );
256
+ }
257
+
258
+ if (
259
+ options.expectedNamespace &&
260
+ envelope.appNamespace !== options.expectedNamespace
261
+ ) {
262
+ errors.push(
263
+ `Namespace mismatch: expected ${options.expectedNamespace} but received ${envelope.appNamespace}`,
264
+ );
265
+ }
266
+
267
+ if (options.expectedOrigin) {
268
+ const expectedOrigin = normalizeOrigin(options.expectedOrigin);
269
+ if (envelope.origin !== expectedOrigin) {
270
+ errors.push(
271
+ `Origin mismatch: expected ${expectedOrigin} but received ${envelope.origin}`,
272
+ );
273
+ }
274
+ }
275
+
276
+ if (
277
+ options.expectedRuntimeVersion &&
278
+ envelope.runtimeVersion !== options.expectedRuntimeVersion
279
+ ) {
280
+ errors.push(
281
+ `Runtime version mismatch: expected ${options.expectedRuntimeVersion} but received ${envelope.runtimeVersion}`,
282
+ );
283
+ }
284
+
285
+ const checksumBase = {
286
+ protocolVersion: envelope.protocolVersion,
287
+ runtimeVersion: envelope.runtimeVersion,
288
+ appNamespace: envelope.appNamespace,
289
+ origin: envelope.origin,
290
+ createdAt: envelope.createdAt,
291
+ payload: envelope.payload,
292
+ };
293
+
294
+ const checksum = hashString(stableStringify(checksumBase));
295
+ if (checksum !== envelope.checksum) {
296
+ errors.push('Hydration checksum mismatch');
297
+ }
298
+
299
+ return {
300
+ ok: errors.length === 0,
301
+ errors,
302
+ };
303
+ }
@@ -0,0 +1,37 @@
1
+ // @effect-diagnostics strictBooleanExpressions:off
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import {
4
+ type EffectContext,
5
+ type EffectContextStorage,
6
+ kEffectContextStorage,
7
+ } from './operation-context';
8
+
9
+ export {
10
+ type CreateEffectOperationContextOptions,
11
+ createEffectOperationContext,
12
+ type EffectContext,
13
+ } from './operation-context';
14
+
15
+ const globalStore = globalThis as typeof globalThis & {
16
+ [kEffectContextStorage]?: EffectContextStorage;
17
+ };
18
+
19
+ const effectContextStorage =
20
+ globalStore[kEffectContextStorage] ??
21
+ (globalStore[kEffectContextStorage] = new AsyncLocalStorage<EffectContext>());
22
+
23
+ export const runWithEffectContext = <T>(
24
+ context: EffectContext,
25
+ cb: () => T,
26
+ ): T => effectContextStorage.run(context, cb);
27
+
28
+ export const useEffectContext = (): EffectContext => {
29
+ const context = effectContextStorage.getStore();
30
+ if (!context) {
31
+ throw new Error(`Can't call useEffectContext out of Effect runtime scope`);
32
+ }
33
+
34
+ return context;
35
+ };
36
+
37
+ export const useOperationContext = () => useEffectContext().operationContext;
@@ -0,0 +1,132 @@
1
+ // @effect-diagnostics anyUnknownInErrorContext:off asyncFunction:off strictBooleanExpressions:off
2
+ import { createSafeFailureResponse } from '@modern-js/runtime-extensions/safe-failure';
3
+ import type { EffectBffRequestHandler } from './module';
4
+ import {
5
+ createEffectOperationContext,
6
+ type EffectContext,
7
+ } from './operation-context';
8
+
9
+ type EffectDispatchContextRunner = <T>(
10
+ context: EffectContext,
11
+ cb: () => T,
12
+ ) => T;
13
+
14
+ export type DispatchEffectBffRequestOptions = {
15
+ prefix?: string;
16
+ env?: Record<string, unknown>;
17
+ path?: string;
18
+ method?: string;
19
+ runWithEffectContext: EffectDispatchContextRunner;
20
+ onError?: (
21
+ error: unknown,
22
+ context: EffectContext,
23
+ ) => Promise<Response> | Response;
24
+ };
25
+
26
+ function normalizeEffectBffPrefix(prefix: string | undefined) {
27
+ if (!prefix || prefix === '/') {
28
+ return '';
29
+ }
30
+ return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
31
+ }
32
+
33
+ function removeEffectBffPrefixFromPath(
34
+ pathname: string,
35
+ prefix: string | undefined,
36
+ ) {
37
+ const normalized = normalizeEffectBffPrefix(prefix);
38
+ if (
39
+ !normalized ||
40
+ (pathname !== normalized && !pathname.startsWith(`${normalized}/`))
41
+ ) {
42
+ return pathname;
43
+ }
44
+ const sliced = pathname.slice(normalized.length);
45
+ return sliced.startsWith('/') ? sliced : `/${sliced}`;
46
+ }
47
+
48
+ function matchesEffectBffPrefix(pathname: string, prefix: string | undefined) {
49
+ const normalized = normalizeEffectBffPrefix(prefix);
50
+ return (
51
+ !normalized ||
52
+ pathname === normalized ||
53
+ pathname.startsWith(`${normalized}/`)
54
+ );
55
+ }
56
+
57
+ function createEffectBffMountedRequest(
58
+ req: Request,
59
+ prefix: string | undefined,
60
+ ) {
61
+ const url = new URL(req.url);
62
+ const nextPath = removeEffectBffPrefixFromPath(url.pathname, prefix);
63
+ if (nextPath !== url.pathname) {
64
+ url.pathname = nextPath;
65
+ return new Request(url, req);
66
+ }
67
+ return req;
68
+ }
69
+
70
+ function createEffectBffDispatchContext(
71
+ originalRequest: Request,
72
+ mountedRequest: Request,
73
+ options: Omit<DispatchEffectBffRequestOptions, 'runWithEffectContext'>,
74
+ ): EffectContext {
75
+ const path = options.path ?? new URL(originalRequest.url).pathname;
76
+ const method = options.method ?? originalRequest.method;
77
+ const env = options.env ?? {};
78
+
79
+ return {
80
+ request: mountedRequest,
81
+ env,
82
+ path,
83
+ method,
84
+ operationContext: createEffectOperationContext({
85
+ request: mountedRequest,
86
+ env,
87
+ path,
88
+ method,
89
+ }),
90
+ };
91
+ }
92
+
93
+ export async function dispatchEffectBffRequestWithContext(
94
+ handler: EffectBffRequestHandler,
95
+ request: Request,
96
+ options: DispatchEffectBffRequestOptions,
97
+ ): Promise<Response> {
98
+ const requestPath = new URL(request.url).pathname;
99
+ if (!matchesEffectBffPrefix(requestPath, options.prefix)) {
100
+ return new Response('', { status: 404 });
101
+ }
102
+
103
+ const mountedRequest = createEffectBffMountedRequest(request, options.prefix);
104
+ const effectContext = createEffectBffDispatchContext(
105
+ request,
106
+ mountedRequest,
107
+ options,
108
+ );
109
+
110
+ try {
111
+ const response = await options.runWithEffectContext(effectContext, () =>
112
+ handler(mountedRequest, effectContext),
113
+ );
114
+ if (!(response instanceof Response)) {
115
+ throw new Error(
116
+ '[BFF][Effect] Effect handler must return Response instance.',
117
+ );
118
+ }
119
+ return new Response(response.body, response);
120
+ } catch (error) {
121
+ if (error instanceof Response) {
122
+ return new Response(error.body, error);
123
+ }
124
+ if (options.onError) {
125
+ const errorResponse = await options.onError(error, effectContext);
126
+ if (errorResponse instanceof Response) {
127
+ return errorResponse;
128
+ }
129
+ }
130
+ return createSafeFailureResponse(error);
131
+ }
132
+ }
@@ -0,0 +1,51 @@
1
+ // @effect-diagnostics strictBooleanExpressions:off
2
+ import {
3
+ type EffectContext,
4
+ type EffectContextStorage,
5
+ kEffectContextStorage,
6
+ } from './operation-context';
7
+
8
+ export {
9
+ type CreateEffectOperationContextOptions,
10
+ createEffectOperationContext,
11
+ type EffectContext,
12
+ } from './operation-context';
13
+
14
+ type EffectContextStorageConstructor = new () => EffectContextStorage;
15
+
16
+ const globalStore = globalThis as typeof globalThis & {
17
+ [kEffectContextStorage]?: EffectContextStorage;
18
+ process?: {
19
+ getBuiltinModule?: (id: string) => unknown;
20
+ };
21
+ };
22
+
23
+ const asyncHooks = globalStore.process?.getBuiltinModule?.(
24
+ 'node:async_hooks',
25
+ ) as { AsyncLocalStorage?: EffectContextStorageConstructor } | undefined;
26
+ const AsyncLocalStorage = asyncHooks?.AsyncLocalStorage;
27
+ if (typeof AsyncLocalStorage !== 'function') {
28
+ throw new Error(
29
+ '[BFF][Effect] The edge runtime must provide AsyncLocalStorage. Enable Node.js compatibility or the nodejs_als compatibility flag.',
30
+ );
31
+ }
32
+
33
+ const effectContextStorage =
34
+ globalStore[kEffectContextStorage] ??
35
+ (globalStore[kEffectContextStorage] = new AsyncLocalStorage());
36
+
37
+ export const runWithEffectContext = <T>(
38
+ context: EffectContext,
39
+ cb: () => T,
40
+ ): T => effectContextStorage.run(context, cb);
41
+
42
+ export const useEffectContext = (): EffectContext => {
43
+ const context = effectContextStorage.getStore();
44
+ if (!context) {
45
+ throw new Error(`Can't call useEffectContext out of Effect runtime scope`);
46
+ }
47
+
48
+ return context;
49
+ };
50
+
51
+ export const useOperationContext = () => useEffectContext().operationContext;