@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.
- package/CHANGELOG.md +16 -0
- package/INNOVATIONS.md +242 -113
- package/README.md +75 -39
- package/eslint.config.ts +1 -1
- package/index.html +1 -0
- package/node/http-server.ts +81 -0
- package/node/server.ts +12 -60
- package/node/service.ts +310 -0
- package/package.json +30 -21
- package/playwright.config.ts +34 -0
- package/src/LexicalMarkdownEditor.css +88 -0
- package/src/LexicalMarkdownEditor.module.css +8 -0
- package/src/LexicalMarkdownEditor.tsx +43 -0
- package/src/MarkdownWorkspace.module.css +154 -98
- package/src/MarkdownWorkspace.tsx +163 -296
- package/src/collaboration-client.ts +238 -0
- package/src/document-domain.ts +169 -0
- package/src/globals.css +9 -2
- package/src/incremental-markdown.ts +279 -0
- package/src/main.tsx +30 -9
- package/src/protocol.ts +35 -0
- package/src/virtualization.ts +53 -0
- package/src/workspace-state.ts +275 -0
- package/tests/browser/editor-conformance.spec.ts +806 -0
- package/tests/collaboration.test.tsx +465 -213
- package/tests/incremental-markdown.test.tsx +101 -0
- package/tsconfig.app.json +1 -1
- package/tsconfig.node.json +8 -2
- package/tsconfig.test.json +1 -1
- package/vite.config.ts +13 -9
- package/node/mosaic-atom.ts +0 -88
- package/src/collaboration/mosaic.ts +0 -46
package/src/main.tsx
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
1
1
|
import "./globals.css"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { Silo } from "atom.io"
|
|
4
|
+
import { StoreProvider } from "@atom.io/react"
|
|
5
5
|
import { createRoot } from "react-dom/client"
|
|
6
6
|
import { io } from "socket.io-client"
|
|
7
7
|
|
|
8
|
+
import { createMarkdownCollaborationClient } from "./collaboration-client.ts"
|
|
8
9
|
import { MarkdownWorkspace } from "./MarkdownWorkspace.tsx"
|
|
9
|
-
import { getBrowserSession
|
|
10
|
+
import { getBrowserSession } from "./session.ts"
|
|
11
|
+
import { createMarkdownWorkspaceState } from "./workspace-state.ts"
|
|
10
12
|
|
|
11
13
|
const session = getBrowserSession()
|
|
14
|
+
const silo = new Silo({
|
|
15
|
+
isProduction: import.meta.env.PROD,
|
|
16
|
+
lifespan: `ephemeral`,
|
|
17
|
+
name: `markdown-client:${session.clientId}`,
|
|
18
|
+
})
|
|
12
19
|
const socket = io(window.location.origin, {
|
|
13
|
-
auth:
|
|
20
|
+
auth: { actor: session.identity.id, session: session.clientId },
|
|
14
21
|
reconnection: true,
|
|
15
22
|
reconnectionDelayMax: 4_000,
|
|
16
23
|
})
|
|
24
|
+
const client = await createMarkdownCollaborationClient({
|
|
25
|
+
identity: session.identity,
|
|
26
|
+
sessionId: session.clientId,
|
|
27
|
+
silo,
|
|
28
|
+
socket,
|
|
29
|
+
})
|
|
30
|
+
const workspace = createMarkdownWorkspaceState({ client, silo })
|
|
31
|
+
window.addEventListener(
|
|
32
|
+
`pagehide`,
|
|
33
|
+
() => {
|
|
34
|
+
workspace[Symbol.dispose]()
|
|
35
|
+
client[Symbol.dispose]()
|
|
36
|
+
socket.close()
|
|
37
|
+
},
|
|
38
|
+
{ once: true },
|
|
39
|
+
)
|
|
17
40
|
|
|
18
41
|
createRoot(document.getElementById(`root`)!).render(
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
</RealtimeProvider>
|
|
23
|
-
</StrictMode>,
|
|
42
|
+
<StoreProvider store={silo.store}>
|
|
43
|
+
<MarkdownWorkspace client={client} state={workspace} />
|
|
44
|
+
</StoreProvider>,
|
|
24
45
|
)
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MosaicTextIndexRange,
|
|
3
|
+
MosaicTextRelativePosition,
|
|
4
|
+
} from "@atom.io/realtime"
|
|
5
|
+
|
|
6
|
+
export const MARKDOWN_EVENTS = {
|
|
7
|
+
command: `markdown-domain:command`,
|
|
8
|
+
materialize: `markdown-domain:materialize`,
|
|
9
|
+
positionAtOffset: `markdown-domain:position-at-offset`,
|
|
10
|
+
resolvePosition: `markdown-domain:resolve-position`,
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
export type MarkdownAcknowledgement<Value> =
|
|
14
|
+
| { readonly ok: true; readonly value: Value }
|
|
15
|
+
| { readonly ok: false; readonly reason: string }
|
|
16
|
+
|
|
17
|
+
export type MarkdownEditCommand = {
|
|
18
|
+
readonly anchor: MosaicTextRelativePosition
|
|
19
|
+
readonly gestureId: string
|
|
20
|
+
readonly head: MosaicTextRelativePosition
|
|
21
|
+
readonly sequence: number
|
|
22
|
+
readonly text: string
|
|
23
|
+
readonly type: `replace`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type MarkdownImportCommand = {
|
|
27
|
+
readonly gestureId: string
|
|
28
|
+
readonly sequence: number
|
|
29
|
+
readonly text: string
|
|
30
|
+
readonly type: `import`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type MarkdownCommand = MarkdownEditCommand | MarkdownImportCommand
|
|
34
|
+
|
|
35
|
+
export type MarkdownViewport = MosaicTextIndexRange
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MosaicTextIndexRange } from "@atom.io/realtime"
|
|
2
|
+
|
|
3
|
+
export type MarkdownVirtualMetrics = {
|
|
4
|
+
readonly averageUtf16UnitsPerRow: number
|
|
5
|
+
readonly overscanRows: number
|
|
6
|
+
readonly rowHeight: number
|
|
7
|
+
readonly totalUtf16Units: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MarkdownVirtualWindow = {
|
|
11
|
+
readonly bottomSpacer: number
|
|
12
|
+
readonly range: MosaicTextIndexRange
|
|
13
|
+
readonly topSpacer: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Map a scroll viewport to a bounded logical range without a segment manifest. */
|
|
17
|
+
export function markdownVirtualWindow(
|
|
18
|
+
viewport: { readonly height: number; readonly scrollTop: number },
|
|
19
|
+
metrics: MarkdownVirtualMetrics,
|
|
20
|
+
): MarkdownVirtualWindow {
|
|
21
|
+
for (const [name, value] of Object.entries({ ...viewport, ...metrics })) {
|
|
22
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
23
|
+
throw new RangeError(`${name} must be a finite non-negative number.`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (metrics.averageUtf16UnitsPerRow < 1 || metrics.rowHeight < 1) {
|
|
27
|
+
throw new RangeError(`Virtual row estimates must be positive.`)
|
|
28
|
+
}
|
|
29
|
+
const totalRows = Math.max(
|
|
30
|
+
1,
|
|
31
|
+
Math.ceil(metrics.totalUtf16Units / metrics.averageUtf16UnitsPerRow),
|
|
32
|
+
)
|
|
33
|
+
const firstVisibleRow = Math.floor(viewport.scrollTop / metrics.rowHeight)
|
|
34
|
+
const visibleRows = Math.max(1, Math.ceil(viewport.height / metrics.rowHeight))
|
|
35
|
+
const firstRow = Math.max(0, firstVisibleRow - metrics.overscanRows)
|
|
36
|
+
const lastRow = Math.min(
|
|
37
|
+
totalRows,
|
|
38
|
+
firstVisibleRow + visibleRows + metrics.overscanRows,
|
|
39
|
+
)
|
|
40
|
+
const start = Math.min(
|
|
41
|
+
metrics.totalUtf16Units,
|
|
42
|
+
Math.floor(firstRow * metrics.averageUtf16UnitsPerRow),
|
|
43
|
+
)
|
|
44
|
+
const end = Math.min(
|
|
45
|
+
metrics.totalUtf16Units,
|
|
46
|
+
Math.max(start, Math.ceil(lastRow * metrics.averageUtf16UnitsPerRow)),
|
|
47
|
+
)
|
|
48
|
+
return {
|
|
49
|
+
bottomSpacer: Math.max(0, (totalRows - lastRow) * metrics.rowHeight),
|
|
50
|
+
range: { end, kind: `utf16-range`, start },
|
|
51
|
+
topSpacer: firstRow * metrics.rowHeight,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { ReadonlyPureSelectorToken, RegularAtomToken, Silo } from "atom.io"
|
|
2
|
+
import type { MosaicTextSelection } from "@atom.io/realtime"
|
|
3
|
+
import {
|
|
4
|
+
createMosaicTextRangeController,
|
|
5
|
+
type MosaicTextRangeController,
|
|
6
|
+
type MosaicTextRangeView,
|
|
7
|
+
} from "@atom.io/realtime/client"
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
MarkdownClientStatus,
|
|
11
|
+
MarkdownCollaborationClient,
|
|
12
|
+
} from "./collaboration-client.ts"
|
|
13
|
+
import type { MarkdownPresence } from "./document-domain.ts"
|
|
14
|
+
import {
|
|
15
|
+
IncrementalMarkdownParser,
|
|
16
|
+
type MarkdownParseInstrumentation,
|
|
17
|
+
type MarkdownSemanticBlock,
|
|
18
|
+
} from "./incremental-markdown.ts"
|
|
19
|
+
import {
|
|
20
|
+
markdownVirtualWindow,
|
|
21
|
+
type MarkdownVirtualWindow,
|
|
22
|
+
} from "./virtualization.ts"
|
|
23
|
+
|
|
24
|
+
export type MarkdownScrollViewport = {
|
|
25
|
+
readonly height: number
|
|
26
|
+
readonly scrollTop: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type MarkdownWorkspacePeer = {
|
|
30
|
+
readonly id: string
|
|
31
|
+
readonly selection: MosaicTextSelection | null
|
|
32
|
+
readonly value: MarkdownPresence
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type MarkdownWorkspaceState = Disposable & {
|
|
36
|
+
readonly parse: RegularAtomToken<readonly MarkdownSemanticBlock[]>
|
|
37
|
+
readonly parseMetrics: RegularAtomToken<MarkdownParseInstrumentation>
|
|
38
|
+
readonly peers: ReadonlyPureSelectorToken<readonly MarkdownWorkspacePeer[]>
|
|
39
|
+
readonly presence: RegularAtomToken<readonly MarkdownPresence[]>
|
|
40
|
+
readonly problem: RegularAtomToken<string | null>
|
|
41
|
+
readonly readProblem: RegularAtomToken<string | null>
|
|
42
|
+
readonly scroll: RegularAtomToken<MarkdownScrollViewport>
|
|
43
|
+
readonly status: RegularAtomToken<MarkdownClientStatus>
|
|
44
|
+
readonly totalLength: RegularAtomToken<number>
|
|
45
|
+
readonly view: RegularAtomToken<MosaicTextRangeView>
|
|
46
|
+
readonly window: ReadonlyPureSelectorToken<MarkdownVirtualWindow>
|
|
47
|
+
flushDeferredScroll(hasLocalDraft: boolean): void
|
|
48
|
+
publishSelection(selection: MosaicTextSelection): Promise<void>
|
|
49
|
+
publishViewport(viewport: MosaicTextSelection): Promise<void>
|
|
50
|
+
refreshLength(): Promise<number | null>
|
|
51
|
+
replace(
|
|
52
|
+
input: Parameters<MarkdownCollaborationClient[`replace`]>[0],
|
|
53
|
+
): Promise<void>
|
|
54
|
+
reportEditorError(error: unknown): void
|
|
55
|
+
setDocumentLength(length: number): void
|
|
56
|
+
setScroll(viewport: MarkdownScrollViewport, defer: boolean): void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const EMPTY_PARSE_METRICS: MarkdownParseInstrumentation = {
|
|
60
|
+
canceled: false,
|
|
61
|
+
elapsedMs: 0,
|
|
62
|
+
parsedBlocks: 0,
|
|
63
|
+
reusedBlocks: 0,
|
|
64
|
+
scannedUtf16Units: 0,
|
|
65
|
+
stableBoundaryIndex: null,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const activePresence = (
|
|
69
|
+
client: MarkdownCollaborationClient,
|
|
70
|
+
): MarkdownPresence[] =>
|
|
71
|
+
client.presence.state.presence.flatMap((envelope) =>
|
|
72
|
+
envelope.kind === `update` && envelope.address.member === `collaborator`
|
|
73
|
+
? [envelope.value as MarkdownPresence]
|
|
74
|
+
: [],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
/** Store-owned application state around the renderer-neutral text controllers. */
|
|
78
|
+
export function createMarkdownWorkspaceState(options: {
|
|
79
|
+
readonly client: MarkdownCollaborationClient
|
|
80
|
+
readonly silo: Silo
|
|
81
|
+
}): MarkdownWorkspaceState {
|
|
82
|
+
const { client, silo } = options
|
|
83
|
+
const key = `markdown-workspace:${client.sessionId}`
|
|
84
|
+
const statusAtom = silo.atom<MarkdownClientStatus>({
|
|
85
|
+
default: client.status(),
|
|
86
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
87
|
+
key: `${key}:status`,
|
|
88
|
+
})
|
|
89
|
+
const presenceAtom = silo.atom<readonly MarkdownPresence[]>({
|
|
90
|
+
default: activePresence(client),
|
|
91
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
92
|
+
key: `${key}:presence`,
|
|
93
|
+
})
|
|
94
|
+
const totalLengthAtom = silo.atom<number>({
|
|
95
|
+
default: 0,
|
|
96
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
97
|
+
key: `${key}:totalLength`,
|
|
98
|
+
})
|
|
99
|
+
const scrollAtom = silo.atom<MarkdownScrollViewport>({
|
|
100
|
+
default: { height: 560, scrollTop: 0 },
|
|
101
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
102
|
+
key: `${key}:scroll`,
|
|
103
|
+
})
|
|
104
|
+
const parseAtom = silo.atom<readonly MarkdownSemanticBlock[]>({
|
|
105
|
+
default: [],
|
|
106
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
107
|
+
key: `${key}:parse`,
|
|
108
|
+
})
|
|
109
|
+
const parseMetricsAtom = silo.atom<MarkdownParseInstrumentation>({
|
|
110
|
+
default: EMPTY_PARSE_METRICS,
|
|
111
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
112
|
+
key: `${key}:parseMetrics`,
|
|
113
|
+
})
|
|
114
|
+
const readProblemAtom = silo.atom<string | null>({
|
|
115
|
+
default: null,
|
|
116
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
117
|
+
key: `${key}:readProblem`,
|
|
118
|
+
})
|
|
119
|
+
const problemAtom = silo.atom<string | null>({
|
|
120
|
+
default: null,
|
|
121
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
122
|
+
key: `${key}:problem`,
|
|
123
|
+
})
|
|
124
|
+
const windowSelector = silo.selector<MarkdownVirtualWindow>({
|
|
125
|
+
get: ({ get }) =>
|
|
126
|
+
markdownVirtualWindow(get(scrollAtom), {
|
|
127
|
+
averageUtf16UnitsPerRow: 88,
|
|
128
|
+
overscanRows: 24,
|
|
129
|
+
rowHeight: 24,
|
|
130
|
+
totalUtf16Units: get(totalLengthAtom),
|
|
131
|
+
}),
|
|
132
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
133
|
+
key: `${key}:window`,
|
|
134
|
+
})
|
|
135
|
+
const peersSelector = silo.selector<readonly MarkdownWorkspacePeer[]>({
|
|
136
|
+
get: ({ get }) =>
|
|
137
|
+
get(presenceAtom)
|
|
138
|
+
.filter((person) => person.session !== client.sessionId)
|
|
139
|
+
.map((person) => ({
|
|
140
|
+
id: person.session,
|
|
141
|
+
selection: person.selection,
|
|
142
|
+
value: person,
|
|
143
|
+
})),
|
|
144
|
+
// eslint-disable-next-line atom.io/naming-convention -- A shared Store needs one workspace namespace per realtime session.
|
|
145
|
+
key: `${key}:peers`,
|
|
146
|
+
})
|
|
147
|
+
const initialWindow = silo.getState(windowSelector)
|
|
148
|
+
const range: MosaicTextRangeController = createMosaicTextRangeController({
|
|
149
|
+
client: client.projection,
|
|
150
|
+
deferStart: true,
|
|
151
|
+
initialRange: initialWindow.range,
|
|
152
|
+
key: `${key}:text-range`,
|
|
153
|
+
overscan: 2_048,
|
|
154
|
+
silo,
|
|
155
|
+
})
|
|
156
|
+
const parser = new IncrementalMarkdownParser()
|
|
157
|
+
let deferredScroll: MarkdownScrollViewport | null = null
|
|
158
|
+
let disposed = false
|
|
159
|
+
|
|
160
|
+
const refreshLength = async (): Promise<number | null> => {
|
|
161
|
+
try {
|
|
162
|
+
const length = await client.projection.readLength()
|
|
163
|
+
if (disposed) return null
|
|
164
|
+
silo.setState(totalLengthAtom, length)
|
|
165
|
+
silo.setState(readProblemAtom, null)
|
|
166
|
+
if (length === 0) {
|
|
167
|
+
silo.setState(range.view, {
|
|
168
|
+
error: null,
|
|
169
|
+
projection: {
|
|
170
|
+
blocks: [],
|
|
171
|
+
complete: true,
|
|
172
|
+
range: { end: 0, kind: `utf16-range`, start: 0 },
|
|
173
|
+
segments: [],
|
|
174
|
+
text: ``,
|
|
175
|
+
},
|
|
176
|
+
status: `ready`,
|
|
177
|
+
})
|
|
178
|
+
} else {
|
|
179
|
+
range.start()
|
|
180
|
+
}
|
|
181
|
+
return length
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (!disposed) {
|
|
184
|
+
silo.setState(
|
|
185
|
+
readProblemAtom,
|
|
186
|
+
error instanceof Error ? error.message : String(error),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const parseProjection = (view: MosaicTextRangeView): void => {
|
|
193
|
+
if (view.status !== `ready`) return
|
|
194
|
+
void parser.parse(view.projection.blocks).then((result) => {
|
|
195
|
+
if (disposed || result.instrumentation.canceled) return
|
|
196
|
+
silo.setState(parseAtom, result.blocks)
|
|
197
|
+
silo.setState(parseMetricsAtom, result.instrumentation)
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
const stopStatus = client.subscribe((next) => {
|
|
201
|
+
silo.setState(statusAtom, next)
|
|
202
|
+
if (next.connection === `live`) void refreshLength()
|
|
203
|
+
})
|
|
204
|
+
const stopPresence = client.presence.subscribe(() => {
|
|
205
|
+
silo.setState(presenceAtom, activePresence(client))
|
|
206
|
+
})
|
|
207
|
+
const stopWindow = silo.subscribe(windowSelector, ({ newValue }) => {
|
|
208
|
+
silo.setState(range.range, newValue.range)
|
|
209
|
+
})
|
|
210
|
+
const stopView = silo.subscribe(range.view, ({ newValue }) => {
|
|
211
|
+
parseProjection(newValue)
|
|
212
|
+
})
|
|
213
|
+
parseProjection(silo.getState(range.view))
|
|
214
|
+
void refreshLength()
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
parse: parseAtom,
|
|
218
|
+
parseMetrics: parseMetricsAtom,
|
|
219
|
+
peers: peersSelector,
|
|
220
|
+
presence: presenceAtom,
|
|
221
|
+
problem: problemAtom,
|
|
222
|
+
readProblem: readProblemAtom,
|
|
223
|
+
scroll: scrollAtom,
|
|
224
|
+
status: statusAtom,
|
|
225
|
+
totalLength: totalLengthAtom,
|
|
226
|
+
view: range.view,
|
|
227
|
+
window: windowSelector,
|
|
228
|
+
flushDeferredScroll(hasLocalDraft) {
|
|
229
|
+
if (hasLocalDraft || deferredScroll === null) return
|
|
230
|
+
silo.setState(scrollAtom, deferredScroll)
|
|
231
|
+
deferredScroll = null
|
|
232
|
+
},
|
|
233
|
+
publishSelection(selection) {
|
|
234
|
+
return client.publishPresence({
|
|
235
|
+
color: client.identity.color,
|
|
236
|
+
name: client.identity.name,
|
|
237
|
+
selection,
|
|
238
|
+
viewport: null,
|
|
239
|
+
})
|
|
240
|
+
},
|
|
241
|
+
publishViewport(viewport) {
|
|
242
|
+
return client.publishPresence({
|
|
243
|
+
color: client.identity.color,
|
|
244
|
+
name: client.identity.name,
|
|
245
|
+
selection: null,
|
|
246
|
+
viewport,
|
|
247
|
+
})
|
|
248
|
+
},
|
|
249
|
+
refreshLength,
|
|
250
|
+
replace: (input) => client.replace(input),
|
|
251
|
+
reportEditorError(error) {
|
|
252
|
+
silo.setState(
|
|
253
|
+
problemAtom,
|
|
254
|
+
error instanceof Error ? error.message : String(error),
|
|
255
|
+
)
|
|
256
|
+
},
|
|
257
|
+
setDocumentLength(length) {
|
|
258
|
+
silo.setState(totalLengthAtom, length)
|
|
259
|
+
},
|
|
260
|
+
setScroll(viewport, defer) {
|
|
261
|
+
if (defer) deferredScroll = viewport
|
|
262
|
+
else silo.setState(scrollAtom, viewport)
|
|
263
|
+
},
|
|
264
|
+
[Symbol.dispose]() {
|
|
265
|
+
if (disposed) return
|
|
266
|
+
disposed = true
|
|
267
|
+
stopStatus()
|
|
268
|
+
stopPresence()
|
|
269
|
+
stopWindow()
|
|
270
|
+
stopView()
|
|
271
|
+
parser.cancel()
|
|
272
|
+
range[Symbol.dispose]()
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
}
|