@abide/abide 0.103.0 → 0.104.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/CHANGELOG.md +13 -0
- package/docs/REGISTRY.md +10 -10
- package/docs/RULEBOOK.md +18 -12
- package/docs/abide.md +22 -37
- package/package.json +1 -1
- package/src/compiler/TypeSession.ts +4 -6
- package/src/compiler/appMap.ts +1 -6
- package/src/compiler/checkApp.ts +9 -9
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/compileApp.ts +44 -7
- package/src/compiler/projectShadow.ts +1 -1
- package/src/server/abideModules.ts +80 -43
- package/src/server/appTree.ts +10 -2
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/versionOf.ts +1 -2
- package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
- package/src/shared/reactive/Cursor.ts +66 -123
- package/src/shared/reactive/Reactive.ts +74 -28
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/chunksOfStream.ts +10 -34
- package/src/shared/routePattern.ts +21 -8
|
@@ -123,13 +123,39 @@ let registered = false
|
|
|
123
123
|
*/
|
|
124
124
|
export function serveCompilation(root: string, compilation: Compilation): void {
|
|
125
125
|
const real = realOrAsGiven(root)
|
|
126
|
-
addCompilation(real, compilation, RUNTIME)
|
|
126
|
+
const handlerFiles = addCompilation(real, compilation, RUNTIME)
|
|
127
127
|
installSourceMaps(serverMapsOf(compilation, real), real)
|
|
128
|
+
if (handlerFiles.length !== 0) Bun.plugin(handlerFilesPlugin(handlerFiles))
|
|
128
129
|
if (registered) return
|
|
129
130
|
registered = true
|
|
130
131
|
Bun.plugin(modulesPlugin(RUNTIME))
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
// 16.2 — A HANDLER FILE LOADED AT ITS OWN PATH RE-EXPORTS ITS FACE, so every spelling of its import
|
|
135
|
+
// is the module the server mounted. The runtime asks `onResolve` only for a specifier with an
|
|
136
|
+
// extension, while `onLoad` is asked by the path the file resolved to, however it was spelled. One
|
|
137
|
+
// plugin per compilation because the filter is fixed at registration, and the runtime's `onLoad`
|
|
138
|
+
// cannot fall through, so the filter names the handler files and nothing else. The face it names
|
|
139
|
+
// is resolved by `modulesPlugin`, registered once.
|
|
140
|
+
function handlerFilesPlugin(paths: string[]): Bun.BunPlugin {
|
|
141
|
+
return {
|
|
142
|
+
name: 'abide-handler-files',
|
|
143
|
+
setup(build) {
|
|
144
|
+
build.onLoad(
|
|
145
|
+
{
|
|
146
|
+
filter: new RegExp(
|
|
147
|
+
`^(?:${paths.map(RegExp.escape).join('|')})$`,
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
(args) => ({
|
|
151
|
+
contents: `export { default } from ${JSON.stringify(`${args.path}${HANDLER_SUFFIX}`)}\n`,
|
|
152
|
+
loader: 'js',
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
133
159
|
/** What the server's table exports: its routes and error pages, server blocks and handlers. */
|
|
134
160
|
export type BuiltTable = ServerTable & {
|
|
135
161
|
/** 37.1 — every module a `<script server>` runs in, imported before the app boots. */
|
|
@@ -162,18 +188,24 @@ export function serverMapsOf(
|
|
|
162
188
|
return maps
|
|
163
189
|
}
|
|
164
190
|
|
|
191
|
+
/** Adds `compilation` to `held`, and answers its handler files' absolute paths. */
|
|
165
192
|
function addCompilation(
|
|
166
193
|
root: string,
|
|
167
194
|
compilation: Compilation,
|
|
168
195
|
held: Held,
|
|
169
|
-
):
|
|
196
|
+
): string[] {
|
|
170
197
|
// RESOLVED, not joined: a dependency's file is keyed above the root (`../node_modules/…`).
|
|
171
198
|
for (const [relative, compiled] of Object.entries(compilation.files))
|
|
172
199
|
held.files.set(resolve(root, relative), compiled)
|
|
173
|
-
|
|
174
|
-
|
|
200
|
+
const handlerFiles: string[] = []
|
|
201
|
+
for (const [relative, face] of Object.entries(compilation.handlers)) {
|
|
202
|
+
const path = resolve(root, relative)
|
|
203
|
+
held.handlers.set(path, face)
|
|
204
|
+
handlerFiles.push(path)
|
|
205
|
+
}
|
|
175
206
|
for (const [name, text] of Object.entries(compilation.tables))
|
|
176
207
|
held.tables.set(`${root}/${VIRTUAL_DIRECTORY}/${name}`, text)
|
|
208
|
+
return handlerFiles
|
|
177
209
|
}
|
|
178
210
|
|
|
179
211
|
// `Bun.resolveSync` asks EVERY abide plugin's `onResolve` again — this one's, and the runtime's, which
|
|
@@ -292,46 +324,45 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
|
|
|
292
324
|
|
|
293
325
|
// A HANDLER FILE IS FOUND BY WHERE ITS IMPORT RESOLVES, however the import spells it, and
|
|
294
326
|
// only from the app's own modules: a package's never names one, and asking bun again for
|
|
295
|
-
// each of a package's relative imports is a resolve per module of the bundle.
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
327
|
+
// each of a package's relative imports is a resolve per module of the bundle. THE FILTER
|
|
328
|
+
// IS THE HANDLER FILES' NAMES: a hook matching every relative import costs the bundle
|
|
329
|
+
// abide's own `sideEffects`, and the barrel's rest — the validator, the csp — moved into
|
|
330
|
+
// the chunk every page loads (38.94). ONLY IN A BUNDLE: the runtime finds a handler file
|
|
331
|
+
// by the path it loads at (`handlerFilesPlugin`), which costs no resolve per import.
|
|
300
332
|
// Each handler file's name without `.ts` — what an import of one ends in.
|
|
301
|
-
|
|
302
|
-
if (bundling)
|
|
303
|
-
const names = new Set<string>()
|
|
333
|
+
const stems = new Set<string>()
|
|
334
|
+
if (bundling)
|
|
304
335
|
for (const path of served.handlers.keys())
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
stems.add(basename(path, '.ts'))
|
|
337
|
+
if (stems.size !== 0)
|
|
338
|
+
build.onResolve(
|
|
339
|
+
{
|
|
340
|
+
filter: new RegExp(
|
|
341
|
+
`(?:^|/)(?:${[...stems].map(RegExp.escape).join('|')})(?:\\.ts)?$`,
|
|
342
|
+
),
|
|
343
|
+
},
|
|
344
|
+
(args) => {
|
|
345
|
+
if (resolving || !RELATIVE_OR_SUBPATH.test(args.path))
|
|
346
|
+
return undefined
|
|
347
|
+
const importer = pathOf(args.importer)
|
|
348
|
+
if (importer.includes('/node_modules/'))
|
|
349
|
+
return undefined
|
|
350
|
+
let path: string
|
|
351
|
+
try {
|
|
352
|
+
path = resolved(args.path, args.importer)
|
|
353
|
+
} catch {
|
|
354
|
+
return undefined
|
|
355
|
+
}
|
|
356
|
+
const face = served.handlers.get(path)
|
|
357
|
+
if (face === undefined) return undefined
|
|
358
|
+
// 20.4 — a handler the browser is handed no wrapper for is no import of its.
|
|
359
|
+
if (inBrowser(args.importer) && face.client === '')
|
|
360
|
+
throw new Error(
|
|
361
|
+
`abide: ${pathOf(args.importer)} imports ${args.path}, whose handler withholds itself from the browser (\`clients: { ui: false }\`). Call it from a <script server>, or let the browser have it. RULEBOOK 20.4.`,
|
|
362
|
+
)
|
|
363
|
+
return { path: `${path}${HANDLER_SUFFIX}` }
|
|
364
|
+
},
|
|
365
|
+
)
|
|
335
366
|
|
|
336
367
|
// A RELATIVE SPECIFIER A FACE IMPORTS AT RUN TIME is resolved from the file the face
|
|
337
368
|
// stands for: bun's runtime reads one from an importer in a namespace as no path at
|
|
@@ -340,12 +371,18 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
|
|
|
340
371
|
// and where a hook matching every relative import in a package costs the bundle that
|
|
341
372
|
// package's `sideEffects`: a two-page app grew by 12 KB, the validator one page never
|
|
342
373
|
// calls moved into the chunk both pages load.
|
|
343
|
-
if (!bundling)
|
|
374
|
+
if (!bundling) {
|
|
375
|
+
// A face `handlerFilesPlugin` re-exports, named by its absolute path, which no file
|
|
376
|
+
// is at.
|
|
377
|
+
build.onResolve({ filter: HANDLER_FACE }, (args) => ({
|
|
378
|
+
path: args.path,
|
|
379
|
+
}))
|
|
344
380
|
build.onResolve({ filter: /^\.\.?\// }, (args) =>
|
|
345
381
|
resolving || pathOf(args.importer) === args.importer
|
|
346
382
|
? undefined
|
|
347
383
|
: { path: resolved(args.path, args.importer) },
|
|
348
384
|
)
|
|
385
|
+
}
|
|
349
386
|
|
|
350
387
|
build.onLoad(
|
|
351
388
|
{ filter: /\.abide$/, namespace: componentNamespace },
|
package/src/server/appTree.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { type Dirent, existsSync, readdirSync } from 'node:fs' // bun has no directory listing or sync existence check of its own
|
|
16
16
|
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
17
17
|
import {
|
|
18
|
+
HOLDS_SERVER,
|
|
18
19
|
LIB_DIRECTORY,
|
|
19
20
|
PUBLIC_DIRECTORY,
|
|
20
21
|
RESERVED_DIRECTORIES,
|
|
@@ -28,7 +29,7 @@ type AppFiles = {
|
|
|
28
29
|
abide: string[]
|
|
29
30
|
/** Every handler file, in path order. */
|
|
30
31
|
handlers: string[]
|
|
31
|
-
/** Every module under a `server` or a `lib` directory, in path order —
|
|
32
|
+
/** Every module under a `server` or a `lib` directory, in path order — read for 37.13. */
|
|
32
33
|
modules: string[]
|
|
33
34
|
/** Every file under a `public` directory, to the address it is answered at below the mount. */
|
|
34
35
|
published: Map<string, string>
|
|
@@ -90,7 +91,14 @@ function walk(
|
|
|
90
91
|
if (entry.isDirectory()) {
|
|
91
92
|
let within = reserved
|
|
92
93
|
if (RESERVED_DIRECTORIES.has(entry.name)) {
|
|
93
|
-
|
|
94
|
+
// 23.16 — `server` nests in the fences that hold code.
|
|
95
|
+
if (
|
|
96
|
+
reserved !== undefined &&
|
|
97
|
+
!(
|
|
98
|
+
entry.name === SERVER_DIRECTORY &&
|
|
99
|
+
HOLDS_SERVER.has(reserved.name)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
94
102
|
refuse(
|
|
95
103
|
path,
|
|
96
104
|
`abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
|
|
@@ -29,7 +29,7 @@ import { type Listed, surfaceOf } from './surfaceOf.ts'
|
|
|
29
29
|
const OPENAPI_LOG = namedLogger('abide:openapi')
|
|
30
30
|
const MCP_LOG = namedLogger('abide:mcp')
|
|
31
31
|
|
|
32
|
-
// What
|
|
32
|
+
// What OpenAPI adds to a listing: whether the handler is keyed.
|
|
33
33
|
type Published = Listed & { keyed: boolean }
|
|
34
34
|
|
|
35
35
|
/** Every handler `surface` lists (`surfaceOf`). */
|
|
@@ -212,18 +212,8 @@ export function openApi(
|
|
|
212
212
|
return Object.keys(document().paths).length > 0 ? document : undefined
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
type Tools = { tools: Published[]; resources: Published[] }
|
|
215
|
+
const published = new WeakMap<Map<string, Mounted>, Reactive<Published[]>>()
|
|
217
216
|
|
|
218
|
-
const published = new WeakMap<Map<string, Mounted>, Reactive<Tools>>()
|
|
219
|
-
|
|
220
|
-
function toolsOf(handlers: Map<string, Mounted>): Tools {
|
|
221
|
-
const found: Tools = { tools: [], resources: [] }
|
|
222
|
-
for (const one of listed(handlers, 'mcp'))
|
|
223
|
-
if (one.keyed || one.method !== 'GET') found.tools.push(one)
|
|
224
|
-
else found.resources.push(one)
|
|
225
|
-
return found
|
|
226
|
-
}
|
|
227
217
|
|
|
228
218
|
type JsonRpc = {
|
|
229
219
|
jsonrpc: '2.0'
|
|
@@ -264,18 +254,20 @@ export class McpSurface {
|
|
|
264
254
|
|
|
265
255
|
/** Whether the route table publishes any handler to it. 20.7. */
|
|
266
256
|
get opened(): boolean {
|
|
267
|
-
|
|
268
|
-
return tools.length > 0 || resources.length > 0
|
|
257
|
+
return this.#published().length > 0
|
|
269
258
|
}
|
|
270
259
|
|
|
271
|
-
#published():
|
|
272
|
-
|
|
260
|
+
#published(): Published[] {
|
|
261
|
+
// Every handler the MCP surface lists, each a tool — a read as much as a mutation. 20.9.
|
|
262
|
+
return overTable(published, this.#handlers, (handlers) =>
|
|
263
|
+
listed(handlers, 'mcp'),
|
|
264
|
+
)()
|
|
273
265
|
}
|
|
274
266
|
|
|
275
267
|
/** Every tool it lists. 20.9. */
|
|
276
268
|
list(): ToolDefinition[] {
|
|
277
269
|
const listing: ToolDefinition[] = []
|
|
278
|
-
for (const tool of this.#published()
|
|
270
|
+
for (const tool of this.#published()) {
|
|
279
271
|
if (tool.description === undefined)
|
|
280
272
|
MCP_LOG.warning(
|
|
281
273
|
`${tool.name} is published with no description.`,
|
|
@@ -305,7 +297,7 @@ export class McpSurface {
|
|
|
305
297
|
}
|
|
306
298
|
|
|
307
299
|
#tool(name: unknown): Published | undefined {
|
|
308
|
-
return this.#published().
|
|
300
|
+
return this.#published().find((one) => one.name === name)
|
|
309
301
|
}
|
|
310
302
|
|
|
311
303
|
async #called(
|
|
@@ -335,7 +327,7 @@ export class McpSurface {
|
|
|
335
327
|
return {
|
|
336
328
|
result: {
|
|
337
329
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
338
|
-
capabilities: { tools: {}
|
|
330
|
+
capabilities: { tools: {} },
|
|
339
331
|
serverInfo: appIdentity(),
|
|
340
332
|
},
|
|
341
333
|
}
|
|
@@ -343,19 +335,6 @@ export class McpSurface {
|
|
|
343
335
|
return { result: {} }
|
|
344
336
|
case 'tools/list':
|
|
345
337
|
return { result: { tools: this.list() } }
|
|
346
|
-
case 'resources/list':
|
|
347
|
-
return {
|
|
348
|
-
result: {
|
|
349
|
-
resources: this.#published().resources.map(
|
|
350
|
-
(resource) => ({
|
|
351
|
-
uri: `abide:${resource.address}`,
|
|
352
|
-
name: resource.name,
|
|
353
|
-
description: resource.description ?? '',
|
|
354
|
-
mimeType: 'application/json',
|
|
355
|
-
}),
|
|
356
|
-
),
|
|
357
|
-
},
|
|
358
|
-
}
|
|
359
338
|
case 'tools/call': {
|
|
360
339
|
const tool = this.#tool(params?.name)
|
|
361
340
|
if (tool === undefined)
|
|
@@ -375,25 +354,6 @@ export class McpSurface {
|
|
|
375
354
|
},
|
|
376
355
|
}
|
|
377
356
|
}
|
|
378
|
-
case 'resources/read': {
|
|
379
|
-
const uri = String(params?.uri ?? '')
|
|
380
|
-
const resource = this.#published().resources.find(
|
|
381
|
-
(one) => `abide:${one.address}` === uri,
|
|
382
|
-
)
|
|
383
|
-
if (resource === undefined)
|
|
384
|
-
return mcpFailure(-32602, `No resource at ${uri}.`)
|
|
385
|
-
const { text, failed } = await this.#called(
|
|
386
|
-
resource,
|
|
387
|
-
undefined,
|
|
388
|
-
asker,
|
|
389
|
-
)
|
|
390
|
-
if (failed) return mcpFailure(-32603, text)
|
|
391
|
-
return {
|
|
392
|
-
result: {
|
|
393
|
-
contents: [{ uri, mimeType: 'application/json', text }],
|
|
394
|
-
},
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
357
|
default:
|
|
398
358
|
return mcpFailure(-32601, `No method ${method}.`)
|
|
399
359
|
}
|
|
@@ -404,8 +364,7 @@ export class McpSurface {
|
|
|
404
364
|
* Answers one MCP JSON-RPC request over `surface`, or `undefined` where it publishes nothing.
|
|
405
365
|
*
|
|
406
366
|
* 20.9 — THE STREAMABLE-HTTP MCP ENDPOINT, answering JSON-RPC over `POST`, as the request's own
|
|
407
|
-
* caller:
|
|
408
|
-
* every mutation, is a tool.
|
|
367
|
+
* caller: every handler it lists is a tool, a read as much as a mutation.
|
|
409
368
|
*/
|
|
410
369
|
export async function answerMcp(
|
|
411
370
|
surface: McpSurface,
|
package/src/server/versionOf.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// 18.7 — THE VERSION A VALUE'S STANDING PRODUCTION IS NAMED BY OVER THE WIRE. Stamped where it is
|
|
2
2
|
// asked for rather than where a production is minted, so a value no browser follows pays nothing on
|
|
3
3
|
// its production path. A production is told apart from the one before by the ordinal, what is held
|
|
4
|
-
//
|
|
5
|
-
// for a patch).
|
|
4
|
+
// and the patch epoch of what is held (5.7 moves no ordinal for a patch).
|
|
6
5
|
//
|
|
7
6
|
// ONE COUNTER FOR THE PROCESS, started at the boot time in microseconds, so no two productions share
|
|
8
7
|
// a number: not two values, not an entry evicted and built again, and not this process and the one
|
|
@@ -22,3 +22,9 @@ export const RESERVED_DIRECTORIES: ReadonlySet<string> = new Set([
|
|
|
22
22
|
LIB_DIRECTORY,
|
|
23
23
|
TESTS_DIRECTORY,
|
|
24
24
|
])
|
|
25
|
+
|
|
26
|
+
/** 23.16 — the fences a `server` directory nests in: it fences a graph rather than an address. */
|
|
27
|
+
export const HOLDS_SERVER: ReadonlySet<string> = new Set([
|
|
28
|
+
LIB_DIRECTORY,
|
|
29
|
+
TESTS_DIRECTORY,
|
|
30
|
+
])
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
// THE CURSOR. RULEBOOK 2.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// held. An app that wants every message keeps them — a channel is an array.
|
|
1
|
+
// THE CURSOR. RULEBOOK 2.6: a streaming producer's chunks, read from the chunk buffer by ordinal,
|
|
2
|
+
// so two chunks between two `await`s are two turns of the loop, and a patch or a write over the
|
|
3
|
+
// stream moves no place (5.7, 9.12). Where the run ENDS is the caller's settling, handed to
|
|
4
|
+
// `closeOn`: `for await` over a `Reactive` is `Reactive.ts`'s `producedRun`.
|
|
6
5
|
//
|
|
7
6
|
// A NARROW FACE rather than the `Cell`, the same way `tracking.ts` takes a `Tracked`: what a
|
|
8
7
|
// cursor reads is a handful of fields, and naming them keeps the cell's internals out of a
|
|
9
8
|
// second file.
|
|
10
9
|
|
|
11
|
-
import { PATCHES } from './PATCHES.ts'
|
|
12
10
|
import {
|
|
13
11
|
dropReader,
|
|
14
12
|
newSubscriber,
|
|
@@ -23,21 +21,14 @@ import {
|
|
|
23
21
|
* place in a stream's chunk buffer without the buffer having to hold one.
|
|
24
22
|
*/
|
|
25
23
|
export type Retained = Tracked & {
|
|
26
|
-
/** The window a `throttle` or a `debounce` holds open, `holding` while a production waits on it. */
|
|
27
|
-
cap: { holding: boolean } | null
|
|
28
24
|
ordinal: number
|
|
29
25
|
ring: unknown[]
|
|
30
26
|
ringBase: number
|
|
31
|
-
servable: boolean
|
|
32
|
-
state: number
|
|
33
|
-
/**
|
|
34
|
-
* Non-null on a streaming producer, whose ring is the accumulation of the run being served
|
|
35
|
-
* (9.1) and is retained whole — the chunks ARE the value.
|
|
36
|
-
*/
|
|
37
|
-
stream: object | null
|
|
38
|
-
value: unknown
|
|
39
27
|
}
|
|
40
28
|
|
|
29
|
+
/** What `take` answers when the cursor has caught up — never a chunk, which may be `undefined`. */
|
|
30
|
+
export const CAUGHT_UP: unique symbol = Symbol('caught up')
|
|
31
|
+
|
|
41
32
|
// The cursors standing over each source, for a patch to keep the place of the ones behind (5.7).
|
|
42
33
|
const CURSORS = new WeakMap<Retained, Set<Cursor>>()
|
|
43
34
|
|
|
@@ -70,61 +61,36 @@ export function hasCursorBehind(source: Retained): boolean {
|
|
|
70
61
|
}
|
|
71
62
|
|
|
72
63
|
/**
|
|
73
|
-
* A
|
|
64
|
+
* A reader of a stream's chunks, in order, waking whoever waits on it when one lands.
|
|
74
65
|
*
|
|
75
|
-
* A RECORD, NOT A SCOPE.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* function object hanging off it is live that whole time. Fields on a prototype are the object
|
|
79
|
-
* alone, and `at` — read and written on every turn — becomes a property load on a fixed structure
|
|
80
|
-
* rather than a scope load.
|
|
66
|
+
* A RECORD, NOT A SCOPE. A cursor outlives the call that made it — a `for await` holds one for the
|
|
67
|
+
* life of the loop — so fields on a prototype are the object alone, and `at`, read and written on
|
|
68
|
+
* every turn, is a property load on a fixed structure rather than a scope load.
|
|
81
69
|
*/
|
|
82
|
-
export class Cursor
|
|
70
|
+
export class Cursor {
|
|
83
71
|
source: Retained
|
|
84
|
-
/** The ordinal of the last
|
|
72
|
+
/** The ordinal of the last chunk this cursor handed over. */
|
|
85
73
|
at: number
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
patched: number
|
|
93
|
-
/**
|
|
94
|
-
* The turns asked for and not yet answered.
|
|
95
|
-
*
|
|
96
|
-
* A QUEUE rather than one slot because a consumer may ask twice before either answer arrives
|
|
97
|
-
* — `Promise.all([ it.next(), it.next() ])` is the shape, and with one slot the second `next`
|
|
98
|
-
* overwrote the first's resolver and that promise never settled at all. `for await` asks one
|
|
99
|
-
* at a time, which is why nothing here said so.
|
|
100
|
-
*/
|
|
101
|
-
waiting: ((result: IteratorResult<unknown>) => void)[]
|
|
74
|
+
/** Called once at the next wake, then cleared. */
|
|
75
|
+
wake: (() => void) | null
|
|
76
|
+
/** Whether the run `closeOn` was handed has settled. */
|
|
77
|
+
closed: boolean
|
|
78
|
+
/** What that run rejected with. */
|
|
79
|
+
failure: unknown
|
|
102
80
|
reader: Subscriber
|
|
103
81
|
|
|
104
82
|
constructor(source: Retained, from: number) {
|
|
105
83
|
this.source = source
|
|
106
84
|
this.at = from
|
|
107
|
-
this.
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// rather than discovered — see the same shape in `Reactive`'s `settled`.
|
|
85
|
+
this.wake = null
|
|
86
|
+
this.closed = false
|
|
87
|
+
this.failure = undefined
|
|
88
|
+
// The one closure left, and it captures this cursor and nothing else. `notify` is called as a
|
|
89
|
+
// method on the SUBSCRIBER, so it cannot be the cursor's own method. Registered outside the
|
|
90
|
+
// tracking protocol, so the source it waits on is declared rather than discovered — see the
|
|
91
|
+
// same shape in `Reactive`'s `settled`.
|
|
115
92
|
this.reader = newSubscriber([source], null)
|
|
116
|
-
|
|
117
|
-
// lands three productions on a consumer holding two turns answers both.
|
|
118
|
-
this.reader.notify = (): void => {
|
|
119
|
-
const waiting = this.waiting
|
|
120
|
-
while (waiting.length !== 0) {
|
|
121
|
-
const next = this.take()
|
|
122
|
-
if (next === null) return
|
|
123
|
-
;(waiting.shift() as (result: IteratorResult<unknown>) => void)(
|
|
124
|
-
next,
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
93
|
+
this.reader.notify = (): void => this.woken()
|
|
128
94
|
source.readers.push(this.reader)
|
|
129
95
|
let cursors = CURSORS.get(source)
|
|
130
96
|
if (cursors === undefined) {
|
|
@@ -135,83 +101,60 @@ export class Cursor implements AsyncIterator<unknown> {
|
|
|
135
101
|
}
|
|
136
102
|
|
|
137
103
|
/**
|
|
138
|
-
*
|
|
104
|
+
* The next chunk, or `CAUGHT_UP`.
|
|
139
105
|
*
|
|
140
|
-
* Reading
|
|
106
|
+
* Reading by ordinal is what keeps a burst from being a gap or a repeat.
|
|
141
107
|
*/
|
|
142
|
-
take():
|
|
108
|
+
take(): unknown {
|
|
143
109
|
const source = this.source
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (value === undefined) return null
|
|
149
|
-
const patched =
|
|
150
|
-
value !== null && typeof value === 'object'
|
|
151
|
-
? (PATCHES.written.get(value) ?? 0)
|
|
152
|
-
: 0
|
|
153
|
-
if (at >= source.ordinal && patched === this.patched) return null
|
|
154
|
-
// 5.9 — A PRODUCTION A WINDOW HOLDS has minted its ordinal and is not yet the value: the
|
|
155
|
-
// turn is the release's, which wakes this cursor.
|
|
156
|
-
if (source.cap?.holding === true) return null
|
|
157
|
-
// The standing production is the only one held, so everything minted or patched
|
|
158
|
-
// since the last turn collapses into it.
|
|
159
|
-
this.at = source.ordinal
|
|
160
|
-
this.patched = patched
|
|
161
|
-
return { value, done: false }
|
|
162
|
-
}
|
|
163
|
-
if (at >= source.ordinal) return null
|
|
164
|
-
// A run that restarted moved the buffer past this cursor: answer from its start.
|
|
165
|
-
if (at < source.ringBase) at = source.ringBase
|
|
166
|
-
const value = source.ring[at - source.ringBase]
|
|
167
|
-
this.at = at + 1
|
|
168
|
-
return { value, done: false }
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
next(): Promise<IteratorResult<unknown>> {
|
|
172
|
-
if (!this.reader.alive)
|
|
173
|
-
return Promise.resolve({ value: undefined, done: true })
|
|
174
|
-
// A TURN ALREADY ASKED FOR AND NOT ANSWERED KEEPS THE ORDER: taking from the
|
|
175
|
-
// ring here while one is queued would hand the later caller the earlier
|
|
176
|
-
// production.
|
|
177
|
-
if (this.waiting.length === 0) {
|
|
178
|
-
const ready = this.take()
|
|
179
|
-
// NOT `async`, and no `await` on the caught-up arm. A cursor behind a
|
|
180
|
-
// stream's chunks is the common shape while a burst drains, and an
|
|
181
|
-
// `async` here would put a microtask tick between every one of those
|
|
182
|
-
// turns.
|
|
183
|
-
if (ready !== null) return Promise.resolve(ready)
|
|
184
|
-
}
|
|
185
|
-
const waiting = this.waiting
|
|
186
|
-
return new Promise((resolve) => {
|
|
187
|
-
waiting.push(resolve)
|
|
188
|
-
})
|
|
110
|
+
if (this.at >= source.ordinal) return CAUGHT_UP
|
|
111
|
+
const value = source.ring[this.at - source.ringBase]
|
|
112
|
+
this.at += 1
|
|
113
|
+
return value
|
|
189
114
|
}
|
|
190
115
|
|
|
191
116
|
/**
|
|
192
|
-
* Ends the cursor
|
|
117
|
+
* Ends the cursor's run where `run` settles: `closed` is set, `failure` holds a rejection, and
|
|
118
|
+
* the current `turn` wakes.
|
|
193
119
|
*
|
|
194
|
-
*
|
|
120
|
+
* ONE REACTION ON THE CLOSE FOR THE WHOLE RUN, never one per chunk held until the run ends. A
|
|
121
|
+
* chunk landing in the same turn as the close is still unread when the close wakes the caller,
|
|
122
|
+
* so a drain before reading `closed` hands it over rather than losing it to whichever settled
|
|
123
|
+
* first.
|
|
195
124
|
*/
|
|
196
|
-
|
|
197
|
-
|
|
125
|
+
closeOn(run: PromiseLike<unknown>): void {
|
|
126
|
+
run.then(
|
|
127
|
+
() => {
|
|
128
|
+
this.closed = true
|
|
129
|
+
this.woken()
|
|
130
|
+
},
|
|
131
|
+
(error: unknown) => {
|
|
132
|
+
this.closed = true
|
|
133
|
+
this.failure = error
|
|
134
|
+
this.woken()
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Settles at the next chunk or the close, whichever lands first. */
|
|
140
|
+
turn(): Promise<void> {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
this.wake = resolve
|
|
143
|
+
})
|
|
198
144
|
}
|
|
199
145
|
|
|
200
|
-
|
|
201
|
-
|
|
146
|
+
private woken(): void {
|
|
147
|
+
const wake = this.wake
|
|
148
|
+
this.wake = null
|
|
149
|
+
wake?.()
|
|
202
150
|
}
|
|
203
151
|
|
|
204
|
-
|
|
152
|
+
/** Drops the subscriber; nothing else does. */
|
|
153
|
+
stop(): void {
|
|
205
154
|
const reader = this.reader
|
|
206
155
|
reader.alive = false
|
|
207
156
|
dropReader(this.source, reader)
|
|
208
157
|
CURSORS.get(this.source)?.delete(this)
|
|
209
|
-
|
|
210
|
-
while (waiting.length !== 0)
|
|
211
|
-
(waiting.shift() as (result: IteratorResult<unknown>) => void)({
|
|
212
|
-
value: undefined,
|
|
213
|
-
done: true,
|
|
214
|
-
})
|
|
215
|
-
return Promise.resolve({ value: undefined, done: true })
|
|
158
|
+
this.wake = null
|
|
216
159
|
}
|
|
217
160
|
}
|