@jarenjs/contract 0.43.1

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 (84) hide show
  1. package/README.md +508 -0
  2. package/dist/types/adapters/fetch.d.ts +27 -0
  3. package/dist/types/adapters/node.d.ts +47 -0
  4. package/dist/types/app/binding.d.ts +122 -0
  5. package/dist/types/app/effect.d.ts +77 -0
  6. package/dist/types/app/index.d.ts +31 -0
  7. package/dist/types/app/subscription.d.ts +82 -0
  8. package/dist/types/bundle.d.ts +43 -0
  9. package/dist/types/cli.d.ts +15 -0
  10. package/dist/types/client/http.d.ts +242 -0
  11. package/dist/types/client/outcome.d.ts +289 -0
  12. package/dist/types/compat.d.ts +36 -0
  13. package/dist/types/compile.d.ts +196 -0
  14. package/dist/types/describe.d.ts +115 -0
  15. package/dist/types/diff.d.ts +91 -0
  16. package/dist/types/errors.d.ts +205 -0
  17. package/dist/types/http/dispatch.d.ts +148 -0
  18. package/dist/types/http/serve.d.ts +154 -0
  19. package/dist/types/http/wire.d.ts +334 -0
  20. package/dist/types/index.d.ts +39 -0
  21. package/dist/types/ledger.d.ts +207 -0
  22. package/dist/types/local/index.d.ts +127 -0
  23. package/dist/types/messages.d.ts +63 -0
  24. package/dist/types/path.d.ts +119 -0
  25. package/dist/types/pipeline.d.ts +157 -0
  26. package/dist/types/port/client.d.ts +142 -0
  27. package/dist/types/port/frame.d.ts +195 -0
  28. package/dist/types/port/serve.d.ts +102 -0
  29. package/dist/types/project/index.d.ts +34 -0
  30. package/dist/types/project/markdown.d.ts +28 -0
  31. package/dist/types/project/openapi.d.ts +102 -0
  32. package/dist/types/project/tools.d.ts +57 -0
  33. package/dist/types/project/typescript.d.ts +59 -0
  34. package/dist/types/public.d.ts +73 -0
  35. package/dist/types/revision.d.ts +36 -0
  36. package/dist/types/stream/client.d.ts +104 -0
  37. package/dist/types/stream/server.d.ts +106 -0
  38. package/dist/types/stream/sse.d.ts +62 -0
  39. package/docs/APP-INTEGRATION.md +301 -0
  40. package/docs/CONTRACT-FORMAT.md +1923 -0
  41. package/package.json +110 -0
  42. package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
  43. package/schemas/jaren-contract-port.schema.json +241 -0
  44. package/schemas/jaren-contract.draft-07.schema.json +287 -0
  45. package/schemas/jaren-contract.schema.json +287 -0
  46. package/src/adapters/fetch.js +109 -0
  47. package/src/adapters/node.js +238 -0
  48. package/src/app/binding.js +426 -0
  49. package/src/app/effect.js +190 -0
  50. package/src/app/index.js +26 -0
  51. package/src/app/subscription.js +130 -0
  52. package/src/bundle.js +168 -0
  53. package/src/cli.js +264 -0
  54. package/src/client/http.js +1150 -0
  55. package/src/client/outcome.js +364 -0
  56. package/src/compat.js +62 -0
  57. package/src/compile.js +1162 -0
  58. package/src/describe.js +109 -0
  59. package/src/diff.js +610 -0
  60. package/src/errors.js +236 -0
  61. package/src/http/dispatch.js +1054 -0
  62. package/src/http/serve.js +301 -0
  63. package/src/http/wire.js +469 -0
  64. package/src/index.js +33 -0
  65. package/src/ledger.js +225 -0
  66. package/src/local/index.js +363 -0
  67. package/src/messages.js +68 -0
  68. package/src/path.js +471 -0
  69. package/src/pipeline.js +241 -0
  70. package/src/port/client.js +518 -0
  71. package/src/port/frame.js +196 -0
  72. package/src/port/serve.js +442 -0
  73. package/src/project/index.js +29 -0
  74. package/src/project/markdown.js +244 -0
  75. package/src/project/openapi.js +564 -0
  76. package/src/project/openapi.jslt.json +149 -0
  77. package/src/project/tools.js +139 -0
  78. package/src/project/typescript.js +152 -0
  79. package/src/project/typescript.jtlt.json +72 -0
  80. package/src/public.js +206 -0
  81. package/src/revision.js +90 -0
  82. package/src/stream/client.js +212 -0
  83. package/src/stream/server.js +306 -0
  84. package/src/stream/sse.js +67 -0
