@abide/abide 0.104.0 → 0.105.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 +16 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +17 -17
- package/docs/RULEBOOK.md +50 -12
- package/docs/abide.md +9 -9
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +45 -86
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -20
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +11 -9
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +180 -512
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +92 -123
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +49 -71
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +4 -2
- package/src/shared/reactive/Reactive.ts +285 -197
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -123
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/reconcile.ts +4 -110
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -33
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
package/src/shared/framing.ts
CHANGED
|
@@ -1,46 +1,26 @@
|
|
|
1
|
-
// 16.32 — A FRAMED ANSWER, read one JSON value per
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// 16.32 — A FRAMED ANSWER, read one JSON value per frame: each line for jsonl, the `data` of each
|
|
2
|
+
// event for sse. One reader, `frameReader`, behind both a body read to the end and one read as it
|
|
3
|
+
// lands — and behind `abide watch`, which reads an event stream's events by name. The writer is the
|
|
4
|
+
// server's (`server/streamEvent.ts`).
|
|
4
5
|
//
|
|
5
6
|
// 16.68 — AND ITS LAST FRAME MAY BE A REFUSAL: one that reached the stream after it had answered, so
|
|
6
|
-
// after its status was sent. An sse `event: refusal` and the Problem Details
|
|
7
|
+
// after its status was sent. An sse `event: refusal` and the Problem Details as its `data`, or a
|
|
7
8
|
// jsonl line `{"$refusal": <Problem Details>}` — read here as the refusal, thrown where it lands.
|
|
8
9
|
|
|
9
|
-
import { failedFrom, isProblem
|
|
10
|
+
import { failedFrom, isProblem } from './Failed.ts'
|
|
11
|
+
import { REFUSAL_KEY } from './REFUSAL_KEY.ts'
|
|
10
12
|
|
|
11
|
-
/** The
|
|
12
|
-
|
|
13
|
+
/** The two framings a streamed JSON answer is read in. 17.6. */
|
|
14
|
+
export type Framed = 'jsonl' | 'sse'
|
|
13
15
|
|
|
14
|
-
/**
|
|
15
|
-
export function
|
|
16
|
-
return prefix === ''
|
|
17
|
-
? `${JSON.stringify({ [REFUSAL_KEY]: problem })}\n`
|
|
18
|
-
: `event: refusal\n${prefix}${JSON.stringify(problem)}\n\n`
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// What a read of one framed body carries from one line to the next: whether an sse event named the
|
|
22
|
-
// data line after it a refusal.
|
|
23
|
-
type Reading = { refusal: boolean }
|
|
24
|
-
|
|
25
|
-
// A refusal's frame, read: thrown where it lands among the chunks.
|
|
26
|
-
class Refused {
|
|
27
|
-
problem: Problem
|
|
28
|
-
constructor(problem: Problem) {
|
|
29
|
-
this.problem = problem
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* What precedes a chunk's JSON on its line: nothing for jsonl, `data: ` for sse, and `null` where
|
|
35
|
-
* the answer is not framed at all.
|
|
36
|
-
*/
|
|
37
|
-
export function framingPrefix(type: string | null | undefined): string | null {
|
|
16
|
+
/** How an answer at `type` is framed, or `null` where it is not framed at all. */
|
|
17
|
+
export function framingOf(type: string | null | undefined): Framed | null {
|
|
38
18
|
if (
|
|
39
19
|
type?.startsWith('application/jsonl') ||
|
|
40
20
|
type?.startsWith('application/x-ndjson')
|
|
41
21
|
)
|
|
42
|
-
return ''
|
|
43
|
-
if (type?.startsWith('text/event-stream')) return '
|
|
22
|
+
return 'jsonl'
|
|
23
|
+
if (type?.startsWith('text/event-stream')) return 'sse'
|
|
44
24
|
return null
|
|
45
25
|
}
|
|
46
26
|
|
|
@@ -52,24 +32,102 @@ export function framingPrefix(type: string | null | undefined): string | null {
|
|
|
52
32
|
*/
|
|
53
33
|
export function carriesBytes(type: string | null | undefined): boolean {
|
|
54
34
|
if (type === null || type === undefined || type === '') return false
|
|
55
|
-
if (
|
|
35
|
+
if (framingOf(type) !== null) return false
|
|
56
36
|
const semicolon = type.indexOf(';')
|
|
57
37
|
const essence = semicolon === -1 ? type : type.slice(0, semicolon)
|
|
58
38
|
return essence !== 'application/json' && !essence.endsWith('+json')
|
|
59
39
|
}
|
|
60
40
|
|
|
41
|
+
/** One frame: an sse event's name — `message` where it named none — `id` and `data`, or a jsonl line. */
|
|
42
|
+
export type Frame = { name: string; id: string | undefined; data: string }
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A framed body's reader, fed its text as it arrives: pushes each frame the text completes onto
|
|
46
|
+
* `frames` — and, where `ended`, the last one too.
|
|
47
|
+
*
|
|
48
|
+
* An event stream is read as the format says (18.27) — fields by name, a comment skipped, `data`
|
|
49
|
+
* lines joined, the event ending at a blank line. A jsonl line is a frame with nothing but its data.
|
|
50
|
+
* A closure over the read's own state, which is all it captures.
|
|
51
|
+
*/
|
|
52
|
+
function frameReader(
|
|
53
|
+
lines: boolean,
|
|
54
|
+
): (text: string, ended: boolean, frames: Frame[]) => void {
|
|
55
|
+
// The text after the last newline read, held until its line completes.
|
|
56
|
+
let rest = ''
|
|
57
|
+
let name = 'message'
|
|
58
|
+
let id: string | undefined
|
|
59
|
+
let data: string | undefined
|
|
60
|
+
return (text, ended, frames) => {
|
|
61
|
+
// A long line across many reads is held unscanned until its newline lands, or each read
|
|
62
|
+
// rescans everything before it.
|
|
63
|
+
if (!ended && !text.includes('\n')) {
|
|
64
|
+
rest += text
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
const read = (rest + text).split('\n')
|
|
68
|
+
// The unfinished last line waits for the next read; at the end, a blank line ends it.
|
|
69
|
+
rest = ended ? '' : (read.pop() as string)
|
|
70
|
+
if (ended) read.push('')
|
|
71
|
+
for (const line of read) {
|
|
72
|
+
// A blank line ends an event; a jsonl line is one.
|
|
73
|
+
if (lines || line === '') {
|
|
74
|
+
const complete = lines ? line || undefined : data
|
|
75
|
+
if (complete !== undefined)
|
|
76
|
+
frames.push({ name, id, data: complete })
|
|
77
|
+
name = 'message'
|
|
78
|
+
id = data = undefined
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
// A field and its value, the one space after the colon dropped and a CRLF's `\r` with
|
|
82
|
+
// it; a line with no colon is a field with no value. A comment — an event stream's
|
|
83
|
+
// opener — names the field ``, which is none.
|
|
84
|
+
const colon = line.indexOf(':')
|
|
85
|
+
const field = colon === -1 ? line : line.slice(0, colon)
|
|
86
|
+
if (field !== 'data' && field !== 'event' && field !== 'id')
|
|
87
|
+
continue
|
|
88
|
+
let start = colon === -1 ? line.length : colon + 1
|
|
89
|
+
if (line.charCodeAt(start) === 32) start += 1
|
|
90
|
+
const value = line.slice(
|
|
91
|
+
start,
|
|
92
|
+
line.charCodeAt(line.length - 1) === 13
|
|
93
|
+
? line.length - 1
|
|
94
|
+
: line.length,
|
|
95
|
+
)
|
|
96
|
+
if (field === 'data')
|
|
97
|
+
data = data === undefined ? value : `${data}\n${value}`
|
|
98
|
+
else if (field === 'event') name = value
|
|
99
|
+
else id = value
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The chunk a frame carried, or — where it is a refusal (16.68) — that refusal, thrown.
|
|
106
|
+
*
|
|
107
|
+
* Per chunk: the reserved key is tested before any key list is built.
|
|
108
|
+
*/
|
|
109
|
+
function chunkOf(frame: Frame, lines: boolean): unknown {
|
|
110
|
+
const value = JSON.parse(frame.data)
|
|
111
|
+
const problem = !lines
|
|
112
|
+
? frame.name === 'refusal' && value
|
|
113
|
+
: typeof value === 'object' &&
|
|
114
|
+
Object.hasOwn(Object(value), REFUSAL_KEY) &&
|
|
115
|
+
Object.keys(value).length === 1 &&
|
|
116
|
+
value[REFUSAL_KEY]
|
|
117
|
+
if (isProblem(problem)) throw failedFrom(problem)
|
|
118
|
+
return value
|
|
119
|
+
}
|
|
120
|
+
|
|
61
121
|
/**
|
|
62
|
-
* A streamed answer's chunks: each JSON value its framing carried, or, for bytes (16.46)
|
|
63
|
-
* `Uint8Array` as it was read
|
|
64
|
-
* handler yielded.
|
|
122
|
+
* A streamed answer's chunks: each JSON value its `framing` carried, or, for bytes (16.46) — an
|
|
123
|
+
* unread body with no framing — each `Uint8Array` as it was read: bytes have no framing to split
|
|
124
|
+
* them back into the chunks the handler yielded.
|
|
65
125
|
*/
|
|
66
126
|
export function answerChunks(
|
|
67
127
|
body: ReadableStream<Uint8Array>,
|
|
68
|
-
|
|
128
|
+
framing: Framed | null,
|
|
69
129
|
): AsyncIterable<unknown> {
|
|
70
|
-
return
|
|
71
|
-
? bytesStream(body)
|
|
72
|
-
: framedStream(body, framingPrefix(type) ?? '')
|
|
130
|
+
return framing === null ? bytesStream(body) : framedStream(body, framing)
|
|
73
131
|
}
|
|
74
132
|
|
|
75
133
|
/**
|
|
@@ -92,113 +150,54 @@ async function* bytesStream(
|
|
|
92
150
|
}
|
|
93
151
|
}
|
|
94
152
|
|
|
95
|
-
/** The chunks a framed text carried, every
|
|
96
|
-
export function framedChunks(text: string,
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
153
|
+
/** The chunks a framed text carried, every frame of it complete. */
|
|
154
|
+
export function framedChunks(text: string, framing: Framed): unknown[] {
|
|
155
|
+
const lines = framing === 'jsonl'
|
|
156
|
+
const frames: Frame[] = []
|
|
157
|
+
frameReader(lines)(text, true, frames)
|
|
158
|
+
return frames.map((frame) => chunkOf(frame, lines))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** An event stream's events, yielded as each completes. */
|
|
162
|
+
export function eventsOf(
|
|
163
|
+
body: ReadableStream<Uint8Array>,
|
|
164
|
+
): AsyncGenerator<Frame> {
|
|
165
|
+
return framedStream(body, 'sse', true) as AsyncGenerator<Frame>
|
|
102
166
|
}
|
|
103
167
|
|
|
104
168
|
/**
|
|
105
|
-
* A framed body's chunks, yielded as each
|
|
169
|
+
* A framed body's chunks — or, where `events`, its frames as they are — yielded as each frame
|
|
170
|
+
* completes.
|
|
106
171
|
*
|
|
107
172
|
* Canceled when the reader stops reading — a refresh starts a new run and the old one's reader has
|
|
108
173
|
* nothing left to hand anything.
|
|
109
174
|
*/
|
|
110
175
|
export async function* framedStream(
|
|
111
176
|
body: ReadableStream<Uint8Array>,
|
|
112
|
-
|
|
177
|
+
framing: Framed,
|
|
178
|
+
events?: true,
|
|
113
179
|
): AsyncGenerator<unknown> {
|
|
180
|
+
const lines = framing === 'jsonl'
|
|
114
181
|
const reader = body.getReader()
|
|
115
182
|
const decoder = new TextDecoder()
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
let buffered = ''
|
|
183
|
+
const framed = frameReader(lines)
|
|
184
|
+
const frames: Frame[] = []
|
|
119
185
|
try {
|
|
120
186
|
for (;;) {
|
|
121
187
|
const { done, value } = await reader.read()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// rescans everything before it.
|
|
127
|
-
if (!done && !decoded.includes('\n')) {
|
|
128
|
-
buffered += decoded
|
|
129
|
-
continue
|
|
130
|
-
}
|
|
131
|
-
buffered = readLines(
|
|
132
|
-
buffered + decoded,
|
|
133
|
-
prefix,
|
|
188
|
+
framed(
|
|
189
|
+
done
|
|
190
|
+
? decoder.decode()
|
|
191
|
+
: decoder.decode(value, { stream: true }),
|
|
134
192
|
done,
|
|
135
|
-
|
|
136
|
-
reading,
|
|
193
|
+
frames,
|
|
137
194
|
)
|
|
138
|
-
for (const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
chunks.length = 0
|
|
195
|
+
for (const frame of frames)
|
|
196
|
+
yield events ? frame : chunkOf(frame, lines)
|
|
197
|
+
frames.length = 0
|
|
143
198
|
if (done) return
|
|
144
199
|
}
|
|
145
200
|
} finally {
|
|
146
201
|
void reader.cancel().catch(() => {})
|
|
147
202
|
}
|
|
148
203
|
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Parses each complete line of `text` into `chunks`, and the unfinished last one too where `ended`.
|
|
152
|
-
*
|
|
153
|
-
* Answers what is left of `text` after the last newline.
|
|
154
|
-
*/
|
|
155
|
-
function readLines(
|
|
156
|
-
text: string,
|
|
157
|
-
prefix: string,
|
|
158
|
-
ended: boolean,
|
|
159
|
-
chunks: unknown[],
|
|
160
|
-
reading: Reading,
|
|
161
|
-
): string {
|
|
162
|
-
let start = 0
|
|
163
|
-
for (;;) {
|
|
164
|
-
const newline = text.indexOf('\n', start)
|
|
165
|
-
if (newline === -1 && !ended) return text.slice(start)
|
|
166
|
-
const end = newline === -1 ? text.length : newline
|
|
167
|
-
if (prefix !== '' && text.startsWith('event: refusal', start))
|
|
168
|
-
reading.refusal = true
|
|
169
|
-
else if (end > start && text.startsWith(prefix, start)) {
|
|
170
|
-
const value: unknown = JSON.parse(
|
|
171
|
-
text.slice(start + prefix.length, end),
|
|
172
|
-
)
|
|
173
|
-
chunks.push(refusalIn(value, prefix, reading) ?? value)
|
|
174
|
-
reading.refusal = false
|
|
175
|
-
}
|
|
176
|
-
if (newline === -1) return ''
|
|
177
|
-
start = newline + 1
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// The refusal a value carries as its frame: the data line an sse event named one, or a jsonl line
|
|
182
|
-
// holding the one reserved key and Problem Details under it.
|
|
183
|
-
function refusalIn(
|
|
184
|
-
value: unknown,
|
|
185
|
-
prefix: string,
|
|
186
|
-
reading: Reading,
|
|
187
|
-
): Refused | undefined {
|
|
188
|
-
if (prefix !== '')
|
|
189
|
-
return reading.refusal && isProblem(value)
|
|
190
|
-
? new Refused(value)
|
|
191
|
-
: undefined
|
|
192
|
-
// Per chunk: the reserved key is tested before any key list is built.
|
|
193
|
-
if (
|
|
194
|
-
value === null ||
|
|
195
|
-
typeof value !== 'object' ||
|
|
196
|
-
!Object.hasOwn(value, REFUSAL_KEY)
|
|
197
|
-
)
|
|
198
|
-
return undefined
|
|
199
|
-
const keys = Object.keys(value)
|
|
200
|
-
const problem = (value as Record<string, unknown>)[REFUSAL_KEY]
|
|
201
|
-
return keys.length === 1 && isProblem(problem)
|
|
202
|
-
? new Refused(problem)
|
|
203
|
-
: undefined
|
|
204
|
-
}
|
package/src/shared/queryText.ts
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A record of members as a query string.
|
|
3
3
|
*
|
|
4
|
-
* ONE QUERY-STRING RULE for `rpcRequest`'s args (16.18) and `url`'s `Query
|
|
4
|
+
* 30.28 — ONE QUERY-STRING RULE for `rpcRequest`'s args (16.18) and `url`'s `Query`: a member left
|
|
5
5
|
* `undefined` is omitted, a list is its member repeated, and anything else is its text — `null`
|
|
6
|
-
* included, which 19.7 reads back as `null` where the schema declares it.
|
|
7
|
-
*
|
|
6
|
+
* included, which 19.7 reads back as `null` where the schema declares it. AN EMPTY LIST IS ITS NAME
|
|
7
|
+
* WITH NO `=`, last, which no text value spells — `tags=` is the one-member list `['']` — so 19.7
|
|
8
|
+
* reads it back as `[]` rather than losing the member.
|
|
8
9
|
*/
|
|
9
10
|
export function queryText(members: Record<string, unknown>): string {
|
|
10
11
|
const query = new URLSearchParams()
|
|
12
|
+
let bare = ''
|
|
11
13
|
for (const [name, value] of Object.entries(members)) {
|
|
12
14
|
if (value === undefined) continue
|
|
13
|
-
if (Array.isArray(value))
|
|
14
|
-
|
|
15
|
-
else query.append(name, String(
|
|
15
|
+
if (!Array.isArray(value)) query.append(name, String(value))
|
|
16
|
+
else if (value.length === 0) bare += `&${bareName(name)}`
|
|
17
|
+
else for (const each of value) query.append(name, String(each))
|
|
16
18
|
}
|
|
17
|
-
|
|
19
|
+
const text = query.toString()
|
|
20
|
+
return text === '' ? bare.slice(1) : text + bare
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A query member's name in the encoding `URLSearchParams` gives it, without its `=`. */
|
|
24
|
+
export function bareName(name: string): string {
|
|
25
|
+
return new URLSearchParams([[name, '']]).toString().slice(0, -1)
|
|
18
26
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// THE CURSOR. RULEBOOK 2.6: a streaming producer's chunks, read from the chunk buffer by ordinal,
|
|
2
2
|
// so two chunks between two `await`s are two turns of the loop, and a patch or a write over the
|
|
3
3
|
// stream moves no place (5.7, 9.12). Where the run ENDS is the caller's settling, handed to
|
|
4
|
-
// `closeOn`:
|
|
4
|
+
// `closeOn`: one run's reader is `Reactive.ts`'s `runChunks`.
|
|
5
5
|
//
|
|
6
6
|
// A NARROW FACE rather than the `Cell`, the same way `tracking.ts` takes a `Tracked`: what a
|
|
7
7
|
// cursor reads is a handful of fields, and naming them keeps the cell's internals out of a
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from './tracking.ts'
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* What a cursor reads
|
|
18
|
+
* What a cursor reads of a cell.
|
|
19
19
|
*
|
|
20
20
|
* `ring[index]` carries ordinal `ringBase + index + 1`, so a cursor holding an ordinal finds its
|
|
21
21
|
* place in a stream's chunk buffer without the buffer having to hold one.
|
|
@@ -92,6 +92,8 @@ export class Cursor {
|
|
|
92
92
|
this.reader = newSubscriber([source], null)
|
|
93
93
|
this.reader.notify = (): void => this.woken()
|
|
94
94
|
source.readers.push(this.reader)
|
|
95
|
+
// ONE PLACED PAST EVERY CHUNK is a wake and never behind, so no patch has a place to keep.
|
|
96
|
+
if (from === Number.POSITIVE_INFINITY) return
|
|
95
97
|
let cursors = CURSORS.get(source)
|
|
96
98
|
if (cursors === undefined) {
|
|
97
99
|
cursors = new Set()
|