@atom.io/template-react-realtime-text-editor 0.0.11 → 0.0.12

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.
@@ -0,0 +1,238 @@
1
+ import type { Silo } from "atom.io"
2
+ import type {
3
+ MosaicTextIndexLookup,
4
+ MosaicTextSelection,
5
+ } from "@atom.io/realtime"
6
+ import {
7
+ createMosaicDomainHistoryClient,
8
+ createMosaicDomainHistorySocketTransport,
9
+ createMosaicDomainPresenceClient,
10
+ createMosaicDomainPresenceSocketTransport,
11
+ createMosaicDomainResidencyClient,
12
+ createMosaicDomainResidencySocketTransport,
13
+ createMosaicDomainSessionClient,
14
+ createMosaicTextProjectionClient,
15
+ type MosaicDomainHistoryClient,
16
+ type MosaicDomainPresenceClient,
17
+ type MosaicDomainResidencyClient,
18
+ type MosaicDomainSessionClientState,
19
+ type MosaicTextProjectionClient,
20
+ type MosaicTextLogicalEdit,
21
+ } from "@atom.io/realtime/client"
22
+ import type { Socket } from "socket.io-client"
23
+
24
+ import {
25
+ activateMarkdownDocumentDomain,
26
+ markdownPresenceKey,
27
+ type MarkdownDocumentDomain,
28
+ type MarkdownPresence,
29
+ } from "./document-domain.ts"
30
+ import type { Identity } from "./identities.ts"
31
+ import {
32
+ MARKDOWN_EVENTS,
33
+ type MarkdownAcknowledgement,
34
+ type MarkdownCommand,
35
+ } from "./protocol.ts"
36
+
37
+ type MarkdownRange = { end: number; kind: `utf16-range`; start: number }
38
+ export type MarkdownClientStatus = MosaicDomainSessionClientState
39
+
40
+ export type MarkdownCollaborationClient = Disposable & {
41
+ readonly domain: MarkdownDocumentDomain
42
+ readonly history: MosaicDomainHistoryClient
43
+ readonly identity: Identity
44
+ readonly presence: MosaicDomainPresenceClient
45
+ readonly projection: MosaicTextProjectionClient<
46
+ MarkdownDocumentDomain[`identity`],
47
+ MarkdownRange
48
+ >
49
+ publishPresence(
50
+ value: Omit<MarkdownPresence, `actor` | `session`>,
51
+ ): Promise<void>
52
+ redo(): Promise<boolean>
53
+ replace(input: {
54
+ readonly selection: MosaicTextSelection
55
+ readonly text: string
56
+ }): Promise<void>
57
+ readonly residency: MosaicDomainResidencyClient<
58
+ MarkdownDocumentDomain[`identity`],
59
+ MarkdownRange
60
+ >
61
+ readonly sessionId: string
62
+ status(): MarkdownClientStatus
63
+ subscribe(listener: (status: MarkdownClientStatus) => void): () => void
64
+ undo(): Promise<boolean>
65
+ }
66
+
67
+ const request = <Value>(
68
+ socket: Socket,
69
+ event: string,
70
+ ...parameters: readonly unknown[]
71
+ ): Promise<Value> =>
72
+ new Promise((resolve, reject) => {
73
+ if (!socket.connected) {
74
+ reject(new Error(`offline`))
75
+ return
76
+ }
77
+ let settled = false
78
+ const finish = (result: () => void): void => {
79
+ if (settled) return
80
+ settled = true
81
+ socket.off(`disconnect`, disconnected)
82
+ result()
83
+ }
84
+ const disconnected = (): void => {
85
+ finish(() => {
86
+ reject(new DOMException(`Socket disconnected`, `AbortError`))
87
+ })
88
+ }
89
+ socket.once(`disconnect`, disconnected)
90
+ socket.emit(
91
+ event,
92
+ ...parameters,
93
+ (acknowledgement: MarkdownAcknowledgement<Value>) => {
94
+ finish(() => {
95
+ if (acknowledgement.ok) resolve(acknowledgement.value)
96
+ else reject(new Error(acknowledgement.reason))
97
+ })
98
+ },
99
+ )
100
+ })
101
+
102
+ export async function createMarkdownCollaborationClient(options: {
103
+ readonly identity: Identity
104
+ readonly presenceRenewalMs?: number
105
+ readonly sessionId: string
106
+ readonly silo: Pick<Silo, `install` | `store`>
107
+ readonly socket: Socket
108
+ }): Promise<MarkdownCollaborationClient> {
109
+ const { identity, sessionId, socket } = options
110
+ const presenceRenewalMs = options.presenceRenewalMs ?? 5_000
111
+ if (!Number.isSafeInteger(presenceRenewalMs) || presenceRenewalMs < 1) {
112
+ throw new Error(`Presence renewal must be a positive integer.`)
113
+ }
114
+ const domain = await activateMarkdownDocumentDomain({ silo: options.silo })
115
+ const residencyTransport = createMosaicDomainResidencySocketTransport<
116
+ MarkdownDocumentDomain[`identity`],
117
+ MarkdownRange
118
+ >(socket, {
119
+ idSource: () => `${sessionId}:residency:${crypto.randomUUID()}`,
120
+ })
121
+ const residency = createMosaicDomainResidencyClient({
122
+ actor: identity.id,
123
+ domain,
124
+ maxResidentBytes: 4 * 1024 * 1024,
125
+ maxResidentMembers: 128,
126
+ session: sessionId,
127
+ transport: residencyTransport,
128
+ })
129
+ const projection = createMosaicTextProjectionClient({
130
+ actor: identity.id,
131
+ domainKey: `markdown-body`,
132
+ evictReleased: true,
133
+ materialize: () => request(socket, MARKDOWN_EVENTS.materialize),
134
+ maximumActiveRanges: 8,
135
+ maximumRangeUtf16Units: 65_536,
136
+ planEdit(_edit: MosaicTextLogicalEdit) {
137
+ throw new Error(
138
+ `Markdown commands are authoritatively composed with index maintenance.`,
139
+ )
140
+ },
141
+ positionAtOffset: (offset) =>
142
+ request<MosaicTextIndexLookup>(
143
+ socket,
144
+ MARKDOWN_EVENTS.positionAtOffset,
145
+ offset,
146
+ ),
147
+ rangeMember: `indexMembers`,
148
+ rangeMemberLimit: 64,
149
+ residency,
150
+ resolvePosition: (position) =>
151
+ request(socket, MARKDOWN_EVENTS.resolvePosition, position),
152
+ rootAddress: domain.address(`indexRoot`),
153
+ session: sessionId,
154
+ })
155
+ const presenceTransport = createMosaicDomainPresenceSocketTransport(socket, {
156
+ idSource: () => `${sessionId}:presence:${crypto.randomUUID()}`,
157
+ })
158
+ const presence = createMosaicDomainPresenceClient({
159
+ domain,
160
+ renewalMs: presenceRenewalMs,
161
+ session: sessionId,
162
+ transport: presenceTransport,
163
+ })
164
+ const historyTransport = createMosaicDomainHistorySocketTransport(socket, {
165
+ idSource: () => `${sessionId}:history:${crypto.randomUUID()}`,
166
+ })
167
+ const history = createMosaicDomainHistoryClient({
168
+ actor: identity.id,
169
+ onObserverError: (error) => {
170
+ domain.store.logger.error(
171
+ `🐞`,
172
+ `unknown`,
173
+ sessionId,
174
+ `A Mosaic Domain history client listener threw.`,
175
+ error,
176
+ )
177
+ },
178
+ session: sessionId,
179
+ transport: historyTransport,
180
+ })
181
+ const session = await createMosaicDomainSessionClient<MarkdownCommand>({
182
+ history,
183
+ presence,
184
+ residency,
185
+ send: (command) => request(socket, MARKDOWN_EVENTS.command, command),
186
+ socket,
187
+ })
188
+
189
+ return {
190
+ domain,
191
+ history,
192
+ identity,
193
+ presence,
194
+ projection,
195
+ publishPresence(value) {
196
+ const latest = structuredClone({
197
+ ...value,
198
+ actor: identity.id,
199
+ session: sessionId,
200
+ })
201
+ return presence.publish(
202
+ domain.address(`collaborator`, markdownPresenceKey(latest)),
203
+ latest,
204
+ )
205
+ },
206
+ redo: () => session.history(`redo`),
207
+ replace({ selection, text }) {
208
+ return session.submit((sequence) => ({
209
+ anchor: selection.anchor,
210
+ gestureId: `${identity.id}:${sessionId}:edit:${sequence}`,
211
+ head: selection.head,
212
+ sequence,
213
+ text,
214
+ type: `replace`,
215
+ }))
216
+ },
217
+ residency,
218
+ sessionId,
219
+ status: session.state,
220
+ subscribe: session.subscribe,
221
+ undo: () => session.history(`undo`),
222
+ [Symbol.dispose]() {
223
+ session[Symbol.dispose]()
224
+ presence[Symbol.dispose]()
225
+ presenceTransport[Symbol.dispose]()
226
+ history[Symbol.dispose]()
227
+ historyTransport[Symbol.dispose]()
228
+ void projection
229
+ .dispose()
230
+ .catch(() => undefined)
231
+ .then(() => residency.dispose())
232
+ .catch(() => undefined)
233
+ .finally(() => {
234
+ domain[Symbol.dispose]()
235
+ })
236
+ },
237
+ }
238
+ }
@@ -0,0 +1,169 @@
1
+ import type { Silo } from "atom.io"
2
+ import { atom, atomFamily, mutableAtom } from "atom.io"
3
+ import {
4
+ createMosaicTextDomainModels,
5
+ mosaicDomain,
6
+ mosaicText,
7
+ type MosaicTextIndexMember,
8
+ type MosaicTextIndexRoot,
9
+ type MosaicTextSelection,
10
+ } from "@atom.io/realtime"
11
+ import { z } from "zod"
12
+
13
+ export const MARKDOWN_INDEX_OPTIONS = {
14
+ maximumAliasGenerations: 4,
15
+ maximumAliasTargets: 8,
16
+ maximumChildrenPerNode: 32,
17
+ maximumFragmentsPerLeaf: 64,
18
+ maximumLeafGraphemes: 65_536,
19
+ maximumLeafUtf16Units: 65_536,
20
+ minimumChildrenPerNode: 8,
21
+ minimumLeafGraphemes: 16_384,
22
+ targetChildrenPerNode: 16,
23
+ targetLeafGraphemes: 32_768,
24
+ } as const
25
+
26
+ export const MarkdownText = mosaicText({
27
+ maximumDeletionIntervalsPerOperation: 16_384,
28
+ maximumHistoryTargets: 10_000,
29
+ maximumRunGraphemes: 65_536,
30
+ maximumRunUtf16Units: 1_000_000,
31
+ maximumRunsPerOperation: 65_536,
32
+ })
33
+
34
+ export const {
35
+ emptyIndexRoot: EMPTY_MARKDOWN_INDEX_ROOT,
36
+ indexMemberModel: markdownIndexMemberModel,
37
+ indexMemberSchema: markdownIndexMemberSchema,
38
+ indexRootModel: markdownIndexRootModel,
39
+ indexRootSchema: markdownIndexRootSchema,
40
+ sourceModel: markdownSourceModel,
41
+ sourceSnapshotSchema: markdownSourceSnapshotSchema,
42
+ } = createMosaicTextDomainModels({
43
+ index: MARKDOWN_INDEX_OPTIONS,
44
+ text: MarkdownText,
45
+ })
46
+
47
+ const id = z.string().min(1).max(512)
48
+ const emptyIndexMember = (memberId: string): MosaicTextIndexMember => ({
49
+ fragments: [],
50
+ id: memberId,
51
+ kind: `leaf`,
52
+ summary: { graphemes: 0, leafCount: 1, lineBreaks: 0, utf16Units: 0 },
53
+ version: 1,
54
+ })
55
+
56
+ export const markdownSourceAtom = mutableAtom<InstanceType<typeof MarkdownText>>(
57
+ { class: MarkdownText, key: `markdownSource` },
58
+ )
59
+ export const markdownIndexRootAtom = atom<MosaicTextIndexRoot>({
60
+ default: EMPTY_MARKDOWN_INDEX_ROOT,
61
+ key: `markdownIndexRoot`,
62
+ })
63
+ export const markdownIndexMemberAtoms = atomFamily<
64
+ MosaicTextIndexMember,
65
+ string
66
+ >({
67
+ default: emptyIndexMember,
68
+ key: `markdownIndexMember`,
69
+ })
70
+
71
+ export type MarkdownPresence = {
72
+ readonly actor: string
73
+ readonly color: string
74
+ readonly name: string
75
+ readonly selection: MosaicTextSelection | null
76
+ readonly session: string
77
+ readonly viewport: {
78
+ readonly anchor: MosaicTextSelection[`anchor`]
79
+ readonly head: MosaicTextSelection[`head`]
80
+ } | null
81
+ }
82
+
83
+ const relativePosition = z
84
+ .object({
85
+ affinity: z.enum([`left`, `right`]),
86
+ offset: z.number().int().nonnegative(),
87
+ runId: id.nullable(),
88
+ })
89
+ .strict()
90
+
91
+ export const markdownPresenceSchema: z.ZodType<MarkdownPresence> = z
92
+ .object({
93
+ actor: id,
94
+ color: z.string().min(1).max(64),
95
+ name: z.string().min(1).max(128),
96
+ selection: z
97
+ .object({ anchor: relativePosition, head: relativePosition })
98
+ .strict()
99
+ .nullable(),
100
+ session: id,
101
+ viewport: z
102
+ .object({ anchor: relativePosition, head: relativePosition })
103
+ .strict()
104
+ .nullable(),
105
+ })
106
+ .strict()
107
+
108
+ export const markdownPresenceAtoms = atomFamily<MarkdownPresence | null, string>(
109
+ { default: null, key: `markdownPresence` },
110
+ )
111
+ export const markdownPresenceKey = (
112
+ identity: Pick<MarkdownPresence, `actor` | `session`>,
113
+ ): string => `${identity.actor}\u0000${identity.session}`
114
+
115
+ export const markdownDocumentDomain = mosaicDomain({
116
+ configSchema: z.object({}).strict(),
117
+ key: `markdown-document`,
118
+ members: {
119
+ collaborator: {
120
+ keySchema: id,
121
+ role: `ephemeral`,
122
+ schema: markdownPresenceSchema,
123
+ token: markdownPresenceAtoms,
124
+ },
125
+ indexMembers: {
126
+ keySchema: id,
127
+ model: markdownIndexMemberModel,
128
+ role: `durable`,
129
+ schema: markdownIndexMemberSchema as never,
130
+ token: markdownIndexMemberAtoms,
131
+ },
132
+ indexRoot: {
133
+ model: markdownIndexRootModel,
134
+ role: `durable`,
135
+ schema: markdownIndexRootSchema,
136
+ token: markdownIndexRootAtom,
137
+ },
138
+ source: {
139
+ model: markdownSourceModel,
140
+ role: `durable`,
141
+ schema: markdownSourceSnapshotSchema,
142
+ token: markdownSourceAtom,
143
+ },
144
+ },
145
+ version: 1,
146
+ })
147
+
148
+ const MARKDOWN_DOMAIN_TOKENS = [
149
+ markdownSourceAtom,
150
+ markdownIndexRootAtom,
151
+ markdownIndexMemberAtoms,
152
+ markdownPresenceAtoms,
153
+ ] as const
154
+
155
+ export function activateMarkdownDocumentDomain(options: {
156
+ readonly instance?: string
157
+ readonly silo: Pick<Silo, `install` | `store`>
158
+ }): ReturnType<typeof markdownDocumentDomain.activate> {
159
+ options.silo.install([...MARKDOWN_DOMAIN_TOKENS])
160
+ return markdownDocumentDomain.activate({
161
+ config: {},
162
+ instance: options.instance ?? `shared-markdown`,
163
+ store: options.silo.store,
164
+ })
165
+ }
166
+
167
+ export type MarkdownDocumentDomain = Awaited<
168
+ ReturnType<typeof activateMarkdownDocumentDomain>
169
+ >
package/src/globals.css CHANGED
@@ -1,11 +1,18 @@
1
1
  @import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&family=Newsreader:opsz,wght@6..72,500;6..72,650&display=swap");
