@orkestrel/scaffold 0.0.91 → 0.0.93
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/dist/agents/templates/brief.md +2 -0
- package/dist/bin/main.js +6 -6
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +2 -2
- package/dist/host/agents/orchestration.md +1 -1
- package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +4 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/responsive-layout.md +1 -1
- package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +3 -3
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +1 -1
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +3 -4
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +1 -1
- package/dist/host/agents/templates/brief.md +2 -0
- package/dist/host/agents/transports/cursor.md +1 -1
- package/dist/host/claude/agents/analyst.md +1 -1
- package/dist/host/claude/agents/grok.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +8 -8
- package/dist/host/claude/agents/planner.md +1 -1
- package/dist/host/claude/agents/researcher.md +1 -1
- package/dist/host/claude/agents/scout.md +1 -1
- package/dist/host/claude/rules/names.md +1 -1
- package/dist/host/claude/rules/patterns.md +1 -1
- package/dist/host/claude/rules/quality.md +11 -0
- package/dist/host/claude/rules/styles.md +5 -0
- package/dist/host/claude/rules/tests.md +7 -8
- package/dist/host/claude/rules/writing.md +2 -1
- package/dist/host/codex/agents/researcher.toml +2 -2
- package/dist/host/codex/agents/scout.toml +1 -1
- package/dist/host/dotfiles/oxlintrc.json +168 -0
- package/dist/host/guides/browser.md +483 -392
- package/dist/host/guides/mcp.md +15 -15
- package/dist/host/guides/pool.md +190 -69
- package/dist/host/guides/probe.md +262 -216
- package/dist/host/guides/router.md +1 -1
- package/dist/host/guides/worker.md +43 -7
- package/dist/host/manifest.json +34 -34
- package/dist/host/tests/config.test.ts +46 -3
- package/dist/src/core/index.cjs +6 -6
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js +6 -6
- package/dist/src/core/index.js.map +1 -1
- package/package.json +6 -6
package/dist/host/guides/mcp.md
CHANGED
|
@@ -5374,11 +5374,11 @@ the exact statement:
|
|
|
5374
5374
|
1. **doc ↔ source bijection.** Every `function` / `class` / `const` /
|
|
5375
5375
|
`interface` / `type` row in the `## Surface` tables (the core dispatch
|
|
5376
5376
|
tables and the `### HTTP transport` + `### WebSocket transport` + `### stdio
|
|
5377
|
-
transport` tables) is a real export of the mcp layer (`src/core` or
|
|
5377
|
+
transport` tables) is a real export of the mcp layer (`src/core` or
|
|
5378
5378
|
`src/server`), and every export of either appears as a Surface row —
|
|
5379
5379
|
exhaustive, both directions.
|
|
5380
5380
|
2. **JSON-RPC 2.0 envelope.** A `dispatch` response is always `{ jsonrpc:
|
|
5381
|
-
'2.0', … }` with exactly one of `result` / `error`. The success arm always
|
|
5381
|
+
'2.0', … }` with exactly one of `result` / `error`. The success arm always
|
|
5382
5382
|
carries an `id`, echoing the request's, because a result answers a request and
|
|
5383
5383
|
a request always has one. The error arm is the only arm whose `id` may be
|
|
5384
5384
|
missing, and it is then omitted — the member is absent from the envelope, never
|
|
@@ -5410,7 +5410,7 @@ transport` tables) is a real export of the mcp layer (`src/core` or
|
|
|
5410
5410
|
metadata, run through the same dispatcher, and are projected back unstamped — so
|
|
5411
5411
|
they inherit the modern engine's execution port, cancellation, bounds, and
|
|
5412
5412
|
validation rather than running beside them. `initialize` → `{ protocolVersion,
|
|
5413
|
-
capabilities: { tools: {} }, serverInfo: { name, version } }`, the version negotiated over the
|
|
5413
|
+
capabilities: { tools: {} }, serverInfo: { name, version } }`, the version negotiated over the
|
|
5414
5414
|
legacy subset only: the client's `params.protocolVersion` is echoed when it is a
|
|
5415
5415
|
supported legacy revision, and every other request — the modern `'2026-07-28'`,
|
|
5416
5416
|
an unsupported revision, a non-string, or an absent one — falls back to the
|
|
@@ -5435,7 +5435,7 @@ capabilities: { tools: {} }, serverInfo: { name, version } }`, the version negot
|
|
|
5435
5435
|
into a `success: false` result, the server adds no try/catch: that branch's
|
|
5436
5436
|
`error` maps to `{ content: [{ type: 'text', text: <error> }], isError: true }`;
|
|
5437
5437
|
a valued `success: true` branch maps to `{ content: [{ type: 'text', text:
|
|
5438
|
-
JSON.stringify(value) }], structuredContent: value }`, carrying the value unchanged
|
|
5438
|
+
JSON.stringify(value) }], structuredContent: value }`, carrying the value unchanged
|
|
5439
5439
|
alongside the backwards-compatible text. A value-less success retains the required
|
|
5440
5440
|
empty text block and omits `structuredContent`.
|
|
5441
5441
|
6. **One modern seam, subscriptions included, and `-32601` for anything off it.**
|
|
@@ -5469,7 +5469,7 @@ JSON.stringify(value) }], structuredContent: value }`, carrying the value unchan
|
|
|
5469
5469
|
carries the same stamp. Ending the consumer's producer closes gracefully when the
|
|
5470
5470
|
honoured filter omits `toolsListChanged`, with
|
|
5471
5471
|
`{ resultType: 'complete', _meta: { 'io.modelcontextprotocol/subscriptionId': id,
|
|
5472
|
-
… } }`. A stream honouring the tools family stays open until failure or signal abort.
|
|
5472
|
+
… } }`. A stream honouring the tools family stays open until failure or signal abort.
|
|
5473
5473
|
The request id is only stream identity: a later request does not supersede
|
|
5474
5474
|
an earlier one. The legacy method remains absent and answers `-32601`.
|
|
5475
5475
|
7. **`handle` maps the boundary failures.** A `JSON.parse` throw (malformed
|
|
@@ -5520,7 +5520,7 @@ JSON.stringify(value) }], structuredContent: value }`, carrying the value unchan
|
|
|
5520
5520
|
continuation, or principal — carrying the caught value the wire never sees; the
|
|
5521
5521
|
emitter isolates a listener throw, routing it
|
|
5522
5522
|
to its own `error` handler (the `error` option, surfaced as `(error,
|
|
5523
|
-
event)`, not a domain event) — so a buggy observer can never corrupt a
|
|
5523
|
+
event)`, not a domain event) — so a buggy observer can never corrupt a
|
|
5524
5524
|
dispatch, and a throwing `error` handler neither escapes nor recurses.
|
|
5525
5525
|
11. **doc ↔ source method bijection.** The `## Methods` tables list exactly
|
|
5526
5526
|
the public methods of each behavioral interface — `MCPServerInterface`,
|
|
@@ -5565,7 +5565,7 @@ event)`, not a domain event) — so a buggy observer can never corrupt a
|
|
|
5565
5565
|
own JSON-parse `try`/`catch`) and draws a sharp line: a TRANSPORT-level
|
|
5566
5566
|
failure — malformed JSON (`-32700`) or a parsed value that is not a
|
|
5567
5567
|
JSON-RPC request (`-32600`, narrowed with `parseJSONRPCMessage` + `'method'
|
|
5568
|
-
in request`, no `as`) — is HTTP **400** with a JSON-RPC error body carrying no
|
|
5568
|
+
in request`, no `as`) — is HTTP **400** with a JSON-RPC error body carrying no
|
|
5569
5569
|
`id` member. A legacy dispatch result — success or in-band JSON-RPC error — is
|
|
5570
5570
|
HTTP **200**. A modern result is **400** for `-32020` / `-32021` / `-32022`
|
|
5571
5571
|
/ `-32602`, **404** for `-32601`, and **200** otherwise; every notification
|
|
@@ -5620,7 +5620,7 @@ in request`, no `as`) — is HTTP **400** with a JSON-RPC error body carrying no
|
|
|
5620
5620
|
consumer-provided list.
|
|
5621
5621
|
13. **The client is the modern-only egress mirror (`src/core`).**
|
|
5622
5622
|
`createMCPClient({ transport, identity?, capabilities?, version?, timeout?,
|
|
5623
|
-
on? })` drives a remote server over an injected `MCPMessageTransportInterface`
|
|
5623
|
+
on? })` drives a remote server over an injected `MCPMessageTransportInterface`
|
|
5624
5624
|
(transport-abstract, like the server). `connect()` issues a modern
|
|
5625
5625
|
`server/discover` carrying `_meta` with the offered revision,
|
|
5626
5626
|
client capabilities, and client identity. It intersects the peer's
|
|
@@ -5708,9 +5708,9 @@ on? })` drives a remote server over an injected `MCPMessageTransportInterface`
|
|
|
5708
5708
|
`error` option, not a domain event). Consumers subscribe through `emitter.on`.
|
|
5709
5709
|
15. **The HTTP client transport drives a remote server over `fetch`
|
|
5710
5710
|
(`src/server`).** `createHTTPClientTransport({ url, headers?, fetch?,
|
|
5711
|
-
timeout? })` returns a `MCPMessageTransportInterface` whose `send` POSTs one
|
|
5711
|
+
timeout? })` returns a `MCPMessageTransportInterface` whose `send` POSTs one
|
|
5712
5712
|
JSON-serialized message to `url` with `content-type:
|
|
5713
|
-
application/json` and an `Accept` of both `application/json` and
|
|
5713
|
+
application/json` and an `Accept` of both `application/json` and
|
|
5714
5714
|
`text/event-stream` (plus any `headers`), then decodes the reply and
|
|
5715
5715
|
emits each carried `JSONRPCMessage` on the `message` event: an
|
|
5716
5716
|
`application/json` body is narrowed with `parseJSONRPCMessage`; a
|
|
@@ -5762,7 +5762,7 @@ application/json` and an `Accept` of both `application/json` and
|
|
|
5762
5762
|
(`upgradeRequestPath`) is not `options.path` (default `DEFAULT_MCP_PATH`),
|
|
5763
5763
|
the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not
|
|
5764
5764
|
`13`. Otherwise it claims (returns `true`): `createNodeWebSocket({
|
|
5765
|
-
socket, key, head, protocol })` (server mode → writes the `101` handshake,
|
|
5765
|
+
socket, key, head, protocol })` (server mode → writes the `101` handshake,
|
|
5766
5766
|
selecting the configured subprotocol only when the client's offer contains it,
|
|
5767
5767
|
and sends unmasked frames), wraps it in a `WebSocketServerTransport`, and
|
|
5768
5768
|
pumps — each inbound `JSONRPCMessage` that `isJSONRPCRequest` runs
|
|
@@ -5831,7 +5831,7 @@ socket, key, head, protocol })` (server mode → writes the `101` handshake,
|
|
|
5831
5831
|
(`src/server`).** `createMCPSession({ path?, ttl?, session?, clock?, origin?, keepalive? })`
|
|
5832
5832
|
returns a `MiddlewareHandler<TState>` (`TState extends MCPSessionState`)
|
|
5833
5833
|
that owns its own closure `Map<string, { session: MCPSession; touched:
|
|
5834
|
-
number; version: MCPVersion }>` — no dependency on `@orkestrel/middleware` and no shared
|
|
5834
|
+
number; version: MCPVersion }>` — no dependency on `@orkestrel/middleware` and no shared
|
|
5835
5835
|
session primitive; the store, mint, and validation are all native to
|
|
5836
5836
|
this package. Compose it with `router.use(createMCPSession())` in front
|
|
5837
5837
|
of a session-agnostic `createMCPRoutes(mcp)`; it owns its `path` (default
|
|
@@ -5875,7 +5875,7 @@ number; version: MCPVersion }>` — no dependency on `@orkestrel/middleware` and
|
|
|
5875
5875
|
the log under a monotone base36 event id (returned), evicting the oldest
|
|
5876
5876
|
past `capacity` + any entry older than the per-event TTL, and fans the
|
|
5877
5877
|
message out to every `attach`ed open stream as `stream.write({ id, data:
|
|
5878
|
-
JSON.stringify(message) })`. `session.replay(afterId)` returns every
|
|
5878
|
+
JSON.stringify(message) })`. `session.replay(afterId)` returns every
|
|
5879
5879
|
retained log entry strictly after `afterId` in append order — an unknown
|
|
5880
5880
|
/ evicted cursor replays nothing. The `createMCPSession` middleware
|
|
5881
5881
|
serves the resumable `GET {path}`: it validates the `mcp-session-id`
|
|
@@ -5999,7 +5999,7 @@ JSON.stringify(message) })`. `session.replay(afterId)` returns every
|
|
|
5999
5999
|
contract over native host APIs (`src/browser`).**
|
|
6000
6000
|
`createWebSocketClientTransport({ url, protocols? })` returns a
|
|
6001
6001
|
`MCPMessageTransportInterface` whose `start()` opens `new WebSocket(url,
|
|
6002
|
-
protocols)` and awaits the native `'open'` event (the RFC 6455 handshake
|
|
6002
|
+
protocols)` and awaits the native `'open'` event (the RFC 6455 handshake
|
|
6003
6003
|
is the host's concern; a connection failure — the native `'error'` event
|
|
6004
6004
|
while not yet `OPEN` — rejects `start()`); `send` writes each message as
|
|
6005
6005
|
one text frame once `OPEN`, queuing (in order) any message sent before —
|
|
@@ -6051,7 +6051,7 @@ protocols)` and awaits the native `'open'` event (the RFC 6455 handshake
|
|
|
6051
6051
|
(the implicit, portless channel — bound once, for the whole lifetime of
|
|
6052
6052
|
the returned handle, so a dedicated worker's very first portless message
|
|
6053
6053
|
needs no first-use setup), and registers one `scope.addEventListener(
|
|
6054
|
-
'message', …)` listener built by `createScopeMessageListener`. That one
|
|
6054
|
+
'message', …)` listener built by `createScopeMessageListener`. That one
|
|
6055
6055
|
listener handles every shape uniformly, per event, with no upfront
|
|
6056
6056
|
detection flag: `event.ports.length > 0` spawns a fresh
|
|
6057
6057
|
`createMessagePortTransport` + `bindServer` for that port (tracked for
|
package/dist/host/guides/pool.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Pool
|
|
2
2
|
|
|
3
|
-
> A typed resource pool with optional bounded capacity,
|
|
4
|
-
> validated reuse, caller-owned cancellation, explicit cleanup
|
|
5
|
-
> event-driven teardown barrier.
|
|
3
|
+
> A typed resource pool with optional bounded capacity, a warm floor, bounded loss recovery,
|
|
4
|
+
> exclusive leases by default, FIFO settlement, validated idle reuse, caller-owned cancellation, and explicit cleanup.
|
|
6
5
|
|
|
7
|
-
The pool
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
The pool supports lazy creation or a warm floor, with no eviction timer, acquire timeout,
|
|
7
|
+
or polling loop. Waits park on promises or signal listeners. An acquire rejects with
|
|
8
|
+
`cleanup` when a floor retains a record, owns `min` records, has nothing eligible, and has no
|
|
9
|
+
refill or disposal pending, even with live leases. The lifecycle hooks are the caller's,
|
|
10
|
+
so the engine itself performs no I/O.
|
|
10
11
|
|
|
11
12
|
## Surface
|
|
12
13
|
|
|
@@ -38,16 +39,16 @@ try {
|
|
|
38
39
|
|
|
39
40
|
### Factories
|
|
40
41
|
|
|
41
|
-
| API | Kind | Summary
|
|
42
|
-
| ------------ | -------- |
|
|
43
|
-
| `createPool` | function | Creates a distinct `PoolInterface` from resource lifecycle hooks, with optional bounded capacity,
|
|
42
|
+
| API | Kind | Summary |
|
|
43
|
+
| ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `createPool` | function | Creates a distinct `PoolInterface` from resource lifecycle hooks, with optional bounded capacity, exclusive leases by default, and FIFO settlement. |
|
|
44
45
|
|
|
45
46
|
### Classes
|
|
46
47
|
|
|
47
|
-
| API | Kind | Summary
|
|
48
|
-
| ----------- | ----- |
|
|
49
|
-
| `Pool` | class | Represents a
|
|
50
|
-
| `PoolError` | class | Represents a stable, machine-readable pool failure that retains the original thrown value as its cause without unsafe coercion, alongside structured context.
|
|
48
|
+
| API | Kind | Summary |
|
|
49
|
+
| ----------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `Pool` | class | Represents a resource pool with optional bounded capacity and a warm floor, whose opaque ownership records preserve FIFO settlement, cancellation, exact lease release, and deterministic teardown under concurrent hooks. |
|
|
51
|
+
| `PoolError` | class | Represents a stable, machine-readable pool failure that retains the original thrown value as its cause without unsafe coercion, alongside structured context. |
|
|
51
52
|
|
|
52
53
|
### Guards
|
|
53
54
|
|
|
@@ -56,27 +57,29 @@ In a guard table a `Shape` cell holds the type the guard narrows to.
|
|
|
56
57
|
| API | Kind | Shape | Summary |
|
|
57
58
|
| -------------- | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
58
59
|
| `isPoolError` | function | `PoolError` | Tests whether an unknown value is a `PoolError`, returning `false` for hostile proxies. |
|
|
59
|
-
| `
|
|
60
|
+
| `isPoolLimit` | function | `number` | Tests whether a value is a positive safe integer for a pool record or lease limit. |
|
|
60
61
|
| `isPoolSignal` | function | `AbortSignal` | Tests whether a value is a native `AbortSignal` for the acquire boundary, returning `false` for hostile proxies. |
|
|
61
62
|
|
|
62
63
|
### Types
|
|
63
64
|
|
|
64
65
|
A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
|
|
65
66
|
|
|
66
|
-
| API | Kind | Shape
|
|
67
|
-
| ------------------ | --------- |
|
|
68
|
-
| `PoolCode` | type | `'invalid' \| 'destroyed' \| 'create' \| 'cleanup'`
|
|
69
|
-
| `PoolContext` | interface | `{ value?, failures? }`
|
|
70
|
-
| `PoolErrorOptions` | interface | `{ code, cause?, context? }`
|
|
71
|
-
| `PoolEventMap` | type | `{ create, acquire, release, destroy }`
|
|
72
|
-
| `PoolToken` | interface | `{ value } plus release`
|
|
73
|
-
| `PoolOptions` | interface | `{ on?, error?, create, destroy?, validate?, max? }`
|
|
74
|
-
| `PoolInterface` | interface | `{ emitter, size, idle, active } plus acquire, clear, destroy`
|
|
75
|
-
|
|
76
|
-
`size` counts every owned record, including records being validated or
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
| API | Kind | Shape | Summary |
|
|
68
|
+
| ------------------ | --------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `PoolCode` | type | `'invalid' \| 'destroyed' \| 'create' \| 'cleanup'` | Names the machine-readable failure codes produced by `PoolError`. |
|
|
70
|
+
| `PoolContext` | interface | `{ value?, failures? }` | Represents the structured context attached to a `PoolError`: the rejected input, or the distinct destroy-hook failures an aggregate cleanup collected. |
|
|
71
|
+
| `PoolErrorOptions` | interface | `{ code, cause?, context? }` | Represents the construction options for `PoolError`: the stable code, an optional cause, and optional structured context. |
|
|
72
|
+
| `PoolEventMap` | type | `{ create, acquire, release, destroy }` | Represents the observable resource lifecycle events emitted by a `PoolInterface`. |
|
|
73
|
+
| `PoolToken` | interface | `{ value } plus release, destroy` | Represents a unique lease over one pool-owned resource record, exposing that record as a readonly `value` and ending it through an idempotent `release` or `destroy`. |
|
|
74
|
+
| `PoolOptions` | interface | `{ on?, error?, create, destroy?, validate?, watch?, capacity?, max?, min?, restarts? }` | Represents the resource lifecycle options for `Pool` and `createPool`: creation, destruction, validation, capacity, a bounded warm floor, and loss observation. |
|
|
75
|
+
| `PoolInterface` | interface | `{ emitter, size, idle, active } plus start, acquire, clear, destroy` | Represents a FIFO resource pool with optional bounded capacity, a warm floor, loss recovery, and deterministic teardown, exposing its record counts and a typed lifecycle emitter. |
|
|
76
|
+
|
|
77
|
+
`size` counts every owned record, including records being validated, destroyed, or retained
|
|
78
|
+
after failed cleanup under `min`. `idle` counts available records with no live lease or pending
|
|
79
|
+
handout. `active` counts records with live leases, counting a shared record once. A partially
|
|
80
|
+
occupied record is active and can accept another lease; it is not idle. A pending handout counts
|
|
81
|
+
against `capacity` but becomes active only at its FIFO grant. An in-flight create reservation
|
|
82
|
+
claims record capacity but is not yet an owned record and therefore is not part of `size`.
|
|
80
83
|
|
|
81
84
|
## Methods
|
|
82
85
|
|
|
@@ -85,24 +88,121 @@ The public call-signature members of `PoolInterface` and `PoolToken`; `Pool` imp
|
|
|
85
88
|
|
|
86
89
|
#### `PoolInterface`
|
|
87
90
|
|
|
88
|
-
| Method | Returns | Summary
|
|
89
|
-
| --------- | ----------------------- |
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
91
|
+
| Method | Returns | Summary |
|
|
92
|
+
| --------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `start` | `Promise<void>` | Fills the warm floor and resets the strikes of a spent floor; without a floor, resolves immediately. |
|
|
94
|
+
| `acquire` | `Promise<PoolToken<T>>` | Queues the caller in FIFO order, shares an occupied record or validates an idle record, and waits for a floor refill or creates on demand without a floor. |
|
|
95
|
+
| `clear` | `Promise<void>` | Destroys the records that are idle at this call's synchronous snapshot and restores a started floor. |
|
|
96
|
+
| `destroy` | `Promise<void>` | Tears down the pool permanently and returns its stable completion barrier. |
|
|
93
97
|
|
|
94
98
|
#### `PoolToken`
|
|
95
99
|
|
|
96
|
-
The lease returned by `acquire`, with
|
|
100
|
+
The lease returned by `acquire`, with operations that return or dispose its exact record.
|
|
97
101
|
|
|
98
|
-
| Method | Returns
|
|
99
|
-
| --------- |
|
|
100
|
-
| `release` | `void`
|
|
102
|
+
| Method | Returns | Summary |
|
|
103
|
+
| --------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
104
|
+
| `release` | `void` | Ends this exact lease once; a repeat call, and a call after loss or teardown took ownership, are no-ops. Other leases on the record remain live. |
|
|
105
|
+
| `destroy` | `Promise<void>` | Destroys this exact record for every co-holder and invalidates their leases; a repeat call, and a call after release, are no-ops. |
|
|
101
106
|
|
|
102
107
|
## Contract
|
|
103
108
|
|
|
109
|
+
### Warm floor and loss
|
|
110
|
+
|
|
111
|
+
Construction creates nothing. Set `min` to a positive safe integer and `restarts` to a
|
|
112
|
+
non-negative safe integer, then call `start()`. `max` defaults to `min`; explicit values
|
|
113
|
+
must equal each other. A missing restart bound, a bound without `min`, and a non-callable
|
|
114
|
+
`watch` are invalid. The restart bound has no default.
|
|
115
|
+
|
|
116
|
+
`start()` resolves when the pool owns `min` live records. Concurrent calls share the pending
|
|
117
|
+
promise. Without `min`, it resolves immediately. Under `min`, queued acquires never cause
|
|
118
|
+
creation, including before `start()` and during failed refills. The pool refills missing
|
|
119
|
+
capacity after startup and cleanup, one create attempt at a time.
|
|
120
|
+
Bound your `create` and `destroy` hooks. A create hook that never settles blocks every later
|
|
121
|
+
refill, including the attempt owed to a lost leased record. A destroy hook that never settles
|
|
122
|
+
keeps its record counted against `max` and blocks replacement of that record.
|
|
123
|
+
|
|
124
|
+
`watch(value, signal)` runs once for each created record. A fulfillment, rejection, or
|
|
125
|
+
synchronous throw declares loss while the record is live. The pool removes the record from
|
|
126
|
+
service, disposes it, and refills if the bound permits. Rejections and synchronous throws
|
|
127
|
+
reach `error(error, 'watch')` only while the record is live; a throwing error handler is
|
|
128
|
+
isolated. Disposal aborts the watch signal before the destroy hook runs, including disposal
|
|
129
|
+
through validation, clear, pool teardown, and token destruction.
|
|
130
|
+
The watch must release its listeners when that signal aborts. Any settlement after the signal
|
|
131
|
+
aborts is ignored, including rejection. A late or repeated loss cannot
|
|
132
|
+
dispose a record twice. A lost record cannot be handed out after validation or from a ready
|
|
133
|
+
result waiting behind an earlier caller. The pool disposes a lost leased record while its
|
|
134
|
+
holder still holds the token. The holder learns of that loss from its own `watch` or `destroy`
|
|
135
|
+
hook. After disposal, the token's `release()` and `destroy()` calls do nothing. During disposal,
|
|
136
|
+
the first `destroy()` call waits for that cleanup attempt and rejects with `cleanup` if it fails.
|
|
137
|
+
|
|
138
|
+
A failed refill adds a strike. Losing a record with no live lease also adds a strike,
|
|
139
|
+
including a previously used record and failed idle validation. Granting a lease resets the
|
|
140
|
+
strikes only when the granted record was created after the last strike; successful creation
|
|
141
|
+
alone does not. Granting an older healthy
|
|
142
|
+
record leaves the strikes unchanged. Any idle record that dies, refills, and dies
|
|
143
|
+
again without a grant between those losses can spend the floor.
|
|
144
|
+
When strikes exceed `restarts`, the floor is spent. With `restarts: 1`, the first failed
|
|
145
|
+
create permits another attempt, and the second refuses the next attempt. `start()` rejects
|
|
146
|
+
with `create` and the last cause; another `start()` resets the strikes. While the floor is
|
|
147
|
+
spent, an acquire without an eligible record rejects with `create` after pending refills and
|
|
148
|
+
disposal settle. If the floor retains a record and owns `min` records, that acquire rejects
|
|
149
|
+
with `cleanup` instead, even with live leases. A spent floor and a rejected `start()` still
|
|
150
|
+
serve their live records. A loss without a thrown cause leaves that cause undefined.
|
|
151
|
+
|
|
152
|
+
A record lost while leased earns one refill attempt even after the bound is spent.
|
|
153
|
+
The credit becomes spendable only when successful disposal removes the record; failed disposal
|
|
154
|
+
grants no credit. That attempt remains owed during a concurrent refill and never resets the strikes.
|
|
155
|
+
Its failed create adds a strike; its success alone does not reset the strikes. Granting that
|
|
156
|
+
replacement resets the strikes only if no strike occurred after its creation. A record that was leased
|
|
157
|
+
and later lost after its last release adds a strike. Under sharing, loss or one lease's
|
|
158
|
+
`destroy()` ends the record for every co-holder, invalidates their leases, and owes one refill
|
|
159
|
+
regardless of the lease count. `token.destroy()` waits for an existing cleanup attempt and
|
|
160
|
+
rejects with `cleanup` if disposal fails.
|
|
161
|
+
A token ends once: after `release()` or `destroy()`, the other call does nothing. Repeating
|
|
162
|
+
either call does nothing. A repeat `token.destroy()` after a `cleanup` rejection resolves
|
|
163
|
+
while the record stays retained. Refills hold no lease before a caller's grant, so repeated idle
|
|
164
|
+
losses spend the budget. A consumer that keeps a lease without a holder, or repeatedly acquires
|
|
165
|
+
refilled records that fail during use, can keep earning refills and qualifying grant resets.
|
|
166
|
+
The pool cannot distinguish such a lease from useful work; callers must acquire only for a holder.
|
|
167
|
+
|
|
168
|
+
Under `min`, a failed destroy hook leaves its record counted against `max` and excluded from
|
|
169
|
+
idle and active counts. The pool never replaces that retained record or retries its destroy
|
|
170
|
+
hook. The pool can run short; `start()` rejects with `cleanup` if retained records prevent
|
|
171
|
+
filling. An acquire rejects with `cleanup` when the floor retains a record, owns `min` records,
|
|
172
|
+
has nothing eligible, and has no refill or disposal pending, even with live leases. The cause is
|
|
173
|
+
the retained record's own cleanup failure. The terminal `destroy()` barrier
|
|
174
|
+
reports the original cleanup failures. Without `min`, cleanup retains its lazy behavior and
|
|
175
|
+
frees capacity after either hook outcome.
|
|
176
|
+
|
|
177
|
+
This example warms an inert resource, explicitly loses its lease, and tears down the pool:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { createPool } from '@orkestrel/pool'
|
|
181
|
+
|
|
182
|
+
const pool = createPool({
|
|
183
|
+
create: () => new EventTarget(),
|
|
184
|
+
min: 1,
|
|
185
|
+
restarts: 1,
|
|
186
|
+
watch: (resource, signal) =>
|
|
187
|
+
new Promise<void>((resolve) => {
|
|
188
|
+
resource.addEventListener('loss', () => resolve(), { once: true, signal })
|
|
189
|
+
signal.addEventListener('abort', () => resolve(), { once: true })
|
|
190
|
+
}),
|
|
191
|
+
})
|
|
192
|
+
await pool.start()
|
|
193
|
+
const token = await pool.acquire()
|
|
194
|
+
await token.destroy()
|
|
195
|
+
await pool.destroy()
|
|
196
|
+
```
|
|
197
|
+
|
|
104
198
|
### Capacity and FIFO
|
|
105
199
|
|
|
200
|
+
`capacity` is a positive safe integer limiting simultaneous leases on each record; it defaults
|
|
201
|
+
to 1. `min` and `max` still bound records. Acquisition chooses the eligible record with the
|
|
202
|
+
fewest live leases and reserved handouts. Idle ties follow release order; occupied ties follow
|
|
203
|
+
record creation order. A validating record is unavailable until validation settles.
|
|
204
|
+
Reservations count before any awaited hook.
|
|
205
|
+
|
|
106
206
|
Every `acquire` receives its queue position before a create or validation hook starts. The
|
|
107
207
|
reentrancy-safe pump may assign several hook operations concurrently, but a head commit
|
|
108
208
|
barrier settles successes and failures in request order. A later fast create or validation
|
|
@@ -110,19 +210,32 @@ cannot overtake an earlier slow one. Capacity obeys:
|
|
|
110
210
|
|
|
111
211
|
```text
|
|
112
212
|
owned records + create reservations <= max
|
|
213
|
+
live leases + reserved handouts per record <= capacity
|
|
113
214
|
```
|
|
114
215
|
|
|
115
|
-
`max` must be a positive safe integer. Omit
|
|
116
|
-
zero, negative values, and unsafe integers are invalid.
|
|
117
|
-
|
|
216
|
+
`max` must be a positive safe integer. Omit `min` and `max` for an unbounded record count.
|
|
217
|
+
`Infinity`, fractions, zero, negative values, and unsafe integers are invalid.
|
|
218
|
+
Construction snapshots `max` and `capacity` once and validates them before retention, then
|
|
219
|
+
snapshots `on` and `error` once each.
|
|
118
220
|
|
|
119
|
-
Record
|
|
221
|
+
Record ownership follows these transitions; an occupied record can carry several leases and
|
|
222
|
+
reserved handouts, and returns to available after its last lease and reservation end:
|
|
120
223
|
|
|
121
224
|
```text
|
|
122
225
|
create reservation -> ready -> leased -> available -> validating -> ready
|
|
123
226
|
\-> destroying -> removed
|
|
227
|
+
\-> retained (failed cleanup with min)
|
|
228
|
+
floor refill --------------------------> available
|
|
124
229
|
```
|
|
125
230
|
|
|
231
|
+
Validation runs only when reserving an idle record; handouts on an occupied record skip it.
|
|
232
|
+
No caller receives a lease while idle validation is pending. This keeps a failed validation
|
|
233
|
+
from silently disposing a record used by co-holders. After the last lease ends, the next idle
|
|
234
|
+
reuse validates again. A freshly created lazy record keeps its direct handout behavior.
|
|
235
|
+
With `capacity` greater than 1, an occupied record is never revalidated. Only `watch` detects
|
|
236
|
+
its loss: without `watch`, a dead record can keep receiving leases until it becomes idle;
|
|
237
|
+
with `watch`, that exposure lasts until the watch settles.
|
|
238
|
+
|
|
126
239
|
Invalid validation, whether `false` or a thrown value, claims and cleans the record before a
|
|
127
240
|
replacement capacity slot becomes available. Cleanup failure rejects that acquire with
|
|
128
241
|
`PoolError` code `cleanup`; successful cleanup lets the same FIFO waiter seek a replacement.
|
|
@@ -145,19 +258,24 @@ eventual `destroy()` barrier.
|
|
|
145
258
|
|
|
146
259
|
### Release and cleanup
|
|
147
260
|
|
|
148
|
-
A token captures its exact opaque record, so `release()` is correct even when multiple
|
|
149
|
-
records contain the same value. Release is idempotent and
|
|
150
|
-
|
|
151
|
-
|
|
261
|
+
A token captures its exact lease and opaque record, so `release()` is correct even when multiple
|
|
262
|
+
records contain the same value or several tokens share a record. Release is idempotent and
|
|
263
|
+
removes only that lease synchronously. A remaining co-holder keeps the record active, and a
|
|
264
|
+
waiter can share the freed place without validation. After the last release, idle reuse validates.
|
|
265
|
+
A record that remains idle emits `release`. Release after loss or teardown took ownership does nothing.
|
|
266
|
+
The pool disposes a lost lease while its holder still holds the token; the holder learns of
|
|
267
|
+
the loss through its own `watch` or `destroy` hook. After disposal, both token methods do nothing.
|
|
152
268
|
|
|
153
269
|
`clear()` synchronously snapshots idle records and installs one cleanup promise per record
|
|
154
270
|
before invoking the hook. Concurrent clears therefore own disjoint snapshots, and a lease
|
|
155
|
-
released after a snapshot is taken is not part of it. Every claimed record stays in `size`
|
|
156
|
-
hook attempt
|
|
157
|
-
whose `context.failures` retains the original thrown
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
271
|
+
released after a snapshot is taken is not part of it. Every claimed record stays in `size` during
|
|
272
|
+
its hook attempt; failed cleanup under `min` retains it afterward. Distinct failures are
|
|
273
|
+
aggregated in a code-`cleanup` `PoolError` whose `context.failures` retains the original thrown
|
|
274
|
+
values.
|
|
275
|
+
Each claimed record's cleanup settlement wakes queued acquires after the destroy ledger
|
|
276
|
+
transition. A failed `clear()` rejects its own aggregate cleanup barrier; under `min`,
|
|
277
|
+
the pool keeps a record whose cleanup fails counted against `max`, and successful cleanup
|
|
278
|
+
restores a started floor.
|
|
161
279
|
|
|
162
280
|
### Destruction
|
|
163
281
|
|
|
@@ -165,7 +283,8 @@ still rejects its own aggregate cleanup barrier.
|
|
|
165
283
|
rejects waiters, emits events, or invokes cleanup. Reentrant and repeated calls return that
|
|
166
284
|
same object. Teardown invalidates idle, leased, and ready records, waits for create,
|
|
167
285
|
validation, existing clear cleanup, and new cleanup activity, and disposes every late
|
|
168
|
-
resource.
|
|
286
|
+
resource. A pending `start()` rejects with `destroyed`. Unresolved create, validation, and
|
|
287
|
+
destroy hooks keep the barrier pending without polling; aborted watches do not delay it.
|
|
169
288
|
|
|
170
289
|
Cleanup already owned by an overlapping `clear()` is shared; its failure is reported to both
|
|
171
290
|
the clear call and the destroy aggregate. Create failures that produced no resource do not
|
|
@@ -176,12 +295,12 @@ hook attempt, then the stable barrier resolves or rejects.
|
|
|
176
295
|
|
|
177
296
|
`PoolError.code` is stable and lowercase:
|
|
178
297
|
|
|
179
|
-
| Code | Owner
|
|
180
|
-
| ----------- |
|
|
181
|
-
| `invalid` | Invalid
|
|
182
|
-
| `destroyed` |
|
|
183
|
-
| `create` |
|
|
184
|
-
| `cleanup` |
|
|
298
|
+
| Code | Owner |
|
|
299
|
+
| ----------- | ----------------------------------------------------------------------------------- |
|
|
300
|
+
| `invalid` | Invalid capacity, restart bound, watch hook, acquire signal, or internal boundary. |
|
|
301
|
+
| `destroyed` | Start, acquire, or clear attempted during terminal teardown. |
|
|
302
|
+
| `create` | A lazy create failed, or a floor refill bound was spent. |
|
|
303
|
+
| `cleanup` | Record disposal failed, including retained records blocking startup or acquisition. |
|
|
185
304
|
|
|
186
305
|
The original thrown value is retained as `cause`; aggregate cleanup values are also in
|
|
187
306
|
`context.failures`. Message construction and `isPoolError` avoid unsafe string coercion and
|
|
@@ -194,12 +313,12 @@ invokes listeners synchronously at each emission point. The `destroy` emission i
|
|
|
194
313
|
one microtask after cleanup settlement so bound outcomes precede observer reentry. Events
|
|
195
314
|
follow their ledger transitions:
|
|
196
315
|
|
|
197
|
-
| Event | Emission point
|
|
198
|
-
| --------- |
|
|
199
|
-
| `create` | After a fresh resource is inserted into the ownership ledger.
|
|
200
|
-
| `acquire` | After the waiter promise receives its token and the record is leased.
|
|
201
|
-
| `release` | Only when
|
|
202
|
-
| `destroy` | After the hook attempt and resource-ledger removal, one microtask after private cleanup settlement, while destroying ownership remains installed through the event. |
|
|
316
|
+
| Event | Emission point |
|
|
317
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
318
|
+
| `create` | After a fresh resource is inserted into the ownership ledger. |
|
|
319
|
+
| `acquire` | After the waiter promise receives its token and the record is leased. |
|
|
320
|
+
| `release` | Only when a released, orphaned, or refilled record remains idle. |
|
|
321
|
+
| `destroy` | After the hook attempt and resource-ledger removal or retention, one microtask after private cleanup settlement, while destroying ownership remains installed through the event. |
|
|
203
322
|
|
|
204
323
|
Because listeners may synchronously reenter or destroy the pool, the engine checks terminal
|
|
205
324
|
and waiter state after every awaited hook and every emit.
|
|
@@ -224,10 +343,10 @@ const pool = createPool({
|
|
|
224
343
|
Check a candidate value or error against the public boundary guards before acting on it:
|
|
225
344
|
|
|
226
345
|
```ts
|
|
227
|
-
import { PoolError, isPoolError,
|
|
346
|
+
import { PoolError, isPoolError, isPoolLimit, isPoolSignal } from '@orkestrel/pool'
|
|
228
347
|
|
|
229
|
-
|
|
230
|
-
|
|
348
|
+
isPoolLimit(4) // true
|
|
349
|
+
isPoolLimit(Infinity) // false: omit max for unbounded capacity
|
|
231
350
|
isPoolSignal(new AbortController().signal) // true
|
|
232
351
|
|
|
233
352
|
const failure = new PoolError({ code: 'destroyed' })
|
|
@@ -260,7 +379,8 @@ await pool.destroy()
|
|
|
260
379
|
hooks, create continuation, abort boundaries, abort-listener detachment, exclusive
|
|
261
380
|
invalid-cleanup waiter ownership, bounded and unbounded replacement, destroy-observer
|
|
262
381
|
reentry ordering, concurrent clear, stable reentrant destruction, late resources,
|
|
263
|
-
aggregate failures, emitter ordering,
|
|
382
|
+
aggregate failures, emitter ordering, high contention, warm floors, exact refill bounds,
|
|
383
|
+
retained cleanup failures, watch cancellation, lost-record handoff races, and owed lease refills.
|
|
264
384
|
- [`tests/src/core/validators.test.ts`](../tests/src/core/validators.test.ts) — the public
|
|
265
385
|
boundary guards alone: accepted and rejected maxima, and native versus hostile signals.
|
|
266
386
|
- [`tests/src/core/factories.test.ts`](../tests/src/core/factories.test.ts) — factory
|
|
@@ -271,7 +391,8 @@ await pool.destroy()
|
|
|
271
391
|
its declaration's description paragraph, the titled `Create a pool` fence against the
|
|
272
392
|
`@example` block of that title (pinned so the titled pair cannot be retired silently), and the
|
|
273
393
|
README pitch against this guide's tagline. It also runs the boundary-guard fence and asserts
|
|
274
|
-
the values its comments claim.
|
|
394
|
+
the values its comments claim. The floor fence is checked line for line in execution order
|
|
395
|
+
and runs with assertions for warming, token cleanup, and watch-listener removal.
|
|
275
396
|
|
|
276
397
|
## See also
|
|
277
398
|
|