@@ -0,0 +1,287 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://jarenjs.dev/schemas/jaren-contract/0.1/draft-07",
4
+ "title": "Jaren operation contract 0.1",
5
+ "description": "Structural grammar of a jaren-contract document (see packages/contract/docs/CONTRACT-FORMAT.md): the operations two Jaren ends may exchange, each a JSON-in/JSON-out operation with a kind, an input object schema, an output schema, declared errors, a policy and an HTTP binding. Every vocabulary here is CLOSED — an unknown member of the root, an operation, policy, http or an error declaration is refused, because a silently ignored policy is a behavior bug. Embedded schemas (input, output, error schemas, $defs) are typed structurally only; the compiler is the authority for $ref resolution, the effective input type, the cross-member rules (path variables against input.properties, location conflicts, a GET body) and route-shape uniqueness.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$contract": {
9
+ "description": "The contract format version.",
10
+ "const": "0.1"
11
+ },
12
+ "id": {
13
+ "description": "An optional identifier for the contract (a tool prefix, a file name).",
14
+ "type": "string",
15
+ "pattern": "^[A-Za-z_][A-Za-z0-9_-]*$"
16
+ },
17
+ "version": {
18
+ "description": "The consumer's version string; a compatibility claim, unrelated to the revision hash.",
19
+ "type": "string",
20
+ "minLength": 1
21
+ },
22
+ "compat": {
23
+ "description": "Peer version strings this contract accepts.",
24
+ "type": "array",
25
+ "items": {
26
+ "type": "string",
27
+ "minLength": 1
28
+ }
29
+ },
30
+ "$defs": {
31
+ "description": "Named schemas the operations reference as #/$defs/<name>.",
32
+ "type": "object",
33
+ "additionalProperties": {
34
+ "description": "A JSON Schema.",
35
+ "type": [
36
+ "object",
37
+ "boolean"
38
+ ]
39
+ }
40
+ },
41
+ "operations": {
42
+ "description": "Operation id (dotted lowercase words) to operation declaration.",
43
+ "type": "object",
44
+ "minProperties": 1,
45
+ "propertyNames": {
46
+ "pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)*$"
47
+ },
48
+ "additionalProperties": {
49
+ "$ref": "#/definitions/operation"
50
+ }
51
+ }
52
+ },
53
+ "required": [
54
+ "$contract",
55
+ "operations"
56
+ ],
57
+ "additionalProperties": false,
58
+ "definitions": {
59
+ "operation": {
60
+ "description": "One operation: its kind, the input object schema whose top-level members the http binding places, the output schema, declared errors, the behavior policy and the http binding (canonical POST /<id> when absent).",
61
+ "type": "object",
62
+ "properties": {
63
+ "kind": {
64
+ "description": "'read', 'command' or 'subscribe' — a subscribe operation's snapshot and { patch, seq } emissions travel the stream binding (SSE over http, push frames over port); the compiler forces its binding to GET + text/event-stream.",
65
+ "enum": [
66
+ "read",
67
+ "command",
68
+ "subscribe"
69
+ ]
70
+ },
71
+ "input": {
72
+ "description": "A JSON Schema whose effective type is object (the compiler follows $ref to check).",
73
+ "type": "object"
74
+ },
75
+ "output": {
76
+ "description": "A JSON Schema; true accepts anything (an opaque or unspecified body).",
77
+ "type": [
78
+ "object",
79
+ "boolean"
80
+ ]
81
+ },
82
+ "errors": {
83
+ "description": "Declared operation errors by code.",
84
+ "type": "object",
85
+ "propertyNames": {
86
+ "pattern": "^[a-z][a-z0-9-]*$"
87
+ },
88
+ "additionalProperties": {
89
+ "$ref": "#/definitions/errorDeclaration"
90
+ }
91
+ },
92
+ "policy": {
93
+ "$ref": "#/definitions/policy"
94
+ },
95
+ "http": {
96
+ "$ref": "#/definitions/http"
97
+ },
98
+ "doc": {
99
+ "description": "Human-readable documentation for projections.",
100
+ "type": "string"
101
+ }
102
+ },
103
+ "required": [
104
+ "kind",
105
+ "output"
106
+ ],
107
+ "additionalProperties": false
108
+ },
109
+ "errorDeclaration": {
110
+ "description": "One declared error: the HTTP status a binding that carries statuses uses (default 400) and an optional schema for its details.",
111
+ "type": "object",
112
+ "properties": {
113
+ "status": {
114
+ "type": "integer",
115
+ "minimum": 100,
116
+ "maximum": 599
117
+ },
118
+ "schema": {
119
+ "description": "A JSON Schema for the error details.",
120
+ "type": [
121
+ "object",
122
+ "boolean"
123
+ ]
124
+ }
125
+ },
126
+ "additionalProperties": false
127
+ },
128
+ "policy": {
129
+ "description": "The declared behavior of an operation; every member has a default the compiler materializes (task: switch for read, exhaust for command; idempotency none; cache none; maxBodyBytes 1048576; errors.details paths; audience public).",
130
+ "type": "object",
131
+ "properties": {
132
+ "task": {
133
+ "enum": [
134
+ "switch",
135
+ "exhaust",
136
+ "concat",
137
+ "parallel"
138
+ ]
139
+ },
140
+ "idempotency": {
141
+ "description": "A read must be none (or absent).",
142
+ "enum": [
143
+ "none",
144
+ "optional",
145
+ "required"
146
+ ]
147
+ },
148
+ "revision": {
149
+ "description": "Where the revision the command asserts lives: \"input:\" followed by an RFC 6901 pointer.",
150
+ "type": "string",
151
+ "pattern": "^input:(?:/(?:[^/~]|~[01])*)*$"
152
+ },
153
+ "cache": {
154
+ "enum": [
155
+ "none",
156
+ "revision"
157
+ ]
158
+ },
159
+ "limits": {
160
+ "type": "object",
161
+ "properties": {
162
+ "maxBodyBytes": {
163
+ "type": "integer",
164
+ "minimum": 1
165
+ }
166
+ },
167
+ "additionalProperties": false
168
+ },
169
+ "errors": {
170
+ "type": "object",
171
+ "properties": {
172
+ "details": {
173
+ "enum": [
174
+ "none",
175
+ "paths",
176
+ "full"
177
+ ]
178
+ }
179
+ },
180
+ "additionalProperties": false
181
+ },
182
+ "retry": {
183
+ "type": "object",
184
+ "properties": {
185
+ "max": {
186
+ "type": "integer",
187
+ "minimum": 0
188
+ },
189
+ "on": {
190
+ "type": "array",
191
+ "items": {
192
+ "type": "string",
193
+ "minLength": 1
194
+ }
195
+ }
196
+ },
197
+ "required": [
198
+ "max",
199
+ "on"
200
+ ],
201
+ "additionalProperties": false
202
+ },
203
+ "stream": {
204
+ "description": "The stream policy of a subscribe operation (refused on any other kind): resume snapshot|replay (default snapshot), heartbeatMs ≥ 1000 (default 15000), maxPatchBytes a positive integer.",
205
+ "type": "object",
206
+ "properties": {
207
+ "resume": {
208
+ "enum": [
209
+ "snapshot",
210
+ "replay"
211
+ ]
212
+ },
213
+ "heartbeatMs": {
214
+ "type": "integer",
215
+ "minimum": 1000
216
+ },
217
+ "maxPatchBytes": {
218
+ "type": "integer",
219
+ "minimum": 1
220
+ }
221
+ },
222
+ "additionalProperties": false
223
+ },
224
+ "audience": {
225
+ "description": "Who may see the operation: public (the default) or server — a server operation is kept out of the public projection and every projection built on it.",
226
+ "enum": [
227
+ "public",
228
+ "server"
229
+ ]
230
+ }
231
+ },
232
+ "additionalProperties": false
233
+ },
234
+ "http": {
235
+ "description": "The REST binding: an uppercase method, a path template (RFC 6570 level-1 {name} or :name segments, canonicalized to {name}), member locations, an optional whole-body member, the success status (default 200) and media type (default application/json; anything else marks the operation opaque).",
236
+ "type": "object",
237
+ "properties": {
238
+ "method": {
239
+ "enum": [
240
+ "GET",
241
+ "HEAD",
242
+ "POST",
243
+ "PUT",
244
+ "PATCH",
245
+ "DELETE",
246
+ "OPTIONS"
247
+ ]
248
+ },
249
+ "path": {
250
+ "description": "A leading /, non-empty segments; a variable is a whole segment {name} or :name; no wildcards, no RFC 6570 operators or modifiers.",
251
+ "type": "string",
252
+ "pattern": "^/$|^(?:/(?:[^/{}:*?#\\s\\x00-\\x1F\\x7F]+|\\{[A-Za-z_][A-Za-z0-9_]*\\}|:[A-Za-z_][A-Za-z0-9_]*))+$"
253
+ },
254
+ "in": {
255
+ "description": "Input member → where it travels; path variables are always path, the rest defaults to query for a read and body for a command.",
256
+ "type": "object",
257
+ "additionalProperties": {
258
+ "enum": [
259
+ "path",
260
+ "query",
261
+ "header",
262
+ "body"
263
+ ]
264
+ }
265
+ },
266
+ "body": {
267
+ "description": "The input member whose value IS the request body (for a raw array or scalar body).",
268
+ "type": "string"
269
+ },
270
+ "status": {
271
+ "type": "integer",
272
+ "minimum": 200,
273
+ "maximum": 299
274
+ },
275
+ "media": {
276
+ "type": "string",
277
+ "pattern": "^[A-Za-z0-9!#$&^_.+-]+/[A-Za-z0-9!#$&^_.+-]+(?:\\s*;.*)?$"
278
+ }
279
+ },
280
+ "required": [
281
+ "method",
282
+ "path"
283
+ ],
284
+ "additionalProperties": false
285
+ }
286
+ }
287
+ }
@@ -0,0 +1,287 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://jarenjs.dev/schemas/jaren-contract/0.1",
4
+ "title": "Jaren operation contract 0.1",
5
+ "description": "Structural grammar of a jaren-contract document (see packages/contract/docs/CONTRACT-FORMAT.md): the operations two Jaren ends may exchange, each a JSON-in/JSON-out operation with a kind, an input object schema, an output schema, declared errors, a policy and an HTTP binding. Every vocabulary here is CLOSED — an unknown member of the root, an operation, policy, http or an error declaration is refused, because a silently ignored policy is a behavior bug. Embedded schemas (input, output, error schemas, $defs) are typed structurally only; the compiler is the authority for $ref resolution, the effective input type, the cross-member rules (path variables against input.properties, location conflicts, a GET body) and route-shape uniqueness.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$contract": {
9
+ "description": "The contract format version.",
10
+ "const": "0.1"
11
+ },
12
+ "id": {
13
+ "description": "An optional identifier for the contract (a tool prefix, a file name).",
14
+ "type": "string",
15
+ "pattern": "^[A-Za-z_][A-Za-z0-9_-]*$"
16
+ },
17
+ "version": {
18
+ "description": "The consumer's version string; a compatibility claim, unrelated to the revision hash.",
19
+ "type": "string",
20
+ "minLength": 1
21
+ },
22
+ "compat": {
23
+ "description": "Peer version strings this contract accepts.",
24
+ "type": "array",
25
+ "items": {
26
+ "type": "string",
27
+ "minLength": 1
28
+ }
29
+ },
30
+ "$defs": {
31
+ "description": "Named schemas the operations reference as #/$defs/<name>.",
32
+ "type": "object",
33
+ "additionalProperties": {
34
+ "description": "A JSON Schema.",
35
+ "type": [
36
+ "object",
37
+ "boolean"
38
+ ]
39
+ }
40
+ },
41
+ "operations": {
42
+ "description": "Operation id (dotted lowercase words) to operation declaration.",
43
+ "type": "object",
44
+ "minProperties": 1,
45
+ "propertyNames": {
46
+ "pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)*$"
47
+ },
48
+ "additionalProperties": {
49
+ "$ref": "#/$defs/operation"
50
+ }
51
+ }
52
+ },
53
+ "required": [
54
+ "$contract",
55
+ "operations"
56
+ ],
57
+ "additionalProperties": false,
58
+ "$defs": {
59
+ "operation": {
60
+ "description": "One operation: its kind, the input object schema whose top-level members the http binding places, the output schema, declared errors, the behavior policy and the http binding (canonical POST /<id> when absent).",
61
+ "type": "object",
62
+ "properties": {
63
+ "kind": {
64
+ "description": "'read', 'command' or 'subscribe' — a subscribe operation's snapshot and { patch, seq } emissions travel the stream binding (SSE over http, push frames over port); the compiler forces its binding to GET + text/event-stream.",
65
+ "enum": [
66
+ "read",
67
+ "command",
68
+ "subscribe"
69
+ ]
70
+ },
71
+ "input": {
72
+ "description": "A JSON Schema whose effective type is object (the compiler follows $ref to check).",
73
+ "type": "object"
74
+ },
75
+ "output": {
76
+ "description": "A JSON Schema; true accepts anything (an opaque or unspecified body).",
77
+ "type": [
78
+ "object",
79
+ "boolean"
80
+ ]
81
+ },
82
+ "errors": {
83
+ "description": "Declared operation errors by code.",
84
+ "type": "object",
85
+ "propertyNames": {
86
+ "pattern": "^[a-z][a-z0-9-]*$"
87
+ },
88
+ "additionalProperties": {
89
+ "$ref": "#/$defs/errorDeclaration"
90
+ }
91
+ },
92
+ "policy": {
93
+ "$ref": "#/$defs/policy"
94
+ },
95
+ "http": {
96
+ "$ref": "#/$defs/http"
97
+ },
98
+ "doc": {
99
+ "description": "Human-readable documentation for projections.",
100
+ "type": "string"
101
+ }
102
+ },
103
+ "required": [
104
+ "kind",
105
+ "output"
106
+ ],
107
+ "additionalProperties": false
108
+ },
109
+ "errorDeclaration": {
110
+ "description": "One declared error: the HTTP status a binding that carries statuses uses (default 400) and an optional schema for its details.",
111
+ "type": "object",
112
+ "properties": {
113
+ "status": {
114
+ "type": "integer",
115
+ "minimum": 100,
116
+ "maximum": 599
117
+ },
118
+ "schema": {
119
+ "description": "A JSON Schema for the error details.",
120
+ "type": [
121
+ "object",
122
+ "boolean"
123
+ ]
124
+ }
125
+ },
126
+ "additionalProperties": false
127
+ },
128
+ "policy": {
129
+ "description": "The declared behavior of an operation; every member has a default the compiler materializes (task: switch for read, exhaust for command; idempotency none; cache none; maxBodyBytes 1048576; errors.details paths; audience public).",
130
+ "type": "object",
131
+ "properties": {
132
+ "task": {
133
+ "enum": [
134
+ "switch",
135
+ "exhaust",
136
+ "concat",
137
+ "parallel"
138
+ ]
139
+ },
140
+ "idempotency": {
141
+ "description": "A read must be none (or absent).",
142
+ "enum": [
143
+ "none",
144
+ "optional",
145
+ "required"
146
+ ]
147
+ },
148
+ "revision": {
149
+ "description": "Where the revision the command asserts lives: \"input:\" followed by an RFC 6901 pointer.",
150
+ "type": "string",
151
+ "pattern": "^input:(?:/(?:[^/~]|~[01])*)*$"
152
+ },
153
+ "cache": {
154
+ "enum": [
155
+ "none",
156
+ "revision"
157
+ ]
158
+ },
159
+ "limits": {
160
+ "type": "object",
161
+ "properties": {
162
+ "maxBodyBytes": {
163
+ "type": "integer",
164
+ "minimum": 1
165
+ }
166
+ },
167
+ "additionalProperties": false
168
+ },
169
+ "errors": {
170
+ "type": "object",
171
+ "properties": {
172
+ "details": {
173
+ "enum": [
174
+ "none",
175
+ "paths",
176
+ "full"
177
+ ]
178
+ }
179
+ },
180
+ "additionalProperties": false
181
+ },
182
+ "retry": {
183
+ "type": "object",
184
+ "properties": {
185
+ "max": {
186
+ "type": "integer",
187
+ "minimum": 0
188
+ },
189
+ "on": {
190
+ "type": "array",
191
+ "items": {
192
+ "type": "string",
193
+ "minLength": 1
194
+ }
195
+ }
196
+ },
197
+ "required": [
198
+ "max",
199
+ "on"
200
+ ],
201
+ "additionalProperties": false
202
+ },
203
+ "stream": {
204
+ "description": "The stream policy of a subscribe operation (refused on any other kind): resume snapshot|replay (default snapshot), heartbeatMs ≥ 1000 (default 15000), maxPatchBytes a positive integer.",
205
+ "type": "object",
206
+ "properties": {
207
+ "resume": {
208
+ "enum": [
209
+ "snapshot",
210
+ "replay"
211
+ ]
212
+ },
213
+ "heartbeatMs": {
214
+ "type": "integer",
215
+ "minimum": 1000
216
+ },
217
+ "maxPatchBytes": {
218
+ "type": "integer",
219
+ "minimum": 1
220
+ }
221
+ },
222
+ "additionalProperties": false
223
+ },
224
+ "audience": {
225
+ "description": "Who may see the operation: public (the default) or server — a server operation is kept out of the public projection and every projection built on it.",
226
+ "enum": [
227
+ "public",
228
+ "server"
229
+ ]
230
+ }
231
+ },
232
+ "additionalProperties": false
233
+ },
234
+ "http": {
235
+ "description": "The REST binding: an uppercase method, a path template (RFC 6570 level-1 {name} or :name segments, canonicalized to {name}), member locations, an optional whole-body member, the success status (default 200) and media type (default application/json; anything else marks the operation opaque).",
236
+ "type": "object",
237
+ "properties": {
238
+ "method": {
239
+ "enum": [
240
+ "GET",
241
+ "HEAD",
242
+ "POST",
243
+ "PUT",
244
+ "PATCH",
245
+ "DELETE",
246
+ "OPTIONS"
247
+ ]
248
+ },
249
+ "path": {
250
+ "description": "A leading /, non-empty segments; a variable is a whole segment {name} or :name; no wildcards, no RFC 6570 operators or modifiers.",
251
+ "type": "string",
252
+ "pattern": "^/$|^(?:/(?:[^/{}:*?#\\s\\x00-\\x1F\\x7F]+|\\{[A-Za-z_][A-Za-z0-9_]*\\}|:[A-Za-z_][A-Za-z0-9_]*))+$"
253
+ },
254
+ "in": {
255
+ "description": "Input member → where it travels; path variables are always path, the rest defaults to query for a read and body for a command.",
256
+ "type": "object",
257
+ "additionalProperties": {
258
+ "enum": [
259
+ "path",
260
+ "query",
261
+ "header",
262
+ "body"
263
+ ]
264
+ }
265
+ },
266
+ "body": {
267
+ "description": "The input member whose value IS the request body (for a raw array or scalar body).",
268
+ "type": "string"
269
+ },
270
+ "status": {
271
+ "type": "integer",
272
+ "minimum": 200,
273
+ "maximum": 299
274
+ },
275
+ "media": {
276
+ "type": "string",
277
+ "pattern": "^[A-Za-z0-9!#$&^_.+-]+/[A-Za-z0-9!#$&^_.+-]+(?:\\s*;.*)?$"
278
+ }
279
+ },
280
+ "required": [
281
+ "method",
282
+ "path"
283
+ ],
284
+ "additionalProperties": false
285
+ }
286
+ }
287
+ }
@@ -0,0 +1,109 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The WHATWG adapter: `toFetchHandler(dispatcher)` puts the
4
+ * dispatcher behind `(Request) => Promise<Response>` — the lingua franca
5
+ * of Bun.serve, Deno, service workers and Cloudflare-style hosts.
6
+ * Dependency-free and structurally typed: it needs only the platform's
7
+ * `Request`, `Response` and `Headers`.
8
+ *
9
+ * The adapter reads the body only when the matched operation can carry
10
+ * one (`text()` for a JSON operation, `arrayBuffer()` for an opaque one),
11
+ * refuses a declared `content-length` above the operation's limit
12
+ * BEFORE reading (the dispatcher answers the 413 from the header), never
13
+ * reads an unmatched request's body, and hands everything else to
14
+ * `dispatch`. `Headers` combines repeated field lines with `, `, so a
15
+ * repeated scalar header member is invisible here (the node adapter sees
16
+ * distinct lines); `text()` decodes with replacement, so invalid UTF-8
17
+ * reaches the JSON parser as U+FFFD (the node adapter hands bytes over
18
+ * and the dispatcher's strict decode answers `JC2005`).
19
+ */
20
+
21
+ /**
22
+ * @typedef {import('../http/serve.js').HttpDispatcher} HttpDispatcher
23
+ */
24
+
25
+ /**
26
+ * Whether the request could carry a body the operation reads.
27
+ * @param {string} method
28
+ * @returns {boolean}
29
+ */
30
+ function mayCarryBody(method) {
31
+ return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';
32
+ }
33
+
34
+ /**
35
+ * Put a dispatcher behind the WHATWG request/response pair.
36
+ * @param {HttpDispatcher} dispatcher
37
+ * @returns {(request: Request) => Promise<Response>}
38
+ * @example
39
+ * Bun.serve({ fetch: toFetchHandler(serveHttp(contract, handlers)) });
40
+ */
41
+ export function toFetchHandler(dispatcher) {
42
+ if (dispatcher === null || typeof dispatcher !== 'object' || typeof dispatcher.dispatch !== 'function') {
43
+ throw new TypeError('toFetchHandler: the argument must be a dispatcher from serveHttp');
44
+ }
45
+ const contract = dispatcher.contract;
46
+ const head = dispatcher.capabilities.head;
47
+
48
+ return async function fetchHandler(request) {
49
+ const method = request.method;
50
+ const url = new URL(request.url);
51
+ /** @type {Record<string, string>} */
52
+ const headers = {};
53
+ request.headers.forEach((value, name) => { headers[name] = value; });
54
+
55
+ let body = null;
56
+ if (mayCarryBody(method) && request.body !== null) {
57
+ // the matched operation decides how the body is read and how big it
58
+ // may be; a declared content-length over the limit is never read
59
+ const hit = contract.match(method, url.pathname)
60
+ ?? (method === 'HEAD' && head ? contract.match('GET', url.pathname) : null);
61
+ if (hit !== null) {
62
+ const declared = Number(headers['content-length']);
63
+ if (!(Number.isFinite(declared) && declared > hit.op.policy.limits.maxBodyBytes)) {
64
+ body = hit.op.http.opaque ? new Uint8Array(await request.arrayBuffer()) : await request.text();
65
+ }
66
+ }
67
+ }
68
+
69
+ const response = await dispatcher.dispatch({
70
+ method, url: url.pathname + url.search, headers, body, signal: request.signal,
71
+ });
72
+ if (typeof response.stream === 'function') {
73
+ // an SSE response: the pump writes into a ReadableStream; a
74
+ // consumer cancel stops the subscription (the request signal
75
+ // covers the disconnect path too)
76
+ const pump = response.stream;
77
+ const encoder = new TextEncoder();
78
+ /** @type {(() => void) | null} */
79
+ let stop = null;
80
+ const streamBody = new ReadableStream({
81
+ start(controller) {
82
+ stop = pump({
83
+ write: (chunk) => {
84
+ try {
85
+ controller.enqueue(encoder.encode(chunk));
86
+ }
87
+ catch {
88
+ // a closed stream drops the write; the abort path stops the pump
89
+ }
90
+ },
91
+ end: () => {
92
+ try {
93
+ controller.close();
94
+ }
95
+ catch {
96
+ // already closed
97
+ }
98
+ },
99
+ });
100
+ },
101
+ cancel() {
102
+ if (stop !== null) stop();
103
+ },
104
+ });
105
+ return new Response(streamBody, { status: response.status, headers: response.headers });
106
+ }
107
+ return new Response(/** @type {BodyInit | null} */ (response.body), { status: response.status, headers: response.headers });
108
+ };
109
+ }