2
2
 
3
3
  :root {
4
+ color-scheme: dark light;
4
5
  font-family: "Manrope", system-ui, sans-serif;
5
- color: #292936;
6
- background: #f3f2f7;
6
+ color: #f1edf7;
7
+ background: #121016;
7
8
  font-synthesis: none;
8
9
  text-rendering: optimizeLegibility;
10
+
11
+ @media (prefers-color-scheme: light) {
12
+ color-scheme: light;
13
+ color: #292936;
14
+ background: #f3f2f7;
15
+ }
9
16
  }
10
17
 
11
18
  * {
@@ -0,0 +1,279 @@
1
+ import type { MosaicTextRelativePosition } from "@atom.io/realtime"
2
+
3
+ export type MarkdownSourceBlock = {
4
+ readonly anchor: MosaicTextRelativePosition
5
+ readonly end: number
6
+ readonly key: string
7
+ readonly start: number
8
+ readonly text: string
9
+ }
10
+
11
+ export type MarkdownSemanticBlock = MarkdownSourceBlock & {
12
+ readonly kind: `blockquote` | `code` | `heading` | `list-item` | `paragraph`
13
+ readonly level?: number
14
+ readonly ordered?: boolean
15
+ }
16
+
17
+ export type MarkdownParseInstrumentation = {
18
+ readonly canceled: boolean
19
+ readonly elapsedMs: number
20
+ readonly parsedBlocks: number
21
+ readonly scannedUtf16Units: number
22
+ readonly stableBoundaryIndex: number | null
23
+ readonly reusedBlocks: number
24
+ }
25
+
26
+ export type MarkdownParseResult = {
27
+ readonly blocks: readonly MarkdownSemanticBlock[]
28
+ readonly instrumentation: MarkdownParseInstrumentation
29
+ }
30
+
31
+ type FenceState = {
32
+ readonly anchor: MosaicTextRelativePosition
33
+ readonly end: number
34
+ readonly key: string
35
+ readonly language: string
36
+ readonly start: number
37
+ readonly text: string
38
+ }
39
+
40
+ type ParseState = { readonly fence: FenceState | null }
41
+
42
+ type CacheEntry = {
43
+ readonly input: ParseState
44
+ readonly output: ParseState
45
+ readonly semantic: readonly MarkdownSemanticBlock[]
46
+ readonly source: MarkdownSourceBlock
47
+ }
48
+
49
+ const samePosition = (
50
+ left: MosaicTextRelativePosition,
51
+ right: MosaicTextRelativePosition,
52
+ ): boolean =>
53
+ left.affinity === right.affinity &&
54
+ left.offset === right.offset &&
55
+ left.runId === right.runId
56
+
57
+ const sameFence = (left: FenceState | null, right: FenceState | null): boolean =>
58
+ left === right ||
59
+ (left !== null &&
60
+ right !== null &&
61
+ left.end === right.end &&
62
+ left.key === right.key &&
63
+ left.language === right.language &&
64
+ left.start === right.start &&
65
+ left.text === right.text &&
66
+ samePosition(left.anchor, right.anchor))
67
+
68
+ const sameState = (left: ParseState, right: ParseState): boolean =>
69
+ sameFence(left.fence, right.fence)
70
+
71
+ const sameSource = (
72
+ left: MarkdownSourceBlock,
73
+ right: MarkdownSourceBlock,
74
+ ): boolean =>
75
+ left.key === right.key &&
76
+ left.start === right.start &&
77
+ left.end === right.end &&
78
+ left.text === right.text &&
79
+ samePosition(left.anchor, right.anchor)
80
+
81
+ const classify = (source: MarkdownSourceBlock): MarkdownSemanticBlock => {
82
+ const heading = /^(#{1,6})\s+(.*)$/u.exec(source.text)
83
+ if (heading) {
84
+ return {
85
+ ...source,
86
+ kind: `heading`,
87
+ level: heading[1].length,
88
+ text: heading[2],
89
+ }
90
+ }
91
+ const ordered = /^\d+[.)]\s+(.*)$/u.exec(source.text)
92
+ if (ordered) {
93
+ return { ...source, kind: `list-item`, ordered: true, text: ordered[1] }
94
+ }
95
+ const unordered = /^[-*+]\s+(.*)$/u.exec(source.text)
96
+ if (unordered) {
97
+ return { ...source, kind: `list-item`, ordered: false, text: unordered[1] }
98
+ }
99
+ if (source.text.startsWith(`> `)) {
100
+ return { ...source, kind: `blockquote`, text: source.text.slice(2) }
101
+ }
102
+ return { ...source, kind: `paragraph` }
103
+ }
104
+
105
+ function parseOne(
106
+ source: MarkdownSourceBlock,
107
+ input: ParseState,
108
+ ): { readonly output: ParseState; readonly semantic: MarkdownSemanticBlock[] } {
109
+ const fenceMarker = /^```([^\n]*)$/u.exec(source.text.trimEnd())
110
+ if (input.fence === null) {
111
+ if (fenceMarker === null) {
112
+ return {
113
+ output: input,
114
+ semantic: source.text.trim() === `` ? [] : [classify(source)],
115
+ }
116
+ }
117
+ return {
118
+ output: {
119
+ fence: {
120
+ anchor: source.anchor,
121
+ end: source.end,
122
+ key: source.key,
123
+ language: fenceMarker[1].trim(),
124
+ start: source.start,
125
+ text: ``,
126
+ },
127
+ },
128
+ semantic: [],
129
+ }
130
+ }
131
+ if (fenceMarker !== null) {
132
+ return {
133
+ output: { fence: null },
134
+ semantic: [
135
+ {
136
+ anchor: input.fence.anchor,
137
+ end: source.end,
138
+ key: input.fence.key,
139
+ kind: `code`,
140
+ start: input.fence.start,
141
+ text: input.fence.text,
142
+ },
143
+ ],
144
+ }
145
+ }
146
+ return {
147
+ output: {
148
+ fence: {
149
+ ...input.fence,
150
+ end: source.end,
151
+ text:
152
+ input.fence.text === ``
153
+ ? source.text
154
+ : `${input.fence.text}\n${source.text}`,
155
+ },
156
+ },
157
+ semantic: [],
158
+ }
159
+ }
160
+
161
+ const turn = (): Promise<void> =>
162
+ new Promise((resolve) => setTimeout(resolve, 0))
163
+
164
+ /**
165
+ * Incremental block parser with explicit cancelation and stable-boundary reuse.
166
+ * It is renderer-neutral; the React viewport may be replaced without moving
167
+ * parser state into component instances.
168
+ */
169
+ export class IncrementalMarkdownParser {
170
+ #cache: CacheEntry[] = []
171
+ #generation: AbortController | null = null
172
+
173
+ public cancel(): void {
174
+ this.#generation?.abort()
175
+ }
176
+
177
+ public async parse(
178
+ source: readonly MarkdownSourceBlock[],
179
+ options: {
180
+ readonly signal?: AbortSignal
181
+ readonly yieldAfterUtf16Units?: number
182
+ } = {},
183
+ ): Promise<MarkdownParseResult> {
184
+ this.cancel()
185
+ const generation = new AbortController()
186
+ this.#generation = generation
187
+ const abort = (): void => {
188
+ generation.abort()
189
+ }
190
+ if (options.signal?.aborted) abort()
191
+ options.signal?.addEventListener(`abort`, abort, { once: true })
192
+ const started = performance.now()
193
+ const yieldAfter = options.yieldAfterUtf16Units ?? 16_384
194
+ let sinceYield = 0
195
+ let scannedUtf16Units = 0
196
+ let parsedBlocks = 0
197
+ let reusedBlocks = 0
198
+ let stableBoundaryIndex: number | null = null
199
+ let propagating = false
200
+ const next: CacheEntry[] = []
201
+ const semantic: MarkdownSemanticBlock[] = []
202
+ let state: ParseState = { fence: null }
203
+
204
+ try {
205
+ // Even the first bounded slice begins outside the browser input turn.
206
+ await turn()
207
+ for (let index = 0; index < source.length; index++) {
208
+ if (generation.signal.aborted) throw generation.signal.reason
209
+ const item = source[index]
210
+ const cached = this.#cache[index]
211
+ if (
212
+ cached !== undefined &&
213
+ sameSource(cached.source, item) &&
214
+ sameState(cached.input, state)
215
+ ) {
216
+ next.push(cached)
217
+ semantic.push(...cached.semantic)
218
+ state = cached.output
219
+ reusedBlocks++
220
+ if (propagating) {
221
+ stableBoundaryIndex = index
222
+ propagating = false
223
+ }
224
+ continue
225
+ }
226
+ propagating = true
227
+ stableBoundaryIndex = null
228
+ const parsed = parseOne(item, state)
229
+ const entry = {
230
+ input: state,
231
+ output: parsed.output,
232
+ semantic: parsed.semantic,
233
+ source: item,
234
+ }
235
+ next.push(entry)
236
+ semantic.push(...parsed.semantic)
237
+ state = parsed.output
238
+ parsedBlocks++
239
+ scannedUtf16Units += item.text.length
240
+ sinceYield += item.text.length
241
+ if (sinceYield >= yieldAfter) {
242
+ sinceYield = 0
243
+ await turn()
244
+ }
245
+ }
246
+ if (state.fence !== null) {
247
+ semantic.push({ ...state.fence, kind: `code` })
248
+ }
249
+ if (!generation.signal.aborted) this.#cache = next
250
+ return {
251
+ blocks: semantic,
252
+ instrumentation: {
253
+ canceled: false,
254
+ elapsedMs: performance.now() - started,
255
+ parsedBlocks,
256
+ reusedBlocks,
257
+ scannedUtf16Units,
258
+ stableBoundaryIndex,
259
+ },
260
+ }
261
+ } catch (error) {
262
+ if (!generation.signal.aborted) throw error
263
+ return {
264
+ blocks: [],
265
+ instrumentation: {
266
+ canceled: true,
267
+ elapsedMs: performance.now() - started,
268
+ parsedBlocks,
269
+ reusedBlocks,
270
+ scannedUtf16Units,
271
+ stableBoundaryIndex,
272
+ },
273
+ }
274
+ } finally {
275
+ options.signal?.removeEventListener(`abort`, abort)
276
+ if (this.#generation === generation) this.#generation = null
277
+ }
278
+ }
279
+ }