@plurnk/plurnk-schemes-http 1.3.12 → 1.4.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/.env.defaults +13 -11
- package/README.md +70 -23
- package/SPEC.md +461 -124
- package/dist/Browser.d.ts +2 -11
- package/dist/Browser.d.ts.map +1 -1
- package/dist/Browser.js +91 -69
- package/dist/Browser.js.map +1 -1
- package/dist/ContentType.d.ts +2 -0
- package/dist/ContentType.d.ts.map +1 -0
- package/dist/ContentType.js +15 -0
- package/dist/ContentType.js.map +1 -0
- package/dist/ErrorDetail.d.ts +6 -0
- package/dist/ErrorDetail.d.ts.map +1 -0
- package/dist/ErrorDetail.js +19 -0
- package/dist/ErrorDetail.js.map +1 -0
- package/dist/Guard.d.ts +1 -1
- package/dist/Guard.d.ts.map +1 -1
- package/dist/Guard.js +94 -53
- package/dist/Guard.js.map +1 -1
- package/dist/Http.d.ts.map +1 -1
- package/dist/Http.js +782 -273
- package/dist/Http.js.map +1 -1
- package/dist/WebFetcher.d.ts +38 -2
- package/dist/WebFetcher.d.ts.map +1 -1
- package/dist/WebFetcher.js +189 -48
- package/dist/WebFetcher.js.map +1 -1
- package/dist/Ws.d.ts +5 -1
- package/dist/Ws.d.ts.map +1 -1
- package/dist/Ws.js +431 -93
- package/dist/Ws.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -9
- package/dist/index.js.map +1 -1
- package/docs/http.md +95 -53
- package/docs/wss.md +39 -43
- package/package.json +14 -11
- package/scripts/install-browser.mjs +0 -19
package/SPEC.md
CHANGED
|
@@ -1,130 +1,467 @@
|
|
|
1
1
|
# plurnk-schemes-http — Specification
|
|
2
2
|
|
|
3
|
-
`http(s)://` scheme
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
This package owns the `http(s)://` request/response scheme, the `ws(s)://`
|
|
4
|
+
full-duplex scheme, plus automatic entry-acquisition and browser-rendering
|
|
5
|
+
foundations. Both handlers implement the DB-free `SchemeCtx` author contract.
|
|
6
|
+
|
|
7
|
+
## §http-manifest §1 HTTP manifest
|
|
8
|
+
|
|
9
|
+
| Field | Value |
|
|
10
|
+
| --------------- | ----------------------------- |
|
|
11
|
+
| Registered name | `http` (`https` routes to it) |
|
|
12
|
+
| Category | `data` |
|
|
13
|
+
| Writers | `model`, `client` |
|
|
14
|
+
| Volatile | `true` |
|
|
15
|
+
| Model-visible | `true` |
|
|
16
|
+
| Requires web | `true` |
|
|
17
|
+
| Default channel | `body` |
|
|
18
|
+
|
|
19
|
+
| Channel | Seed type | Meaning |
|
|
20
|
+
| -------- | -------------------------- | --------------------------------------------------------------- |
|
|
21
|
+
| `body` | `application/octet-stream` | Source text, derived Unicode, binary marker, SSE data, or HTML |
|
|
22
|
+
| `header` | `text/plain` | Status line, response headers, and package acquisition metadata |
|
|
23
|
+
| `html` | `text/html` | Faithful HTML used to produce the readable body |
|
|
24
|
+
|
|
25
|
+
`package.json#plurnk.schemes` registers `http` through the default export and
|
|
26
|
+
`wss` through `Ws`.
|
|
27
|
+
|
|
28
|
+
Routing is not identity. `NetworkAddress` {§network-address} retains the exact
|
|
29
|
+
addressed protocol and stores
|
|
30
|
+
`/<host>[:<non-default-port>]<path>[?<serialized-query>]`. Query ordering,
|
|
31
|
+
duplicate names, and an explicit empty `?` remain significant. The fragment is
|
|
32
|
+
a Plurnk channel selector and never enters network identity or transport.
|
|
33
|
+
Request metadata affects transport but not identity. URL userinfo is rejected;
|
|
34
|
+
neither credentials nor metadata are reconstructed from `raw`. Within storage
|
|
35
|
+
pathnames, `%28` and `%29` canonicalize to literal parentheses and renderers
|
|
36
|
+
encode them again.
|
|
37
|
+
|
|
38
|
+
## §op-surface §2 HTTP operation surface
|
|
39
|
+
|
|
40
|
+
| Operation | Remote action | Contract |
|
|
41
|
+
| --------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
42
|
+
| Unscoped `READ(url)` | GET unless a fresh GET copy is usable | Seed, subscribe, materialize every channel, publish the selected channel, and return `102` |
|
|
43
|
+
| Scoped `READ(url)<scope>` | None | Delegate selected-channel presence and scope projection to universal READ; never acquire |
|
|
44
|
+
| `FIND(url)` / matcher `READ(url)` | GET only when acquisition is required | Prepare an exact entry, then use universal query, matcher, weighting, and pagination |
|
|
45
|
+
| `FIND(pattern-url)` | None | Query already-materialized web entries; a path pattern does not discover the remote web |
|
|
46
|
+
| `SEND[200](url):body:` | POST | Stream and persist the response under the addressed URL |
|
|
47
|
+
| `EDIT(url):body:` | PUT | Replace the whole remote resource; a line marker is invalid |
|
|
48
|
+
| `KILL(url)` | DELETE | Delete the remote resource and stream its response |
|
|
49
|
+
| `SEND[410](url)` | None | Delete the local stored entry |
|
|
50
|
+
| `SEND[499](url)` | Cancellation is engine-routed | The routed subscription handle aborts acquisition; scheme dispatch itself is a `200` no-op |
|
|
51
|
+
|
|
52
|
+
Every direct network method uses the same streaming path. Request
|
|
53
|
+
headers are ordered target metadata: one trailing `{Key: value}` block per
|
|
54
|
+
header. The loop `SEND[code]` is never the remote HTTP status; remote status and
|
|
55
|
+
headers are persisted in `header`.
|
|
56
|
+
Exact-versus-pattern FIND preparation uses the shared
|
|
57
|
+
`PathSyntax.hasGlob` classifier {§path-glob}; HTTP owns no reduced
|
|
58
|
+
path-pattern grammar.
|
|
59
|
+
|
|
60
|
+
## §http-lifecycle §3 Acquisition, materialization, and query lifecycle
|
|
61
|
+
|
|
62
|
+
```mermaid
|
|
63
|
+
flowchart TD
|
|
64
|
+
op{"HTTP operation"}
|
|
65
|
+
|
|
66
|
+
op -->|scoped READ| selected["Universal READ selects the channel,<br/>requires it to exist, and applies scope"]
|
|
67
|
+
|
|
68
|
+
op -->|FIND or matcher READ| pattern{"Path pattern?"}
|
|
69
|
+
pattern -->|yes| query["Universal catalog, matcher,<br/>weight, and pagination"]
|
|
70
|
+
pattern -->|no| usable{"Usable exact entry?"}
|
|
71
|
+
usable -->|yes| query
|
|
72
|
+
usable -->|no| prefetch["WebFetcher GET"]
|
|
73
|
+
|
|
74
|
+
op -->|unscoped READ| fresh{"Fresh stored GET?"}
|
|
75
|
+
fresh -->|yes| replay["Seed → open → replay stored channels → close"]
|
|
76
|
+
fresh -->|no| stream["Seed entry → open subscription"]
|
|
77
|
+
op -->|POST, PUT, or DELETE| stream
|
|
78
|
+
|
|
79
|
+
prefetch --> guard["Guard.fetch checks the target<br/>and every followed redirect"]
|
|
80
|
+
guard -->|refused or unavailable| dead
|
|
81
|
+
guard -->|response| prefetchType{"Accepted response?"}
|
|
82
|
+
prefetchType -->|2xx HTML| prefetchHtml["Shared HTML materialization;<br/>render lazily only if absent"]
|
|
83
|
+
prefetchType -->|2xx non-HTML| prefetchClass{"Configured media class"}
|
|
84
|
+
prefetchType -->|dead| dead["404 not-materialized"]
|
|
85
|
+
prefetchClass -->|text| prefetchText["Materialize complete UTF-8 text"]
|
|
86
|
+
prefetchClass -->|binary| prefetchBinary["Bounded readable-byte projection"]
|
|
87
|
+
prefetchHtml -->|present, including empty| materialize["Write entry"]
|
|
88
|
+
prefetchHtml -->|absent| noProjection["422 no-readable-projection"]
|
|
89
|
+
prefetchText --> materialize
|
|
90
|
+
prefetchBinary -->|present, including empty| materialize
|
|
91
|
+
prefetchBinary -->|absent| noProjection
|
|
92
|
+
prefetchBinary -->|input ceiling| inputLimit["413 projection-input-limit"]
|
|
93
|
+
materialize --> query
|
|
94
|
+
|
|
95
|
+
stream --> response{"Response path"}
|
|
96
|
+
response -->|304 with stored GET| restore["Restore stored channels and refresh stamp"]
|
|
97
|
+
response -->|GET + HTML| render["Browser → rendered HTML"]
|
|
98
|
+
response -->|GET + SSE| acquired["Persist header → READ 102"]
|
|
99
|
+
acquired --> events["Detached event data → body chunks"]
|
|
100
|
+
response -->|other| representation{"Body representation"}
|
|
101
|
+
representation -->|none| empty["Header only"]
|
|
102
|
+
representation -->|configured text| text["UTF-8 response chunks → body"]
|
|
103
|
+
representation -->|configured binary| binaryProjection["Bounded readable-byte projection"]
|
|
104
|
+
binaryProjection -->|present, including empty| projected["Derived Unicode → body"]
|
|
105
|
+
binaryProjection -->|absent| binary["Cancel bytes → typed empty marker + 415"]
|
|
106
|
+
binaryProjection -->|input ceiling| binaryLimit["Typed empty marker + 413"]
|
|
107
|
+
restore --> close["Persist/publish → close exact result"]
|
|
108
|
+
render --> directHtml["Shared HTML materialization"]
|
|
109
|
+
directHtml -->|present, including empty| close
|
|
110
|
+
directHtml -->|absent| noProjection
|
|
111
|
+
events --> close
|
|
112
|
+
empty --> close
|
|
113
|
+
text --> close
|
|
114
|
+
projected --> close
|
|
115
|
+
binary --> close
|
|
116
|
+
binaryLimit --> close
|
|
22
117
|
```
|
|
23
118
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
| Consumer / response | `body` | Auxiliary materialization | Completion |
|
|
120
|
+
| -------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------- |
|
|
121
|
+
| Direct GET + HTML | Present projection of the rendered DOM, including `""` | Rendered DOM in `html`; render/projection metadata | `102`; absent projection is `422` |
|
|
122
|
+
| Direct GET + `text/event-stream` | One `data` value plus newline per `text/plain` chunk | Initial response in `header` | `102` after header; origin close settles |
|
|
123
|
+
| Direct request + no response body | Empty seed | Response and package metadata in `header` | Subscription closes; op is `102` |
|
|
124
|
+
| Direct request + configured textual type | Incremental UTF-8 text under the declared type | Response and package metadata in `header` | Response-body end closes the stream |
|
|
125
|
+
| Direct request + readable binary type | Derived Unicode under the projection output type | Origin type and projection identity in `header` | `102`; bytes are never durable |
|
|
126
|
+
| Direct request + unreadable binary/unknown | Empty marker; declared type or `application/octet-stream` | Response and package metadata in `header` | `415 binary-response-unsupported` |
|
|
127
|
+
| Direct binary input exceeds configured bound | Empty marker under the source type | Limit evidence in the terminal Problem | `413 projection-input-limit` |
|
|
128
|
+
| Exact WebFetcher + non-empty 2xx HTML | Present projection of server HTML or the lazy rendered fallback | Selected HTML plus origin/projection metadata | Materialize; absent projection is `422` |
|
|
129
|
+
| Exact WebFetcher + configured text | Complete UTF-8 response | Byte response in `header` | Materialize, then universal query |
|
|
130
|
+
| Exact WebFetcher + readable binary | Derived Unicode under the projection output type | Origin type and projection identity in `header` | Materialize; absent projection is `422` |
|
|
131
|
+
|
|
132
|
+
A fragmentless direct operation publishes only `body`. An explicit fragment
|
|
133
|
+
publishes that named channel. Every acquired channel remains durable even when
|
|
134
|
+
it is not published to the requesting loop. FIND returns standard JSON metadata
|
|
135
|
+
and match coordinates; matcher READ returns selected content and navigation
|
|
136
|
+
evidence.
|
|
137
|
+
|
|
138
|
+
### §http-text-decoding Text response decoding
|
|
139
|
+
|
|
140
|
+
| Surface | Contract |
|
|
141
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
142
|
+
| Response media type | WHATWG `MIMEType` essence; absent or unparseable metadata becomes `application/octet-stream` |
|
|
143
|
+
| Direct textual response | Incremental replacement-mode UTF-8 through `TextDecoder`, preserving response backpressure |
|
|
144
|
+
| WebFetcher textual response | Fetch `Response.text()` UTF-8 decoding; buffering does not select a different character encoding |
|
|
145
|
+
| `charset` parameter | Preserve in `header` as origin evidence; it does not replace Fetch text decoding |
|
|
146
|
+
| JSON and XML textual families | Use the same HTTP byte-to-string rule; format projections consume the resulting Unicode string |
|
|
147
|
+
| Direct HTML GET | Browser navigation owns the separate HTML document encoding algorithm |
|
|
148
|
+
| Server-sent events | UTF-8 only under the HTML event-stream standard |
|
|
149
|
+
| Malformed UTF-8 | Preserve the Encoding Standard's replacement-character behavior |
|
|
150
|
+
|
|
151
|
+
This decoder boundary remains text normalization, not a media-format processor.
|
|
152
|
+
A configured binary type bypasses it and enters the mimetype family's bounded
|
|
153
|
+
readable-byte projection {§mimetype-binary-input}; raw bytes never become a
|
|
154
|
+
durable channel.
|
|
155
|
+
|
|
156
|
+
### §html-materialization Readable materialization
|
|
157
|
+
|
|
158
|
+
`WebFetcher.materialize` is the shared readable-representation seam for exact
|
|
159
|
+
query preparation and executor entry acquisition. Direct GET uses the same
|
|
160
|
+
HTML and binary projection results while retaining incremental text streaming.
|
|
161
|
+
|
|
162
|
+
| Input or event | Action | Result |
|
|
163
|
+
| ------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------- |
|
|
164
|
+
| Configured non-HTML text | Decode with Fetch UTF-8 | Original representation in `body` |
|
|
165
|
+
| Configured binary with reader | Apply the bounded byte projection | Derived Unicode plus source type and identity |
|
|
166
|
+
| Configured binary without reader | Cancel without retaining bytes | `null`; the consumer applies its absence policy |
|
|
167
|
+
| Binary input exceeds the common bound | Cancel and preserve the typed cause | `WebMaterializationError` caused by `ProjectionInputLimitError` |
|
|
168
|
+
| HTML with a present projection | Stop | Projection in `body`; selected HTML in `html` |
|
|
169
|
+
| HTML with an absent projection | Render once when the caller supplies a renderer | Rendered projection in `body`; rendered `html` |
|
|
170
|
+
| HTML still absent after fallback | Stop | `null`; the consumer applies its absence policy |
|
|
171
|
+
| Projection implementation throws | Stop and preserve the cause | `WebMaterializationError` with stage `projection` |
|
|
172
|
+
| Lazy renderer throws | Stop before another projection attempt | `WebMaterializationError` with stage `render` |
|
|
173
|
+
|
|
174
|
+
A projection object is present even when its content is `""`; only `null`
|
|
175
|
+
denotes absence. A materialization exception retains its original `cause`; it
|
|
176
|
+
never enters the absence channel.
|
|
177
|
+
|
|
178
|
+
## §http-status §4 HTTP status mapping
|
|
179
|
+
|
|
180
|
+
| Outcome | Operation status |
|
|
181
|
+
| ---------------------------------------------------------- | ------------------------------------------------ |
|
|
182
|
+
| Scoped READ | Exact universal selected-channel READ result |
|
|
183
|
+
| Exact FIND / matcher READ after preparation | Exact universal query result |
|
|
184
|
+
| Exact acquisition returns no WebFetcher value | `404` (`not-materialized`) |
|
|
185
|
+
| Direct HTML or exact preparation has no readable projection | `422` (`no-readable-projection`) |
|
|
186
|
+
| Direct textual, projected-binary, or empty response | `102` |
|
|
187
|
+
| Direct binary response has no readable projection | `415` (`binary-response-unsupported`) |
|
|
188
|
+
| Binary projection input exceeds the configured byte bound | `413` (`projection-input-limit`) |
|
|
189
|
+
| `SEND[410]` | Exact entry-delete result |
|
|
190
|
+
| Routed `SEND[499]` dispatch | `200` |
|
|
191
|
+
| Client-cancelled acquisition | Direct `499` (`cancelled`) |
|
|
192
|
+
| SSE cancellation after acquisition | `102` initial; terminal `499` |
|
|
193
|
+
| SSE parser/transfer failure after acquisition | `102` initial; terminal `502` |
|
|
194
|
+
| Multi-statement HTTP edit batch | `409` (`non-atomic-edit-batch`) |
|
|
195
|
+
| Invalid target, channel, line edit, or URL userinfo | `400` with the corresponding stable Problem kind |
|
|
196
|
+
| Non-corresponding 304 | `502` (`fetch-failed`) |
|
|
197
|
+
| Direct network or acquisition exception | `502` (`fetch-failed`) |
|
|
198
|
+
| Direct or prepared HTML render exception | `502` (`render-failed`) |
|
|
199
|
+
| Direct or prepared projection exception | `500` (`projection-failed`) |
|
|
200
|
+
| Uninterpreted SEND status | `501` (`send-status-unsupported`) |
|
|
201
|
+
|
|
202
|
+
An HTTP error status is still a successfully acquired direct response: its
|
|
203
|
+
status remains in `header` and its body streams normally. WebFetcher instead
|
|
204
|
+
treats a non-2xx response as unmaterializable. Handler failures use RFC 9457
|
|
205
|
+
Problem Details. Caught direct-acquisition diagnostics are bounded by
|
|
206
|
+
`PLURNK_SCHEMES_HTTP_ERROR_DETAIL_LIMIT` in model-facing detail while complete
|
|
207
|
+
errors remain in daemon diagnostics.
|
|
208
|
+
|
|
209
|
+
A direct binary response first asks the installed mimetype family for a bounded
|
|
210
|
+
readable projection. A present result stores only its derived Unicode and
|
|
211
|
+
appends authoritative `x-plurnk-projection-id` evidence after the origin and
|
|
212
|
+
acquisition fields. Absence preserves an empty marker under the source type and
|
|
213
|
+
returns non-retryable `415`; exceeding the input ceiling preserves the same
|
|
214
|
+
marker and returns non-retryable `413` with configured and observed sizes.
|
|
215
|
+
These statuses describe Plurnk's materialization boundary, not the remote HTTP
|
|
216
|
+
outcome—a POST, PUT, or DELETE might already have changed the remote resource.
|
|
217
|
+
Missing or malformed `Content-Type` becomes `application/octet-stream`; the
|
|
218
|
+
handler does not sniff or guess unknown bytes.
|
|
219
|
+
|
|
220
|
+
## §5 Dependencies and configuration
|
|
221
|
+
|
|
222
|
+
| Surface | Runtime contract |
|
|
223
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------- |
|
|
224
|
+
| Platform | Node ≥26 native fetch, streams, abort signals, decoding, DNS, and `WebSocket` |
|
|
225
|
+
| SSE | `eventsource-parser` for bounded WHATWG event-stream framing |
|
|
226
|
+
| Browser rendering | Required lazy-loaded `playwright` client; executable provisioning follows {§browser-provisioning} |
|
|
227
|
+
|
|
228
|
+
### §http-config Operator configuration
|
|
229
|
+
|
|
230
|
+
| Concern | Contract |
|
|
231
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
232
|
+
| Canonical registry | Shipped `.env.defaults`; the daemon assembles it as a set-if-unset floor {§operator-config-env-defaults} |
|
|
233
|
+
| Required values | Missing or invalid required values fail at their owning read; code carries no hidden fallback |
|
|
234
|
+
| Browser method | Exactly one of `launch`, `connect`, `connectOverCDP`, or `disabled` |
|
|
235
|
+
| Method-specific data | Endpoint belongs to connection methods; channel/executable belong to launch and are mutually exclusive |
|
|
236
|
+
| Browser location | Operator-set Playwright location variables remain authoritative |
|
|
237
|
+
|
|
238
|
+
`Http.ready()` verifies the selected browser route before the handler is
|
|
239
|
+
advertised. Disabled rendering is a valid configured route and fails clearly
|
|
240
|
+
if an HTML operation later requires the browser.
|
|
241
|
+
|
|
242
|
+
### §browser-provisioning Browser provisioning
|
|
243
|
+
|
|
244
|
+
| Component or method | Contract |
|
|
245
|
+
| ------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
246
|
+
| Playwright client | Required runtime dependency; lazy import keeps non-render paths from initializing it |
|
|
247
|
+
| `launch` | Requires an operator-provisioned compatible browser, selected by Playwright, channel, or exact path |
|
|
248
|
+
| `connect` | Requires an external Playwright endpoint; no local browser executable |
|
|
249
|
+
| `connectOverCDP` | Requires an external Chromium CDP endpoint; no local browser executable |
|
|
250
|
+
| `disabled` | Requires no browser executable; a render attempt fails clearly |
|
|
251
|
+
|
|
252
|
+
The package has no browser-install lifecycle script and never assigns
|
|
253
|
+
`PLAYWRIGHT_BROWSERS_PATH`. Operators may provision the standard compatible
|
|
254
|
+
Chromium with `npx playwright install chromium`; methods never fall back into
|
|
255
|
+
one another.
|
|
256
|
+
|
|
257
|
+
### §automatic-fetch-check Automatic acquisition URL check
|
|
258
|
+
|
|
259
|
+
`WebFetcher` is the sole caller of `Guard.fetch`. Before automatic byte
|
|
260
|
+
acquisition, it accepts credential-free HTTP(S) targets only when every
|
|
261
|
+
resolved address is ordinary globally reachable unicast, and repeats the check
|
|
262
|
+
before every manually followed redirect. A refused or unresolvable target is
|
|
263
|
+
the ordinary unavailable `null` result.
|
|
264
|
+
|
|
265
|
+
Direct HTTP operations, WebSocket connections, Playwright navigation, browser
|
|
266
|
+
subresources, and Playwright/CDP endpoints do not use this check. Loopback,
|
|
267
|
+
private, and link-local destinations are therefore valid explicit targets.
|
|
268
|
+
|
|
269
|
+
Redirect transitions follow WHATWG Fetch: 301/302 rewrite POST to GET; 303
|
|
270
|
+
rewrites methods other than GET/HEAD to GET; 307/308 preserve method and body.
|
|
271
|
+
A body rewrite removes body headers, cross-origin redirects remove
|
|
272
|
+
`Authorization`, and followed redirect bodies are cancelled. The configured
|
|
273
|
+
hop limit returns the last redirect rather than following beyond the limit.
|
|
274
|
+
Validation-time DNS answers are not pinned to connection-time resolution. This
|
|
275
|
+
check therefore makes no DNS-rebinding, browser-sandbox, or total-egress claim.
|
|
276
|
+
|
|
277
|
+
## §render-lifecycle §6 Render lifecycle
|
|
278
|
+
|
|
279
|
+
| Concern | Contract |
|
|
280
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
281
|
+
| Direct gate | A GET HTML response cancels the byte probe body and navigates through the browser |
|
|
282
|
+
| Prefetch gate | Server HTML is primary; browser rendering is a lazy fallback only when its readable projection is absent |
|
|
283
|
+
| Pool | One warm browser per `Browser`; one atomically acquired context per worker; prefetch uses worker `0` |
|
|
284
|
+
| Navigation | Mobile-emulated by default; `networkidle` with bounded substantive-DOM timeout salvage |
|
|
285
|
+
| Projection | A present result, including `""`, becomes `body`; exact HTML used becomes `html` |
|
|
286
|
+
| Cancellation | The render-owned page close aborts in-flight navigation; an already-aborted render never navigates |
|
|
287
|
+
| Page cleanup | Close each opened page once and await it; preserve its failure alone or aggregate it after a primary failure |
|
|
288
|
+
| Shutdown | Attempt every context and browser close, await all, then aggregate failures under {§handler-lifecycle} |
|
|
289
|
+
|
|
290
|
+
### §host-rewrite Acquisition target rewrite
|
|
291
|
+
|
|
292
|
+
Only acquisition GETs are eligible for host rewriting. A GitHub
|
|
293
|
+
`…/blob/…` address uses the corresponding `raw.githubusercontent.com` source for
|
|
294
|
+
both byte and browser transport. Direct READ, exact FIND, matcher READ, and
|
|
295
|
+
WebFetcher prefetch share that rule. The originally addressed GitHub URL remains
|
|
296
|
+
entry identity. POST, PUT, and DELETE are never retargeted. Rewritten targets
|
|
297
|
+
are checked under {§automatic-fetch-check} only when `WebFetcher` acquires them.
|
|
298
|
+
|
|
299
|
+
### §revalidation GET representation freshness
|
|
300
|
+
|
|
301
|
+
Acquired responses append package-owned `x-plurnk-request-method`,
|
|
302
|
+
`x-plurnk-fetched-at`, and `x-plurnk-cache-variant` fields after origin headers.
|
|
303
|
+
Derived responses then append `x-plurnk-projection-id`. Only package metadata
|
|
304
|
+
after the acquisition stamp is authoritative, so an origin cannot spoof it.
|
|
305
|
+
Plurnk stores one representation per canonical URL rather than a variant set:
|
|
306
|
+
|
|
307
|
+
| Acquisition context | Package variant | Later cache use |
|
|
308
|
+
| -------------------------------------------- | --------------- | --------------- |
|
|
309
|
+
| No explicit target metadata; no `Vary` | `default` | Eligible |
|
|
310
|
+
| Any explicit target metadata | `bypass` | Ineligible |
|
|
311
|
+
| No explicit target metadata; any `Vary` | `bypass` | Ineligible |
|
|
312
|
+
| Stored response lacks authoritative evidence | Marker absent | Ineligible |
|
|
313
|
+
|
|
314
|
+
This conservative selection avoids persisting request values or fabricating a
|
|
315
|
+
multi-variant store. Exact FIND passes target metadata through acquisition but
|
|
316
|
+
does not reuse that response later. A `304` that introduces `Vary` changes the
|
|
317
|
+
restored representation's package marker to `bypass`.
|
|
318
|
+
|
|
319
|
+
Only an eligible GET representation can supply a direct READ's body, TTL stamp,
|
|
320
|
+
conditional validators, or exact-FIND preparation. Completion comes from
|
|
321
|
+
channel lifecycle, not body length: every stored channel must be final (`static`
|
|
322
|
+
after exact materialization or `closed` after a successful stream); `active`,
|
|
323
|
+
`errored`, or unknown state is ineligible. Durable operation evidence and HTTP
|
|
324
|
+
reuse eligibility remain distinct: an acquired response stays in the entry even
|
|
325
|
+
when its origin policy prevents later cache use.
|
|
326
|
+
|
|
327
|
+
| Stored origin policy | Direct READ after acquisition | Exact FIND after acquisition |
|
|
328
|
+
| ------------------------------------- | ------------------------------------------------------ | ---------------------------------------- |
|
|
329
|
+
| `no-store` | Full acquisition without stored validators | Full acquisition |
|
|
330
|
+
| `no-cache` (qualified or unqualified) | Validate when a stored validator exists; else acquire | Full acquisition |
|
|
331
|
+
| Valid `max-age` | Serve only inside origin lifetime and operator ceiling | Reuse only while fresh under both limits |
|
|
332
|
+
| Valid `Expires`, without `max-age` | Same, using the origin expiration lifetime | Same |
|
|
333
|
+
| Invalid or ambiguous explicit expiry | Treat as stale; validate or acquire | Full acquisition |
|
|
334
|
+
| No explicit origin lifetime | Use the operator TTL as Plurnk's heuristic | Reuse inside the operator TTL |
|
|
335
|
+
|
|
336
|
+
The operator ceiling is `PLURNK_SCHEMES_HTTP_TTL_MS`; `0` disables every
|
|
337
|
+
validation-free reuse. Origin age is the greater of the response's `Age` value
|
|
338
|
+
and apparent age from `Date`, plus residence since the authoritative package
|
|
339
|
+
stamp. A representation is fresh only while both origin lifetime and operator
|
|
340
|
+
ceiling permit it. Unknown cache extensions are inert. Stale content is never
|
|
341
|
+
served, so `must-revalidate` requires no separate path.
|
|
342
|
+
|
|
343
|
+
Outside the fresh window, direct READ sends a stored ETag or Last-Modified only
|
|
344
|
+
when that field is singular and syntactically valid. A 304 can restore the
|
|
345
|
+
stored channels only when its validator corresponds to the nominated stored
|
|
346
|
+
representation:
|
|
347
|
+
|
|
348
|
+
| 304 validator | Correspondence requirement |
|
|
349
|
+
| ----------------------------------------------------- | ------------------------------------------- |
|
|
350
|
+
| Strong ETag | Same stored strong ETag |
|
|
351
|
+
| Weak ETag | Same opaque tag under weak comparison |
|
|
352
|
+
| No ETag; Last-Modified | Same valid stored Last-Modified instant |
|
|
353
|
+
| Missing, malformed, unsolicited, or non-corresponding | Invalid acquisition; `502` (`fetch-failed`) |
|
|
354
|
+
|
|
355
|
+
A corresponding 304 restores the stored channels without rendering and
|
|
356
|
+
updates the header under the following ownership rule; any other response
|
|
357
|
+
replaces the channels:
|
|
358
|
+
|
|
359
|
+
| 304 metadata class | Stored-header action |
|
|
360
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
361
|
+
| Present end-to-end origin fields, including cache and validators | Replace prior fields of the same case-insensitive name |
|
|
362
|
+
| Origin fields absent from the 304 | Preserve |
|
|
363
|
+
| `Content-Type`, `Content-Encoding`, `Content-Range`, `Content-Length` | Preserve metadata describing the already-processed body |
|
|
364
|
+
| Package method, acquisition stamp, and variant | Rebuild authoritatively; refresh stamp and variant |
|
|
365
|
+
| Projection identity | Preserve |
|
|
366
|
+
|
|
367
|
+
A 304-provided `Vary`, `no-store`, `no-cache`, expiry, or validator
|
|
368
|
+
therefore governs the next operation without relabeling derived Unicode as a
|
|
369
|
+
different source representation.
|
|
370
|
+
|
|
371
|
+
A stored derived representation is reusable only while its projection identity
|
|
372
|
+
matches the currently installed reader for the origin media type. A mismatch
|
|
373
|
+
invalidates the body, TTL shortcut, and origin validators together: a 304 can
|
|
374
|
+
certify unchanged source bytes, not output from a different projection.
|
|
375
|
+
|
|
376
|
+
POST, PUT, and DELETE responses retain their method marker but cannot satisfy a
|
|
377
|
+
later GET or exact-FIND acquisition. An unmarked authored entry and an eligible
|
|
378
|
+
stored GET remain visible to universal FIND as durable evidence; exact HTTP
|
|
379
|
+
preparation applies the policy above. `SEND[410]` deletes the stored entry.
|
|
380
|
+
|
|
381
|
+
### §sse Server-sent events
|
|
382
|
+
|
|
383
|
+
A direct GET whose response is `text/event-stream` feeds the bounded parser.
|
|
384
|
+
Each event's joined `data` value plus a newline becomes one `text/plain` body
|
|
385
|
+
chunk. Comments and `event`, `id`, and `retry` metadata are not projected.
|
|
386
|
+
The response plus persisted header is the acquisition boundary: READ returns
|
|
387
|
+
`102`, and parsing continues through the retained `StreamSubscription` without
|
|
388
|
+
retaining `SchemeCtx`. Buffer exhaustion and post-acquisition transport failure
|
|
389
|
+
settle that subscription at `502`; cancellation settles it at `499`. The
|
|
390
|
+
handler does not reconnect; events accumulate until the origin closes or the
|
|
391
|
+
operation is cancelled.
|
|
392
|
+
|
|
393
|
+
## §prefetch §7 WebFetcher
|
|
394
|
+
|
|
395
|
+
| Result | Meaning |
|
|
396
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------- |
|
|
397
|
+
| Non-empty 2xx HTML | Server text, MIME type, package-stamped headers, and a lazy browser renderer |
|
|
398
|
+
| Other 2xx body | One unconsumed byte stream, MIME type, package-stamped headers, and cancellation owner |
|
|
399
|
+
| Materialized configured text | Complete Fetch-decoded UTF-8 body |
|
|
400
|
+
| Materialized readable binary | Bounded derived Unicode plus source type, projection identity, and enriched header |
|
|
401
|
+
| Lazy renderer value | Non-empty rendered HTML, or `null` only when rendering yields no HTML |
|
|
402
|
+
| Lazy renderer failure | Rejects with the complete browser failure; materialization preserves stage and cause |
|
|
403
|
+
| Top-level fetch `null` | Refused, unreachable, timed-out, non-2xx, missing body, or empty HTML byte response |
|
|
404
|
+
| Materialization `null` | Empty configured text or no final readable projection |
|
|
405
|
+
| Caller-cancelled acquisition | Rejects with the caller signal's exact reason |
|
|
406
|
+
|
|
407
|
+
Top-level `null` is a liveness value rather than a thrown failure. Caller
|
|
408
|
+
cancellation is not liveness: a pre-aborted caller fails before acquisition, and
|
|
409
|
+
the first abort reason selected between the caller and configured byte-probe
|
|
410
|
+
deadline owns the outcome. The probe deadline remains `null`. WebFetcher owns
|
|
411
|
+
no entry identity, registry selection, or query policy; its consumer supplies
|
|
412
|
+
the projection capability and materializes the returned value. Lazy-render
|
|
413
|
+
exceptions remain distinct from a renderer that honestly returns no HTML. The
|
|
414
|
+
handler lifecycle closes the shared browser.
|
|
415
|
+
|
|
416
|
+
## §ws §8 WebSocket
|
|
417
|
+
|
|
418
|
+
`wss` is a first-class data scheme; `ws` routes to the same handler. WebSocket
|
|
419
|
+
is bidirectional and stateful, not an HTTP content type. It uses `messages` as a
|
|
420
|
+
`text/plain` default channel and the same canonical network address contract
|
|
421
|
+
{§network-address}.
|
|
422
|
+
|
|
423
|
+
### §ws-lifecycle Socket ownership and settlement
|
|
424
|
+
|
|
425
|
+
```mermaid
|
|
426
|
+
stateDiagram-v2
|
|
427
|
+
[*] --> Idle
|
|
428
|
+
Idle --> Claimed: claim canonical workspace address
|
|
429
|
+
Claimed --> Connecting: seed entry, open subscription, construct socket
|
|
430
|
+
Claimed --> Idle: setup or construction failure
|
|
431
|
+
Connecting --> Open: native open; messages active; READ 102
|
|
432
|
+
Connecting --> Settling: pre-open close/error, KILL, cancel, activation failure, or shutdown
|
|
433
|
+
Open --> Open: ordered inbound frame or SEND[200]
|
|
434
|
+
Open --> Settling: closing state, close/error, KILL, cancel, binary frame, persistence failure, or shutdown
|
|
435
|
+
Settling --> Idle: await retained work, close subscription, release claim
|
|
109
436
|
```
|
|
110
437
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
438
|
+
| Operation or event | Contract |
|
|
439
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
440
|
+
| `READ(ws(s)://…)` | Claim, seed/subscribe, construct `CONNECTING`, then return `102` after native `open` plus durable activation |
|
|
441
|
+
| Concurrent duplicate READ | `409` in `claimed`, `connecting`, `open`, or `settling`; cleanup releases the only claim |
|
|
442
|
+
| `SEND[200](ws(s)://…)` | Send only for owner `open` plus native `readyState=OPEN`; absent or non-open owner is `409`; send throw is `502` |
|
|
443
|
+
| `SEND[499](ws(s)://…)` | Engine-routed cancellation closes the owning READ; scheme dispatch returns `200` |
|
|
444
|
+
| `KILL(ws(s)://…)` | Close/cancel the claimed owner; no owner is `404`; an attempted close throw is `502` |
|
|
445
|
+
| Inbound frame after native `open` | Join the owner's persistence chain; the next write begins only after the preceding write succeeds |
|
|
446
|
+
| Binary frame after native `open` | Retain the text prefix, prune the binary and later frames, settle `415 binary-frame-unsupported`, close `1003` |
|
|
447
|
+
| First inbound persistence failure | Retain the successful prefix, prune queued and later frames, and settle with `500 message-persistence-failed` |
|
|
448
|
+
| Socket closes before `open` | Close subscription with `502 connection-failed`; the pending READ returns that exact failure |
|
|
449
|
+
| Socket closes after `open` | Drain the accepted frame prefix; initial READ remains `102`; settle with `200` unless a drained write fails |
|
|
450
|
+
| Failure after `open` | Initial READ remains `102`; persist the exact terminal failure and wake through subscription settlement |
|
|
451
|
+
|
|
452
|
+
The in-instance registry is keyed by workspace, addressed protocol, and
|
|
453
|
+
canonical network pathname. The claim remains registered through terminal
|
|
454
|
+
cleanup, so a new READ cannot overlap an owner's subscription settlement. Every
|
|
455
|
+
terminal path drains retained owner work, closes the transport when necessary,
|
|
456
|
+
closes the durable subscription, then releases the claim. A persistence failure
|
|
457
|
+
found while draining supersedes a graceful terminal result; cancellation and
|
|
458
|
+
transport failures retain their exact result. Handler shutdown requests
|
|
459
|
+
settlement for every remainder, awaits every owner, and aggregates
|
|
460
|
+
transport-close failures under {§handler-lifecycle}.
|
|
461
|
+
|
|
462
|
+
| Transport limit | Current contract |
|
|
463
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
464
|
+
| Payload projection | String event data only into `text/plain`; binary data terminates with Plurnk `415` and WebSocket `1003` |
|
|
465
|
+
| Reconnection | None; READ again after terminal cleanup |
|
|
466
|
+
| Handshake metadata | Default global-WebSocket identity; custom target headers are not applied |
|
|
467
|
+
| Runtime | Node ≥26 |
|