querysub 0.2.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.
Files changed (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. package/yarnSpec.txt +56 -0
@@ -0,0 +1,2322 @@
1
+ import type preact from "preact";
2
+ import { isNode, sort } from "socket-function/src/misc";
3
+ import { isDeploy } from "../3-path-functions/deployCheck";
4
+ import { Querysub } from "../4-querysub/QuerysubController";
5
+ import { PermissionsCheck } from "../4-querysub/permissions";
6
+ import { ClientWatcher, clientWatcher } from "../1-path-client/pathValueClientWatcher";
7
+ import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
8
+ import { rawSchema } from "../2-proxy/pathDatabaseProxyBase";
9
+ import { SyncWatcher, atomic, atomicObjectWrite, atomicObjectWriteNoFreeze, doProxyOptions, doUnatomicWrites, proxyWatcher, registerSchemaPrefix, unregisterSchemaPrefix } from "../2-proxy/PathValueProxyWatcher";
10
+ import { createPathValueProxy, getProxyPath, isValueProxy, isValueProxy2 } from "../2-proxy/pathValueProxy";
11
+ import { measureBlock, measureCode, measureFnc } from "socket-function/src/profiling/measure";
12
+ import { canHaveChildren } from "socket-function/src/types";
13
+ import { errorify, logErrors } from "../errors";
14
+ import { cache, lazy } from "socket-function/src/caching";
15
+ import { getPathStr1, getPathIndexAssert } from "../path";
16
+ import { blue, green, red, yellow } from "socket-function/src/formatting/logColors";
17
+ import { heapTagObj } from "../diagnostics/heapTag";
18
+ import { onHotReload } from "socket-function/hot/HotReloadController";
19
+ import { isClient } from "../config2";
20
+ import { formatPercent, formatTime } from "socket-function/src/formatting/format";
21
+ import { lazyImport } from "socket-function/require/RequireController";
22
+ import { Schema2Fncs } from "../2-proxy/schema2";
23
+
24
+ lazyImport(() => import("../5-diagnostics/qreactDebug"));
25
+
26
+ // TIMING:
27
+ // Worst case (trivial child Components), about 20X slower than preact (12.5X with disableMeasurements)
28
+ // Realistically (div, prop, image child Components), about 8X slower than preact (4.3X with disableMeasurements)
29
+ // In practice the page will probably start to lag from the large DOM/layout state
30
+ // before it lags due to the rendering speed.
31
+
32
+ const idSymbol = Symbol("id");
33
+
34
+ interface QComponent {
35
+ id: number;
36
+ parentComponentId?: number;
37
+
38
+ props: { [key: string]: unknown; };
39
+ state: { [key: string]: unknown; };
40
+ vNodeForRender: { value: VirtualDOM } | undefined;
41
+ vNodeForMount: VirtualDOM | undefined;
42
+ newPropSeqNum: number;
43
+
44
+ rootDOMNodes: DOMNode[] | undefined;
45
+
46
+ isParentSVG?: boolean;
47
+
48
+ inlineState: {
49
+ [id: number]: {
50
+ // NOTE: No props, as the child can't re-render independently,
51
+ // so even if the props don't change / aren't accessed,
52
+ // the child will still have to re-render.
53
+ state: { [key: string]: unknown; };
54
+ };
55
+ };
56
+ };
57
+ interface ErrorHandler {
58
+ (config: { error: any; debugName: string }): VirtualDOM;
59
+ }
60
+ type DOMNode = HTMLElement | SVGElement;
61
+
62
+ const ALLOWED_TO_RENDER = () => { };
63
+ type VirtualDOMElement = Omit<preact.VNode<{ [key: string]: unknown }>, "type"> & {
64
+ type: any;
65
+ sourceModuleId?: string;
66
+ callStack?: string;
67
+ /** XSS protector, which allows us to detect virtual dom which has been accidentally serialized */
68
+ allowedToRender?: typeof ALLOWED_TO_RENDER;
69
+ };
70
+ type VirtualDOMBase = (
71
+ VirtualDOMElement
72
+ | string | number | bigint | boolean | null | undefined
73
+ );
74
+ export type VirtualDOM = VirtualDOMBase | VirtualDOMBase[];
75
+
76
+ type GetTFromAttributes<T> = T extends preact.JSX.HTMLAttributes<infer U> ? U : never;
77
+
78
+ export namespace qreact {
79
+ export namespace JSX {
80
+ export type IntrinsicElements = {
81
+ [key in keyof preact.JSX.IntrinsicElements]: (
82
+ // NOTE: IntrinsicAttributes is not working, but... this works just as well.
83
+ preact.JSX.IntrinsicElements[key] & {
84
+ /** Only called on mount of the element, and not on re-render, or update.
85
+ * - Should always be used in conjunction with a key, otherwise it might
86
+ * be called for an unrelated component, and then skipped on your component.
87
+ */
88
+ ref2?: (element: GetTFromAttributes<preact.JSX.IntrinsicElements[key]>) => void;
89
+ }
90
+ );
91
+ };
92
+ export type Element = preact.JSX.Element;
93
+ }
94
+ }
95
+
96
+
97
+ let schema = rawSchema<{
98
+ [LOCAL_DOMAIN]: {
99
+ qreact: {
100
+ components: {
101
+ [id: number]: QComponent;
102
+ };
103
+ };
104
+ hotreloadSeqNum: number;
105
+ unsyncedSeqNum: number;
106
+ };
107
+ }>();
108
+
109
+ export class Component<T = {}, S = {}> {
110
+ constructor(public props: T & {
111
+ children?: VirtualDOM | object;
112
+ key?: string | number;
113
+ }) { }
114
+ state = {} as S;
115
+ context = {};
116
+ setState(state: S) {
117
+ for (let key in state) {
118
+ (this as any).state[key] = state[key];
119
+ }
120
+ }
121
+ forceUpdate() { }
122
+ render(): preact.ComponentChild { return undefined; }
123
+
124
+ componentWillMount?(): void;
125
+ componentDidMount?(): void;
126
+ componentWillUnmount?(): void;
127
+ }
128
+ export interface QComponentStatic {
129
+ /** Compares props via JSON.stringify when updating them, skipping identical values.
130
+ * - Results in fewer re-renders, but can be slower if the props are large.
131
+ */
132
+ jsonComparePropUpdates?: boolean;
133
+ /** Decomposes props into individual primitive values when writing (the default state of
134
+ * most synced writes).
135
+ * - Results in minimal re-renders, even skipping intermediate component re-renders
136
+ * where possible, but is MUCH slower if the props are large.
137
+ * - Makes passing exotics and functions more difficult, requiring pre-wrapping them an
138
+ * object with atomicObjectWrite.
139
+ * NOTE: Usually props can be shallow, because the props only change when the parent re-renders.
140
+ * ALSO, it is vastly preferrably to simply pass synced values as props, which will then ===,
141
+ * BUT, which will be able to trigger changed IF nested values are accessed.
142
+ */
143
+ deepProps?: boolean;
144
+ }
145
+
146
+
147
+ class ComponentJSONCompare<T = {}> extends Component<T> {
148
+ public static jsonComparePropUpdates = true;
149
+ public render(): preact.ComponentChild { return undefined; }
150
+ }
151
+ class ComponentDeepProps<T = {}> extends Component<T> {
152
+ public static deepProps = true;
153
+ public render(): preact.ComponentChild { return undefined; }
154
+ }
155
+
156
+ export class qreact {
157
+ public static Component = Component;
158
+ public static createElement = createElement;
159
+ public static Fragment = Fragment;
160
+
161
+ /** See QComponentStatic.jsonComparePropUpdates */
162
+ public static ComponentJSONCompare = ComponentJSONCompare;
163
+ /** See QComponentStatic.deepProps */
164
+ public static ComponentDeepProps = ComponentDeepProps;
165
+
166
+ public static context = getDebugContext;
167
+
168
+ public static mountToDOM = mountToDOM;
169
+ public static renderToDOM = mountToDOM;
170
+ public static render = mountToDOM;
171
+
172
+ // IMPORTANT! The currentTarget is wrapped so we can copy certain properties, as event callbacks
173
+ // might be executed after the initial execution (in order to sync data from the server),
174
+ // and the original event will have it's value changed by then. IF you absolutely need
175
+ // the original target, call getCurrentTarget. OR, more likely, if you need to check
176
+ // if .target === .currentTarget, call isTarget.
177
+ public static getCurrentTarget = getCurrentTarget;
178
+ public static isTarget = isTarget;
179
+
180
+ public static errorHandler: ErrorHandler;
181
+
182
+ public static debug = async () => {
183
+ const { enableDebugComponents } = await import("../5-diagnostics/qreactDebug");
184
+ enableDebugComponents();
185
+ };
186
+
187
+ public static getRenderingComponent = getRenderingComponent;
188
+ public static getAncestor = getAncestor;
189
+ public static getAncestorProps = getAncestorProps;
190
+ public static watchDispose = watchDispose;
191
+ public static watchUnmount = watchDispose;
192
+ public static onDispose = onDispose;
193
+ public static onUnmount = onDispose;
194
+ public static watchUnsyncedComponents = watchUnsyncedComponents;
195
+ public static rehydrateVirtualDOM = rehydrateVirtualDOM;
196
+ public static hasEventHandlers = hasEventHandlers;
197
+ public static getCurrentComponentId = getCurrentComponentId;
198
+
199
+ public static allComponents = new Set<ExternalRenderClass>();
200
+
201
+ public static DEBUG_TIME = 0;
202
+ public static INJECT_FILENAMES = false;
203
+ public static INJECT_LINE_NUMBERS = false;
204
+
205
+ public static JSX = null as any as {
206
+ IntrinsicElements: { [key: string]: { [key: string]: unknown } };
207
+ };
208
+ }
209
+ (globalThis as any).qreact = qreact;
210
+
211
+ module.remapExports = (exports, callingModule) => {
212
+ let newExports = { ...exports } as any;
213
+
214
+ function createElement(...args: any[]) {
215
+ return (createElementBase as any)(callingModule, ...args);
216
+ }
217
+ newExports.qreact = new Proxy(newExports.qreact, {
218
+ get(target, prop) {
219
+ if (prop === "createElement") {
220
+ return createElement;
221
+ }
222
+ return target[prop];
223
+ },
224
+ set(target, prop, value) {
225
+ target[prop] = value;
226
+ return true;
227
+ }
228
+ });
229
+ return newExports;
230
+ };
231
+
232
+
233
+ const elementCtor = heapTagObj("JSXElement");
234
+ function createElement(
235
+ type: preact.ComponentClass | string,
236
+ props?: { [key: string]: unknown },
237
+ ...children: preact.ComponentChild[]
238
+ ): preact.VNode<{ [key: string]: unknown }> {
239
+ return createElementBase(undefined, type, props, ...children);
240
+ }
241
+
242
+ function createElementBase(
243
+ callingModule: NodeJS.Module | undefined,
244
+ type: preact.ComponentClass | string,
245
+ props?: { [key: string]: unknown },
246
+ ...children: preact.ComponentChild[]
247
+ ): preact.VNode<{ [key: string]: unknown }> {
248
+ props = props || {};
249
+ if (children.length) {
250
+ props.children = children;
251
+ }
252
+ return elementCtor({
253
+ type,
254
+ props: props as any,
255
+ key: props?.["key"],
256
+ ref: props?.["ref"] as any,
257
+ sourceModuleId: callingModule?.id,
258
+ callStack: qreact.INJECT_LINE_NUMBERS ? new Error().stack : undefined,
259
+ allowedToRender: ALLOWED_TO_RENDER,
260
+ // NOTE: Compatibility with preact is too difficult, because it REQUIRES special minified names. However,
261
+ // if you're using qreact, you really have to use qreact.render anyways, so this is fine...
262
+ // constructor: undefined,
263
+ // __: null,
264
+ // __b: 0,
265
+ // __c: null,
266
+ // __d: undefined,
267
+ // __e: null,
268
+ // __i: -1,
269
+ // __k: null,
270
+ // __u: 0,
271
+ // __v: 1
272
+ });
273
+ }
274
+
275
+
276
+ /** IMPORTANT! DO NOT call this on user controlled data, otherwise the user will be able to render
277
+ * arbitrary data clientside, which lets them take over the side with large elements to trick
278
+ * users to perform actions (such as clicking on a div they think is a "confirm" button, but which
279
+ * is above a "delete your account" button).
280
+ */
281
+ export function rehydrateVirtualDOM<T extends VirtualDOM>(vNode: T): T {
282
+ return temporaryUnsafeVirtualDOM(() => QRenderClass.diffVNodes({
283
+ next: vNode,
284
+ prev: undefined,
285
+ onNewNode(vNode, anchor, parent) {
286
+ if (isVNode(vNode)) {
287
+ vNode.allowedToRender = ALLOWED_TO_RENDER;
288
+ }
289
+ return undefined;
290
+ },
291
+ onRemovedNode(vNode) {
292
+
293
+ },
294
+ onUpdatedNode(prev, next, anchor, parent) {
295
+ return undefined;
296
+ },
297
+ }) as T);
298
+ }
299
+
300
+ export function Fragment(props: { children: preact.ComponentChild[] }) {
301
+ return props.children;
302
+ }
303
+
304
+ export const getSchemaPrefix = lazy(() => {
305
+ return getProxyPath(() => schema()[LOCAL_DOMAIN].qreact.components);
306
+ });
307
+
308
+ let seqNum = 0;
309
+ function nextLocalId() {
310
+ return seqNum++;
311
+ }
312
+
313
+ /** All of the fields of QRenderClass which should be externally accessible. */
314
+ export type ExternalRenderClass = {
315
+ data(): QComponent;
316
+ getParent(): ExternalRenderClass | undefined;
317
+ readonly renderWatcher: SyncWatcher;
318
+ readonly VNodeWatcher: SyncWatcher;
319
+ readonly mountDOMWatcher: SyncWatcher;
320
+ readonly debugName: string;
321
+ readonly instance: preact.Component<{ [key: string]: unknown }>;
322
+ getVSCodeLink(): string | undefined;
323
+ // TODO: We can add more types here as needed, exposing parts of QRenderClass which
324
+ // are safe for external callers to access.
325
+ };
326
+
327
+ function getCurrentComponentId() {
328
+ return QRenderClass.renderingComponentId ?? QRenderClass.eventComponentId;
329
+ }
330
+ function getRenderingComponent() {
331
+ return QRenderClass.getInstanceAllowedUndefined(QRenderClass.renderingComponentId ?? -1) as ExternalRenderClass | undefined;
332
+ }
333
+ function getAncestorProps<Props>(
334
+ componentClass: { new(...args: any[]): { props: Props } },
335
+ component?: unknown
336
+ ): Props | undefined {
337
+ let id = (component as any)?.[idSymbol];
338
+ let ancestor = getAncestor(componentClass, id);
339
+ if (!ancestor) return undefined;
340
+ return ancestor.instance.props as Props;
341
+ }
342
+ function getAncestor(
343
+ componentClass: unknown,
344
+ componentId = getCurrentComponentId()
345
+ ): ExternalRenderClass | undefined {
346
+ let component = QRenderClass.getInstanceAllowedUndefined(componentId ?? -1);
347
+ while (component) {
348
+ if (component.instance.constructor === componentClass) return component;
349
+ component = component.getParent();
350
+ }
351
+ return undefined;
352
+ }
353
+ function onDispose(fnc: () => void) {
354
+ let component = getRenderingComponent();
355
+ if (!component) throw new Error(`Can watch dispose if not inside of a `);
356
+ watchDispose(component, fnc);
357
+ }
358
+ function watchDispose(renderClass: ExternalRenderClass, fnc: () => void) {
359
+ let watchers = disposeWatchers.get(renderClass);
360
+ if (!watchers) {
361
+ watchers = new Set();
362
+ disposeWatchers.set(renderClass, watchers);
363
+ }
364
+ watchers.add(fnc);
365
+ }
366
+ let disposeWatchers = new Map<ExternalRenderClass, Set<() => void>>();
367
+
368
+ let mouseIsDown = false;
369
+ if (!isNode()) {
370
+ document.addEventListener("mousedown", () => mouseIsDown = true);
371
+ document.addEventListener("mouseup", () => mouseIsDown = false);
372
+ }
373
+
374
+
375
+ let renderClasses = new Map<number, QRenderClass>();
376
+ // NOTE: In theory this COULD BE our root Component, but... it makes compatibility easier
377
+ // if it is separate. It also keeps better isolation between user components and the framework
378
+ // (as it is weird for user component to inherit the framework code...).
379
+ class QRenderClass {
380
+ public static renderingComponentId: number | undefined;
381
+ public static eventComponentId: number | undefined;
382
+
383
+ public id = nextLocalId();
384
+
385
+ public renderWatcher: SyncWatcher;
386
+ public VNodeWatcher: SyncWatcher;
387
+ public mountDOMWatcher: SyncWatcher;
388
+ public instance: preact.Component<{ [key: string]: unknown }>;
389
+ public debugName: string;
390
+
391
+ public domNodes = new Map<VirtualDOM, {
392
+ componentChildren: DOMNode[];
393
+ } | {
394
+ // NOTE: We have to use an array, because VirtualDOM could be a primitive, which can result
395
+ // in multiple indistinguishable TextNodes (apart from by their index within their parent).
396
+ directChildren: DOMNode[];
397
+ }>();
398
+ public childComponents = new Map<VirtualDOM, number>();
399
+
400
+ public vNode: VirtualDOM | undefined;
401
+ public prevMountedVNode: VirtualDOM | undefined;
402
+ public internalRootDOMNodes: DOMNode[] | undefined;
403
+
404
+ public data() {
405
+ return schema()[LOCAL_DOMAIN].qreact.components[this.id];
406
+ }
407
+
408
+ constructor(public config: {
409
+ vNode: preact.VNode<{ [key: string]: unknown }> & { type: preact.ComponentClass };
410
+ props: { [key: string]: unknown };
411
+ rawProps: { [key: string]: unknown };
412
+ /* The root of all of the rendering */
413
+ masterRootDOMNode?: DOMNode;
414
+ id?: number;
415
+ // Should JUST be used for debugging, and for things such as context, getParent should be used
416
+ parentId?: number;
417
+ }) {
418
+ qreact.allComponents.add(this);
419
+ this.id = config.id ?? this.id;
420
+ renderClasses.set(this.id, this);
421
+
422
+ let vNode = config.vNode;
423
+ let self = this;
424
+ // Set up watchers for virtual watching AND dom mounting
425
+ let debugName = vNode.type.name + `|${this.id}|`;
426
+ const getDebugName = (type: string) => {
427
+ return vNode.type.name + `|${type}|${this.id}`;
428
+ };
429
+ const registerOwnTime = (type: string, startTime: number) => {
430
+ registerComponentStats(vNode.type.name + "." + type, Date.now() - startTime);
431
+ };
432
+ this.debugName = debugName;
433
+
434
+ let statics = vNode.type as QComponentStatic;
435
+ // NOTE: This used to exist in an `atomicWrites: true` runOnce, but... I don't think that is required anymore
436
+ let instance = new vNode.type(config.props) as preact.Component<{ [key: string]: unknown }>;
437
+ (instance as any)[idSymbol] = this.id;
438
+ if (isValueProxy(instance)) {
439
+ // This will probably result in an error later on. It is usually due to us failing to identify fragments.
440
+ console.warn(`Constructor returned a proxy, instead of a component.`);
441
+ }
442
+ instance.setState = (state) => {
443
+ for (let key in state) {
444
+ (instance as any).state[key] = (state as any)[key];
445
+ }
446
+ };
447
+ instance.forceUpdate = () => {
448
+ // Delay, as we aren't allowed to have nested trigger?
449
+ if (proxyWatcher.inWatcher()) {
450
+ Querysub.onCommitFinished(() => {
451
+ this.renderWatcher?.explicitlyTrigger();
452
+ });
453
+ } else {
454
+ this.renderWatcher?.explicitlyTrigger();
455
+ }
456
+ };
457
+ this.instance = instance;
458
+
459
+ let initialState = {};
460
+ if (Schema2Fncs.getPaths(instance.state).length > 0) {
461
+ let statePath = getProxyPath(() => this.data().state);
462
+ let state = instance.state;
463
+ registerSchemaPrefix({ schema: state, prefixPathStr: statePath });
464
+ watchDispose(this, () => {
465
+ unregisterSchemaPrefix({ schema: state, prefixPathStr: statePath });
466
+ });
467
+ } else {
468
+ initialState = instance.state || {};
469
+ }
470
+
471
+
472
+ // Force state to be a getter/setter
473
+ Object.defineProperty(instance, "state", {
474
+ get() { return self.data().state; },
475
+ set(value) { self.data().state = value; },
476
+ });
477
+
478
+ proxyWatcher.runOnce({
479
+ debugName: getDebugName("setInitialState"),
480
+ atomicWrites: false,
481
+ noLocks: true,
482
+ watchFunction() {
483
+ // TODO: Why are we iterating instead of just setting it directly, with `state = initialState` ?
484
+ for (let [key, value] of Object.entries(initialState)) {
485
+ (instance as any).state[key] = value;
486
+ }
487
+ self.data().parentComponentId = config.parentId;
488
+ },
489
+ });
490
+
491
+ self.instance.componentWillMount && logErrors(proxyWatcher.commitFunction({
492
+ debugName: getDebugName("componentWillMount"),
493
+ watchFunction() {
494
+ self.instance.componentWillMount?.();
495
+ },
496
+ }));
497
+
498
+ const runContextCallback = (code: (() => void) | undefined) => {
499
+ if (!code) return;
500
+ let prevEvent = QRenderClass.eventComponentId;
501
+ QRenderClass.eventComponentId = self.id;
502
+ try {
503
+ code();
504
+ } finally {
505
+ QRenderClass.eventComponentId = prevEvent;
506
+ }
507
+ };
508
+
509
+
510
+ let nextInlineId = 1;
511
+
512
+ let renderPrevVNode: VirtualDOM | undefined;
513
+ // Watch render. In it's own watcher so it can write with locks (because most code will only read/write from
514
+ // local state, and therefore does not need locks).
515
+ const renderWatcher = this.renderWatcher = proxyWatcher.createWatcher({
516
+ debugName: getDebugName("render"),
517
+ canWrite: true,
518
+ watchFunction() {
519
+
520
+ // NOTE: We might want to consider waiting if we have a synchronous trigger. Synchronous
521
+ // re-renders cause weird things with event bubbling, that is often unexpected (and
522
+ // never intended). Ex, a parent node is re-used, so the event bubbles to what
523
+ // was never the click target's real parent, which could be in a completely different
524
+ // part of the screen.
525
+ // IMPORTANT! isCurrentLoopSynchronous doesn't work, because... our watch loop waits a microtick,
526
+ // while event handling requires waiting to the end of the event loop (a tick, basically).
527
+ // TODO: So... we really need to explicitly handle events, adding a listener at the document so we
528
+ // can wait for them to finish bubbling. AND, we need to handle stopPropagation (and preventDefault?)
529
+ // so we know when to trigger them early.
530
+ // - Fortunately, frame rendering is so slow, that is is easy to verify we don't break anything.
531
+ // If we log the time in the event, and in the render, they should be a few millisecond apart.
532
+ // If they are > 10ms apart (16ms, 32ms, etc), then we waited a frame, which is really bad
533
+ // (it halves our framerate, or more).
534
+ // - The archive viewer has mitigation code via drag.ts, which setTimeouts onDone
535
+ // (as the onDone doesn't happen that often). If we comment that out, the chart will
536
+ // navigate on bar click, which is a good way to verify any kind of fix works.
537
+ // ACTUALLY! Even waiting for bubbling won't work. We need to wait for the onclick to happen,
538
+ // which isn't even queued yet?
539
+ // - We might need to explicitly handle onMouseUp / onClick?
540
+ // if (clientWatcher.isCurrentLoopSynchronous()) {
541
+ // setImmediate(() => {
542
+ // renderWatcher.explicitlyTrigger();
543
+ // });
544
+ // return;
545
+ // }
546
+
547
+ if (self.disposed || self.disposing) return;
548
+ let vNode: VirtualDOM;
549
+ try {
550
+ let time = Date.now();
551
+ QRenderClass.renderingComponentId = self.id;
552
+ self.data().newPropSeqNum;
553
+ schema()[LOCAL_DOMAIN].hotreloadSeqNum;
554
+ if (typeof instance.render !== "function") {
555
+ // The component might be a functional component, in which case we can just call it
556
+ vNode = new config.vNode.type(config.props) as unknown as VirtualDOM;
557
+ } else {
558
+ vNode = instance.render() as VirtualDOM;
559
+ }
560
+ registerOwnTime("render", time);
561
+
562
+ time = Date.now();
563
+
564
+ vNode = QRenderClass.normalizeVNode({ vNode, array: true });
565
+
566
+ registerOwnTime("parseVDom", time);
567
+
568
+ } catch (e: any) {
569
+ // NOTE: Rethrowing the error WON'T let us have an "uncaught exception" (which is useful when
570
+ // debugging, as it makes automatically stepping in easier), because proxyWatcher has to
571
+ // catch it anyways!
572
+ vNode = qreact.errorHandler({ error: e, debugName });
573
+ vNode = QRenderClass.normalizeVNode({ vNode, array: true });
574
+ } finally {
575
+ QRenderClass.renderingComponentId = undefined;
576
+ }
577
+
578
+ self.data().vNodeForRender = atomicObjectWriteNoFreeze({ value: vNode });
579
+ componentRendered(self);
580
+ }
581
+ });
582
+
583
+ // Update props, dispose, and create new component children
584
+ this.VNodeWatcher = proxyWatcher.createWatcher({
585
+ debugName: getDebugName("vNodeDiff"),
586
+ canWrite: true,
587
+ atomicWrites: true,
588
+ noLocks: true,
589
+ watchFunction() {
590
+ if (self.disposed || self.disposing) return;
591
+ let vNodeValue = atomic(self.data().vNodeForRender);
592
+ if (!vNodeValue) return;
593
+ let time = Date.now();
594
+ let vNode = vNodeValue?.value;
595
+
596
+ let prev = self.vNode;
597
+ self.data().vNodeForMount = vNode;
598
+
599
+ let prevChildComponents = self.childComponents;
600
+ let nextChildComponents = new Map() as typeof prevChildComponents;
601
+ let onSynced: (() => void)[] = [];
602
+
603
+ QRenderClass.diffVNodes({
604
+ alreadyNormalized: true,
605
+ prev: prev,
606
+ next: vNode,
607
+ onNewNode(vNode) {
608
+ if (!isVNodeClass(vNode)) return undefined;
609
+
610
+ let vNodeId = nextLocalId();
611
+ let syncedData = QRenderClass.getData(vNodeId);
612
+ let props: { [key: string]: unknown } = Object.create(null);
613
+ let rawProps: { [key: string]: unknown } = Object.create(null);
614
+ for (let [key, value] of Object.entries(vNode.props)) {
615
+ rawProps[key] = value;
616
+ }
617
+ if (statics.deepProps) {
618
+ // NOTE: By removing atomic we can leverage the proxy automatically decomposing
619
+ // the props to each individual primitive value.
620
+ doProxyOptions({ atomicWrites: false }, () => {
621
+ syncedData.props = vNode.props;
622
+ props = syncedData.props;
623
+ });
624
+ } else {
625
+ // Have each prop be a synced field, so conditional props work (accessing a prop not
626
+ // provided returns undefined). Atomic writes is on, so write objects
627
+ // aren't destructured (this allows passing functions, exotics, etc).
628
+ for (let [key, value] of Object.entries(vNode.props)) {
629
+ syncedData.props[key] = value;
630
+ // We then need to use a getter in the props, so that it causes a synced read
631
+ // UNLESS that prop is not defined, then it is just undefined
632
+ Object.defineProperty(props, key, {
633
+ configurable: true,
634
+ enumerable: true,
635
+ get() {
636
+ // Always access props, so we incur a watch
637
+ let value = syncedData.props[key];
638
+ // Return undefined, instead of a proxy
639
+ if (rawProps[key] === undefined) return undefined;
640
+ return value;
641
+ },
642
+ });
643
+ }
644
+ }
645
+ nextChildComponents.set(vNode, vNodeId);
646
+
647
+ onSynced.push(() => new QRenderClass({
648
+ vNode: vNode as any,
649
+ props,
650
+ rawProps,
651
+ id: vNodeId,
652
+ parentId: self.id
653
+ }));
654
+ },
655
+ onRemovedNode(vNode) {
656
+ if (!isVNodeClass(vNode)) return;
657
+ const deadId = prevChildComponents.get(vNode);
658
+ if (deadId !== undefined) {
659
+ onSynced.push(() => {
660
+ recursivelyMarkDisposing(deadId);
661
+ function recursivelyMarkDisposing(id: number) {
662
+ let component = QRenderClass.getInstance(id);
663
+ if (!component) return;
664
+ if (component.disposing) return;
665
+ component.disposing = true;
666
+ for (let childId of component.childComponents.values()) {
667
+ recursivelyMarkDisposing(childId);
668
+ }
669
+ }
670
+ void Promise.resolve().finally(() => {
671
+ QRenderClass.getInstance(deadId).dispose();
672
+ });
673
+ });
674
+ }
675
+ },
676
+ onUpdatedNode(prevVNode, nextVNode) {
677
+ if (!isVNodeClass(prevVNode) || !isVNodeClass(nextVNode)) return undefined;
678
+ const childComponent = prevChildComponents.get(prevVNode);
679
+ if (!childComponent) {
680
+ debugger;
681
+ throw new Error(`Could not find component for vNode ${prevVNode}`);
682
+ }
683
+ nextChildComponents.set(nextVNode, childComponent);
684
+
685
+ // Update props
686
+ let prevProps = prevVNode.props;
687
+ let nextProps = nextVNode.props;
688
+ if ("data-break" in nextProps) {
689
+ debugger;
690
+ }
691
+ if (statics.deepProps) {
692
+ // NOTE: By removing atomic we can leverage the proxy automatically decomposing
693
+ // the props to each individual primitive value, and checking each individual
694
+ // primitive value for changes.
695
+ doProxyOptions({ atomicWrites: false }, () => {
696
+ QRenderClass.getData(childComponent).props = nextProps;
697
+ });
698
+ } else {
699
+ let childInstance = QRenderClass.getInstance(childComponent);
700
+ if (!childInstance) {
701
+ console.warn(`Could not find child instance for component ${childComponent}, and so cannot update props`);
702
+ return;
703
+ }
704
+ const props = childInstance.instance.props;
705
+ const rawProps = childInstance.config.rawProps;
706
+ let syncedData = QRenderClass.getData(childComponent);
707
+ for (let key in nextProps) {
708
+ let prev = prevProps[key];
709
+ let next = nextProps[key];
710
+ // NOTE: Object.is is required to handle NaN case
711
+ if (Object.is(prev, next)) continue;
712
+ let prevPath = isValueProxy2(prev);
713
+ let nextPath = isValueProxy2(next);
714
+ if (prevPath && nextPath && prevPath === nextPath) continue;
715
+ if (statics.jsonComparePropUpdates) {
716
+ if (JSON.stringify(prev) === JSON.stringify(next)) continue;
717
+ }
718
+ // ONLY trigger non-synced side-effects if the component once we know the component is synced.
719
+ // The synced writes will be ignored if we aren't synced, but the non-synced writes will not,
720
+ // so we have to be very careful about them.
721
+ onSynced.push(() => {
722
+ if (!(key in props)) {
723
+ // For a re-render, because if the prop was accessed, there was no
724
+ // getter, so it the path won't have been watched.
725
+ childInstance.data().newPropSeqNum++;
726
+ Object.defineProperty(props, key, {
727
+ configurable: true,
728
+ enumerable: true,
729
+ get() {
730
+ // Always access props, so we incur a watch
731
+ let value = syncedData.props[key];
732
+ // Return undefined, instead of a proxy
733
+ if (rawProps[key] === undefined) return undefined;
734
+ return value;
735
+ },
736
+ });
737
+ }
738
+ rawProps[key] = next;
739
+ });
740
+ syncedData.props[key] = next;
741
+ }
742
+ for (let key in prevProps) {
743
+ if (nextProps[key] !== undefined) continue;
744
+ syncedData.props[key] = undefined;
745
+ onSynced.push(() => {
746
+ delete props[key];
747
+ });
748
+ }
749
+
750
+ }
751
+ },
752
+ });
753
+
754
+ // NOTE: I'm not really sure how we can have unsynced accesses anymore, as we don't run with locks,
755
+ // and we should only be accessing local state. But... I guess this doesn't hurt too much?
756
+ // - We might need to eventually remove it for performance reasons though...
757
+ if (Querysub.allSynced()) {
758
+ self.childComponents = nextChildComponents;
759
+ self.vNode = vNode;
760
+ for (let fnc of onSynced) fnc();
761
+ }
762
+
763
+ registerOwnTime("diffVDom", time);
764
+ },
765
+ });
766
+
767
+ // Apply vNode to DOM
768
+ let isFirstMount = true;
769
+ this.mountDOMWatcher = proxyWatcher.createWatcher({
770
+ debugName: getDebugName("updateDOM"),
771
+ canWrite: true,
772
+ atomicWrites: true,
773
+ noLocks: true,
774
+ // Delay DOM execution, so all VNodes can be created before we try to mount anything
775
+ orderGroup: 1,
776
+ // Run in reverse, so children have their dom nodes created first, and then the parents can inject them into their DOM.
777
+ order: -this.id,
778
+ onResultUpdated(result, writes, watcher) {
779
+ if (result && "error" in result) {
780
+ console.error(`Error in ${watcher.debugName}`, result.error);
781
+ setImmediate(() => { throw errorify(result.error); });
782
+ }
783
+ },
784
+ watchFunction() {
785
+ if (self.disposed || self.disposing) return;
786
+ let time = Date.now();
787
+ let next = atomic(self.data().vNodeForMount);
788
+ if (!next) {
789
+ return;
790
+ }
791
+ let prev = atomic(self.prevMountedVNode);
792
+ self.prevMountedVNode = next;
793
+
794
+ let prevDOMNodes = self.domNodes;
795
+ let nextDOMNodes = new Map() as typeof prevDOMNodes;
796
+ self.domNodes = nextDOMNodes;
797
+
798
+ let nextRootDOMNodes: DOMNode[] = [];
799
+ function getComponentChildren(vNode: VirtualDOM) {
800
+ let component = self.childComponents.get(vNode);
801
+ if (!component) {
802
+ console.warn(`Could not find childComponent for attempted component mount in ${debugName}`);
803
+ return [];
804
+ }
805
+ return atomic(QRenderClass.getData(component).rootDOMNodes) ?? [];
806
+ }
807
+
808
+ let activeChain: DOMNode[] = [];
809
+ {
810
+ let curActive = document.activeElement;
811
+ while (curActive) {
812
+ activeChain.push(curActive as DOMNode);
813
+ curActive = curActive.parentElement;
814
+ }
815
+ }
816
+ type Anchor = { parent: DOMNode } | { prev: DOMNode } | undefined;
817
+ function replaceNodes(anchor: Anchor, prevNodes: DOMNode[], nextNodes: DOMNode[]) {
818
+ let mustPreserveFocus = false;
819
+ // Moving elements around doesn't only break focus... it also breaks clicks! So... once the user clicks,
820
+ // stop replacing children, and use our non-invasive technique.
821
+ if (mouseIsDown) {
822
+ mustPreserveFocus = true;
823
+ }
824
+ if (!mustPreserveFocus) {
825
+ let prevNodesSet = new Set(prevNodes);
826
+ for (let nextNode of nextNodes) {
827
+ if (prevNodesSet.has(nextNode) && activeChain.includes(nextNode)) {
828
+ mustPreserveFocus = true;
829
+ break;
830
+ }
831
+ }
832
+ let changedNodes = new Set<DOMNode>();
833
+ for (let nextNode of nextNodes) {
834
+ if (!prevNodesSet.has(nextNode)) {
835
+ changedNodes.add(nextNode);
836
+ }
837
+ }
838
+ let nextNodesSet = new Set(nextNodes);
839
+ for (let prevNode of prevNodes) {
840
+ if (!nextNodesSet.has(prevNode)) {
841
+ changedNodes.add(prevNode);
842
+ }
843
+ }
844
+ // HACK: Due to issues with scroll positions being lost, just use the slow technique
845
+ // unless we have a lot of changes.
846
+ if (changedNodes.size < 3000) {
847
+ mustPreserveFocus = true;
848
+ }
849
+ }
850
+ if (!mustPreserveFocus && anchor && "parent" in anchor && activeChain.includes(anchor.parent)) {
851
+ mustPreserveFocus = true;
852
+ }
853
+ // Also if we are scrolled, preserved that
854
+ if (!mustPreserveFocus && nextNodes.some(x => x.scrollTop > 0)) {
855
+ mustPreserveFocus = true;
856
+ }
857
+
858
+ if (anchor && "parent" in anchor && !mustPreserveFocus) {
859
+ let prevFocus = document.activeElement;
860
+ anchor.parent.replaceChildren(...nextNodes);
861
+ if (prevFocus !== document.activeElement) {
862
+ console.error(`We broke focus by rendering, fix this`);
863
+ debugger;
864
+ }
865
+ } else {
866
+ // NOTE: It is A LOT slower to call remove and insert, but... it preserves focus,
867
+ // and is simple (and works with prev nodes, etc).
868
+ // NOTE: In my testing documentFragments aren't that much faster. There probably is still a way
869
+ // to optimize this though...
870
+ let nextNodesSet = new Set(nextNodes);
871
+ for (let prevNode of prevNodes) {
872
+ if (!nextNodesSet.has(prevNode)) {
873
+ prevNode.remove();
874
+ }
875
+ }
876
+ // NOTE: insertInAnchor is smart, and will noop if the insertion does nothing.
877
+ for (let nextElem of nextNodes) {
878
+ insertInAnchor(anchor, nextElem);
879
+ anchor = { prev: nextElem };
880
+ }
881
+ }
882
+ }
883
+
884
+ function insertInAnchor(anchor: Anchor, node: DOMNode) {
885
+ let prev = document.activeElement;
886
+ if (!anchor) {
887
+ nextRootDOMNodes.push(node);
888
+ return;
889
+ }
890
+ if ("parent" in anchor) {
891
+ if (anchor.parent.firstChild === node) return;
892
+ anchor.parent.insertBefore(node, anchor.parent.firstChild || null);
893
+ return;
894
+ }
895
+ if (nextRootDOMNodes[nextRootDOMNodes.length - 1] === anchor.prev) {
896
+ nextRootDOMNodes.push(node);
897
+ return;
898
+ }
899
+ if (anchor.prev.nextSibling === node) {
900
+ return;
901
+ }
902
+
903
+ // BUT, if the node is in the sibling order, then
904
+ // just remove the nodes which are in the way, and insert the node.
905
+ // - This handles the case of previous nodes being removed, which is far more common
906
+ // than nodes being rearranged (which would be the only reason to node remove the
907
+ // intermediate nodes, such as if the first node became the last).
908
+ if (node.parentElement === anchor.prev.parentElement) {
909
+ // First, check if the node is there
910
+ let cur = anchor.prev.nextSibling;
911
+ while (cur && cur !== node) {
912
+ let next = cur.nextSibling;
913
+ cur = next;
914
+ }
915
+ // If it is, remove the intermediate, and then... we will be the next node!
916
+ if (cur === node) {
917
+ cur = anchor.prev.nextSibling;
918
+ while (cur && cur !== node) {
919
+ let next = cur.nextSibling;
920
+ cur.remove();
921
+ cur = next;
922
+ }
923
+ if (anchor.prev.nextSibling !== node) {
924
+ // Impossible!? We just removed all the other siblings!
925
+ debugger;
926
+ }
927
+ return;
928
+ }
929
+ }
930
+
931
+ anchor.prev.after(node);
932
+ if (prev !== document.activeElement) {
933
+ debugger;
934
+ }
935
+ }
936
+
937
+ let isParentSVG = !!atomic(self.data().isParentSVG);
938
+ function isSVG(node: DOMNode | undefined) {
939
+ return isParentSVG || node?.namespaceURI === "http://www.w3.org/2000/svg";
940
+ }
941
+
942
+ function triggerRef(vNode: VirtualDOM, node: DOMNode) {
943
+ if (!isVNode(vNode)) return;
944
+ const ref = vNode.ref;
945
+ if (typeof ref === "function") {
946
+ // IMPORTANT! We have to detach the ref, otherwise it might access unsynced state, which will
947
+ // break our render function.
948
+ void Promise.resolve().finally(() => {
949
+ logErrors(proxyWatcher.commitFunction({
950
+ canWrite: true,
951
+ watchFunction() {
952
+ ref(node);
953
+ },
954
+ }));
955
+ });
956
+ }
957
+ }
958
+
959
+ // NOTE: This is a bit inefficient as it requires running twice, because the first run our child
960
+ // components will have no rootDOMNodes. But... it's probably fine...
961
+ QRenderClass.diffVNodes({
962
+ alreadyNormalized: true,
963
+ next,
964
+ prev,
965
+ onNewNode(vNode, anchor, parent) {
966
+ if (isVNodeClass(vNode)) {
967
+ if (isSVG(parent)) {
968
+ let component = self.childComponents.get(vNode);
969
+ if (component) {
970
+ QRenderClass.getData(component).isParentSVG = true;
971
+ }
972
+ }
973
+
974
+ let domNodes = getComponentChildren(vNode);
975
+ nextDOMNodes.set(vNode, { componentChildren: domNodes });
976
+ for (let node of domNodes) {
977
+ insertInAnchor(anchor, node);
978
+ anchor = { prev: node };
979
+ }
980
+ return domNodes[domNodes.length - 1];
981
+ }
982
+
983
+ let node = createDOMNode(vNode, isSVG(parent));
984
+ if (isVNode(vNode)) {
985
+ let ref2 = (vNode as any).ref2 || vNode.props.ref2;
986
+ if (typeof ref2 === "function") {
987
+ if (!vNode.key) {
988
+ console.warn(`Using ref2 without a key is not recommended. Due to node reuse it is possible for a node to be added with ref that reuses an existing node, which will prevent ref from every being called. Ref used in ${debugName}, with vNode ${vNode.type} and ref ${vNode.ref}. Full stack ${self.getDebugComponentStack().map(x => x.debugName).join(", ")}`);
989
+ }
990
+ // IMPORTANT! We have to detach the ref, otherwise it might access unsynced state, which will
991
+ // break our render function.
992
+ void Promise.resolve().finally(() => {
993
+ logErrors(proxyWatcher.commitFunction({
994
+ canWrite: true,
995
+ watchFunction() {
996
+ ref2(node);
997
+ },
998
+ }));
999
+ });
1000
+ }
1001
+
1002
+ triggerRef(vNode, node);
1003
+ }
1004
+
1005
+ let obj = nextDOMNodes.get(vNode);
1006
+
1007
+ if (isVNode(vNode) && typeof vNode.type !== "function" && nextDOMNodes.has(vNode)) {
1008
+ // TODO: We should allow this, it happens a lot when passing components as props
1009
+ throw new Error(`VNode was reused. This is not supported. In ${debugName}, type ${vNode.type}`);
1010
+ }
1011
+ if (!obj) nextDOMNodes.set(vNode, obj = { directChildren: [] });
1012
+ if ("componentChildren" in obj) {
1013
+ debugger;
1014
+ throw new Error(`Internal error, component was matched with a non-component DOM node in ${debugName}`);
1015
+ }
1016
+
1017
+ if (isVNode(vNode) && vNode.props["data-break"]) {
1018
+ debugger;
1019
+ }
1020
+
1021
+ obj.directChildren.push(node);
1022
+ updateDOMNodeFields(node, vNode, undefined, self);
1023
+ insertInAnchor(anchor, node);
1024
+ return node;
1025
+ },
1026
+ onRemovedNode(vNode) {
1027
+ let domNodes = prevDOMNodes.get(vNode);
1028
+ if (!domNodes) return;
1029
+
1030
+ if (isVNode(vNode) && vNode.props["data-break"]) {
1031
+ debugger;
1032
+ }
1033
+
1034
+ // NOTE: We remove all the nodes, even if some are still used. This only impacts identical TextNodes,
1035
+ // which is fine, as reusing them probably doesn't add much performance?
1036
+ prevDOMNodes.delete(vNode);
1037
+ let nodes = "componentChildren" in domNodes ? domNodes.componentChildren : domNodes.directChildren;
1038
+ for (let node of nodes) {
1039
+ // NOTE: Always remove, as we are removing all duplicates with the same vNode,
1040
+ // which may have different parents, some of which may be removed, and some of which are kept.
1041
+ node.remove();
1042
+ /*
1043
+ // We SHOULDN'T have to remove event listeners, but maybe we will have to?
1044
+ let eventNode = node as DOMNode & { _eventHandlers?: { [key: string]: EventListener } };
1045
+ for (let key in eventNode._eventHandlers) {
1046
+ eventNode.removeEventListener(key, eventNode._eventHandlers[key]);
1047
+ }
1048
+ delete eventNode._eventHandlers;
1049
+ */
1050
+ if (isVNode(vNode)) {
1051
+ if (typeof vNode.ref === "function") {
1052
+ vNode.ref(null);
1053
+ }
1054
+ }
1055
+ }
1056
+ },
1057
+ onUpdatedNode(prev, vNode, anchor, parent) {
1058
+ // TODO: It MIGHT be faster to remove TextNodes instead of reusing them? I'm not sure
1059
+ // how the browser handles reused TextNodes, but we might be in the case where
1060
+ // we completely rearrange them, which might cause performance issues?
1061
+ let prevNodeBase = prevDOMNodes.get(prev);
1062
+
1063
+ // NOTE: Types don't change here (otherwise it would be a new and remove, not an update),
1064
+ // so it is always the case that `prev.type === next.type`
1065
+ if (isVNode(prev) && isVNode(vNode)) {
1066
+ if (typeof prev.type === "function") {
1067
+ if (!prevNodeBase || !("componentChildren" in prevNodeBase)) {
1068
+ throw new Error(`Internal error, component was matched with a non-component DOM node in ${debugName}`);
1069
+ }
1070
+ let prevNodes = prevNodeBase.componentChildren;
1071
+ let nextNodes = getComponentChildren(vNode);
1072
+ nextDOMNodes.set(vNode, { componentChildren: nextNodes });
1073
+
1074
+ replaceNodes(anchor, prevNodes, nextNodes);
1075
+
1076
+ return nextNodes[nextNodes.length - 1];
1077
+ } else {
1078
+ // Built-in ("div", "span", etc)
1079
+ if (!prevNodeBase || "componentChildren" in prevNodeBase) {
1080
+ throw new Error(`Internal error, non-component DOM node was matched with a component node in ${debugName}`);
1081
+ }
1082
+ let prevNodes = prevNodeBase.directChildren;
1083
+ if (prevNodes.length !== 1) {
1084
+ debugger;
1085
+ throw new Error(`Internal error, non-primitive DOM node had more than one child in ${debugName}`);
1086
+ }
1087
+ if (prev.props["data-break"] || vNode.props["data-break"]) {
1088
+ debugger;
1089
+ }
1090
+ nextDOMNodes.set(vNode, { directChildren: prevNodes });
1091
+ let domNode = prevNodes[0];
1092
+
1093
+ // Call insert, in case the order's changed
1094
+ insertInAnchor(anchor, domNode);
1095
+ updateDOMNodeFields(domNode, vNode, prev, self);
1096
+
1097
+ // Don't trigger ref into the dom node is updated
1098
+ triggerRef(vNode, domNode);
1099
+
1100
+ return domNode;
1101
+ }
1102
+ } else {
1103
+ // Primitive
1104
+ if (prevNodeBase && "componentChildren" in prevNodeBase) {
1105
+ throw new Error(`Internal error, non-component DOM node was matched with a component node in ${debugName}`);
1106
+ }
1107
+ // Take the first node
1108
+ let prevNodes = prevNodeBase?.directChildren;
1109
+ let prevNode = prevNodes?.shift();
1110
+ let nextNodes = nextDOMNodes.get(vNode);
1111
+ if (!nextNodes) {
1112
+ nextDOMNodes.set(vNode, nextNodes = { directChildren: [] });
1113
+ }
1114
+ if ("componentChildren" in nextNodes) {
1115
+ throw new Error(`Internal error, next nodes is corrupted ${debugName}`);
1116
+ }
1117
+ if (!prevNode) {
1118
+ prevNode = createDOMNode(vNode, isSVG(parent));
1119
+ } else {
1120
+ updateDOMNodeFields(prevNode, String(vNode), prev, self);
1121
+ }
1122
+ nextNodes.directChildren.push(prevNode);
1123
+
1124
+ insertInAnchor(anchor, prevNode);
1125
+ return prevNode;
1126
+ }
1127
+ },
1128
+ });
1129
+
1130
+ // NOTE: We can't update the root dom nodes inline (such as by finding the previous value, and using that
1131
+ // as an anchor), because if we do it might mess up our parent when they try to update our dom nodes.
1132
+ // (And there shouldn't be too much of a delay between our mount and our parent re-running their mount)
1133
+
1134
+ // Update rootDOMNodes efficiently. If only fields change, we don't need to update them. They only need
1135
+ // to be updated if the instances change.
1136
+ let prevRootDOMNodes = self.internalRootDOMNodes ?? [];
1137
+ self.internalRootDOMNodes = nextRootDOMNodes;
1138
+ function rootDOMNodesChanged() {
1139
+ if (prevRootDOMNodes.length !== nextRootDOMNodes.length) return true;
1140
+ for (let i = 0; i < prevRootDOMNodes.length; i++) {
1141
+ if (prevRootDOMNodes[i] !== nextRootDOMNodes[i]) return true;
1142
+ }
1143
+ return false;
1144
+ }
1145
+ if (rootDOMNodesChanged()) {
1146
+ for (let node of nextRootDOMNodes) {
1147
+ if ("setAttribute" in node && !node.hasAttribute("data-qreact-id")) {
1148
+ node.setAttribute("data-qreact-id", String(self.id));
1149
+ node.setAttribute("data-qreact-name", self.debugName);
1150
+ }
1151
+ }
1152
+ self.data().rootDOMNodes = nextRootDOMNodes;
1153
+ }
1154
+
1155
+ if (config.masterRootDOMNode) {
1156
+ replaceNodes(
1157
+ { parent: config.masterRootDOMNode },
1158
+ Array.from(config.masterRootDOMNode.children) as DOMNode[],
1159
+ nextRootDOMNodes
1160
+ );
1161
+ }
1162
+
1163
+ if (isFirstMount) {
1164
+ isFirstMount = false;
1165
+ if (self.instance.componentDidMount) {
1166
+ void Promise.resolve().finally(() => {
1167
+ logErrors(proxyWatcher.commitFunction({
1168
+ canWrite: true,
1169
+ watchFunction() {
1170
+ QRenderClass.renderingComponentId = self.id;
1171
+ try {
1172
+ runContextCallback(() => self.instance?.componentDidMount?.());
1173
+ } finally {
1174
+ QRenderClass.renderingComponentId = undefined;
1175
+ }
1176
+ },
1177
+ }));
1178
+ });
1179
+ }
1180
+ }
1181
+
1182
+ if (Querysub.anyUnsynced()) {
1183
+ throw new Error(`DOM mounting shouldn't access ANY remote values. If it does, it requires rerunning DOM constructors, which is highly inefficient. Any callbacks should be detached with Promise.resolve().finally(...), etc.`);
1184
+ }
1185
+
1186
+ registerOwnTime("mount", time);
1187
+ }
1188
+ });
1189
+ }
1190
+
1191
+ private static componentIdDedupeBase = cache((name: string) => {
1192
+ let index = 0;
1193
+ return () => {
1194
+ return name + "_" + index++;
1195
+ };
1196
+ });
1197
+ private static componentIdDedupe = cache((fnc: preact.ComponentType) => {
1198
+ return QRenderClass.componentIdDedupeBase(fnc.name)();
1199
+ });
1200
+
1201
+
1202
+ /*
1203
+ - Converts all props.children to VirtualDOM[].
1204
+ - Shallow copies everything as well.
1205
+ - Shallow copies props.
1206
+ - Flattens arrays
1207
+ - If "config.array", converts the root to be VirtualDOM[]
1208
+
1209
+
1210
+ NOTE: Called inside of diffVNodes, which is already called by default, so this usually shouldn't be
1211
+ called explicitly.
1212
+ */
1213
+ private static normalizeVNode(config: {
1214
+ vNode: VirtualDOM;
1215
+ array?: boolean;
1216
+ }): VirtualDOM {
1217
+ let result = normalizeNode(config.vNode);
1218
+ if (config.array) {
1219
+ return result as VirtualDOM;
1220
+ }
1221
+ return result[0];
1222
+ function normalizeNode(node: VirtualDOM | VirtualDOM[] | undefined): VirtualDOM[] {
1223
+ let flattened = fullyFlatArrays(node);
1224
+ flattened = flattened.map(sub => {
1225
+ if (!isVNode(sub)) return sub;
1226
+ sub = { ...sub };
1227
+ // Shallow copy props, as even though they might contain proxy or otherwise complex values,
1228
+ // props itself should just be a lookup.
1229
+ sub.props = { ...sub.props };
1230
+ if (typeof sub.type === "string") {
1231
+ let children = sub.props.children;
1232
+ if (!children) {
1233
+ sub.props.children = [];
1234
+ } else {
1235
+ sub.props.children = normalizeNode(children as any);
1236
+ }
1237
+ }
1238
+ return sub;
1239
+ });
1240
+ return flattened;
1241
+
1242
+ function fullyFlatArrays(value: VirtualDOM | VirtualDOM[] | VirtualDOM[][], output: VirtualDOM[] = []): VirtualDOM[] {
1243
+ // If we don't detect proxies early we might end up creating watches on values we don't
1244
+ // really care about ("type", etc). So, replace them immediately with strings.
1245
+ if (isValueProxy2(value)) return [String(node)];
1246
+ if (value === undefined || value === null || value === false) {
1247
+ return output;
1248
+ }
1249
+ if (!canHaveChildren(value)) {
1250
+ output.push(value as any);
1251
+ return output;
1252
+ }
1253
+ if (isFragment(value)) {
1254
+ fullyFlatArrays((value as any).props.children as any, output);
1255
+ return output;
1256
+ }
1257
+ if (Array.isArray(value)) {
1258
+ for (let item of value) {
1259
+ fullyFlatArrays(item, output);
1260
+ }
1261
+ return output;
1262
+ }
1263
+ output.push(value);
1264
+ return output;
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ // NOTE: We call diffVNodes twice every time a component changes. This is fine, diffing is just reading, which is
1270
+ // extremely fast. This also provides the potential for lazy/throttled DOM reconcilation (the slowest part of rendering),
1271
+ // which can make the UI feel considerably more responsive.
1272
+ // NOTE: We don't optimize for cases where only a few nodes have changes, but most are ===. vNode reuse is
1273
+ // rare, and barely helps. Instead they should split their component into smaller components.
1274
+ // NOTE: All nodes are either new, or updates. We allow updates if the types are the same (VNode.type OR if they
1275
+ // are primitives, always the same).
1276
+ public static diffVNodes(config: {
1277
+ alreadyNormalized?: boolean;
1278
+ prev: VirtualDOM | undefined;
1279
+ next: VirtualDOM;
1280
+ onTransformNode?: (
1281
+ prev: VirtualDOM,
1282
+ next: VirtualDOM,
1283
+ ) => VirtualDOM;
1284
+ // NOTE: Anchor is the value returned from onNewNode/onUpdatedNode of the parent/prev node. If either returns
1285
+ // undefined we assume the anchor is unchanged, and keep the some one (parent or prev).
1286
+ onNewNode: (
1287
+ vNode: VirtualDOM,
1288
+ anchor: { parent: DOMNode } | { prev: DOMNode } | undefined,
1289
+ parent: DOMNode | undefined,
1290
+ ) => DOMNode | undefined;
1291
+ onRemovedNode: (vNode: VirtualDOM) => void;
1292
+ // NOTE: onUpdatedNode might not mean the node is updated, it is just what we call if it isn't new or removed.
1293
+ onUpdatedNode: (
1294
+ prev: VirtualDOM,
1295
+ next: VirtualDOM,
1296
+ anchor: { parent: DOMNode } | { prev: DOMNode } | undefined,
1297
+ parent: DOMNode | undefined
1298
+ ) => DOMNode | undefined;
1299
+ }): VirtualDOM {
1300
+ let { prev, next, onNewNode, onRemovedNode, onUpdatedNode, onTransformNode } = config;
1301
+
1302
+ if (!config.alreadyNormalized) {
1303
+ next = QRenderClass.normalizeVNode({ vNode: next, array: true });
1304
+ }
1305
+
1306
+ iterate(next as VirtualDOM[], (prev ?? []) as VirtualDOM[], undefined);
1307
+ return next;
1308
+ function iterate(
1309
+ next: VirtualDOM[],
1310
+ prev: VirtualDOM[],
1311
+ parent: DOMNode | undefined,
1312
+ ) {
1313
+ function getNodeKey(node: VirtualDOM) {
1314
+ if (Array.isArray(node)) return 0;
1315
+ if (isVNode(node)) {
1316
+ if (node.key) {
1317
+ return String(node.key);
1318
+ }
1319
+ // So... if you change the value of a select option, it changes the parent select.value. Which is...
1320
+ // odd. We can easily fix this by just using the value as the key, which prevents us from ever
1321
+ // changing the .value
1322
+ if (node.type === "option") {
1323
+ return String(node.props.value);
1324
+ }
1325
+ let type = node.type;
1326
+ if (typeof type === "function") {
1327
+ return QRenderClass.componentIdDedupe(type);
1328
+ }
1329
+ return type;
1330
+ }
1331
+ return typeof node;
1332
+ }
1333
+
1334
+
1335
+ let anchor: { parent: DOMNode } | { prev: DOMNode } | undefined = parent ? { parent } : undefined;
1336
+
1337
+ let sourceNodes = new Map<string | number, VirtualDOM[]>();
1338
+ if (!Array.isArray(prev)) {
1339
+ // IMPORTANT! prev was probably an error, look at `prev.props.children[0]` to see the actual error.
1340
+ console.warn(`prev is not an array, this is unexpected`, prev);
1341
+ debugger;
1342
+ prev = [];
1343
+ }
1344
+ for (let node of prev) {
1345
+ let key = getNodeKey(node);
1346
+ let nodes = sourceNodes.get(key);
1347
+ if (!nodes) sourceNodes.set(key, nodes = []);
1348
+ nodes.push(node);
1349
+ }
1350
+ for (let i = 0; i < next.length; i++) {
1351
+ let node = next[i];
1352
+ let key = getNodeKey(node);
1353
+ let match = sourceNodes.get(key)?.shift();
1354
+
1355
+ if (onTransformNode) {
1356
+ let inputNode = node;
1357
+ node = onTransformNode(match, node);
1358
+ if (node !== inputNode) {
1359
+ node = QRenderClass.normalizeVNode({ vNode: node });
1360
+ next[i] = node;
1361
+ }
1362
+ }
1363
+
1364
+ let result: DOMNode | undefined;
1365
+ // NOTE: At first components will return undefined, and everything will anchor to the same node.
1366
+ // But once components start having dom values, they will all stack up, and our order will make sense.
1367
+ if (match === undefined) {
1368
+ result = onNewNode(node, anchor, parent);
1369
+ if (result !== undefined) anchor = { prev: result };
1370
+ } else {
1371
+ result = onUpdatedNode(match, node, anchor, parent);
1372
+ if (result !== undefined) anchor = { prev: result };
1373
+ }
1374
+
1375
+ if (isVNode(node)) {
1376
+ if (typeof node.type === "string") {
1377
+ let children = node.props.children;
1378
+ let matchChildren = (match && isVNode(match) && match.props.children) ?? [];
1379
+ iterate(children as VirtualDOM[], matchChildren as VirtualDOM[], result);
1380
+ }
1381
+ }
1382
+ }
1383
+
1384
+ for (let nodes of sourceNodes.values()) {
1385
+ for (let node of nodes) {
1386
+ onRemovedNode(node);
1387
+ removeRecursively(node);
1388
+ function removeRecursively(node: VirtualDOM) {
1389
+ if (isVNode(node) && typeof node.type === "string") {
1390
+ let children = node.props.children as VirtualDOM[];
1391
+ for (let child of children) {
1392
+ onRemovedNode(child);
1393
+ removeRecursively(child);
1394
+ }
1395
+ }
1396
+ }
1397
+ }
1398
+ }
1399
+ }
1400
+ }
1401
+
1402
+ private disposing = false;
1403
+ public disposed = false;
1404
+ private dispose() {
1405
+ if (this.disposed) return;
1406
+ this.disposing = true;
1407
+ this.disposed = true;
1408
+
1409
+ componentRendered(this);
1410
+ qreact.allComponents.delete(this);
1411
+
1412
+ let disposers = disposeWatchers.get(this);
1413
+ if (disposers) {
1414
+ disposeWatchers.delete(this);
1415
+ for (let disposer of disposers) {
1416
+ try {
1417
+ disposer();
1418
+ } catch (e: any) {
1419
+ console.error(`Error in dispose callback for ${this.debugName}, ${disposer.name}, ${e.stack}`);
1420
+ }
1421
+ }
1422
+ }
1423
+
1424
+ let self = this;
1425
+ let instance = self.instance;
1426
+
1427
+ instance.componentWillUnmount && logErrors(proxyWatcher.commitFunction({
1428
+ debugName: `${this.debugName}.componentWillUnmount`,
1429
+ watchFunction() {
1430
+ instance.componentWillUnmount?.();
1431
+ },
1432
+ }));
1433
+
1434
+ // Recursively dispose all children
1435
+ for (let child of this.childComponents.values()) {
1436
+ QRenderClass.getInstance(child).dispose();
1437
+ }
1438
+
1439
+ this.renderWatcher?.dispose();
1440
+ this.VNodeWatcher.dispose();
1441
+ this.mountDOMWatcher.dispose();
1442
+ renderClasses.delete(this.id);
1443
+
1444
+ this.renderWatcher = undefined as any;
1445
+ this.VNodeWatcher = undefined as any;
1446
+ this.mountDOMWatcher = undefined as any;
1447
+ this.instance = undefined as any;
1448
+ this.domNodes = undefined as any;
1449
+ this.childComponents = undefined as any;
1450
+ this.vNode = undefined as any;
1451
+ this.prevMountedVNode = undefined as any;
1452
+ this.internalRootDOMNodes = undefined as any;
1453
+
1454
+ // Clean up all synced fields too
1455
+ logErrors(proxyWatcher.commitFunction({
1456
+ debugName: `${this.debugName}.dispose`,
1457
+ watchFunction() {
1458
+ let data = self.data();
1459
+ for (let key in Object.keys(data)) {
1460
+ delete (data as any)[key];
1461
+ }
1462
+ delete schema()[LOCAL_DOMAIN].qreact.components[self.id];
1463
+ },
1464
+ }));
1465
+ }
1466
+
1467
+ public static getInstance(id: number) {
1468
+ let instance = renderClasses.get(id);
1469
+ if (!instance) throw new Error(`Could not find component with id ${id}`);
1470
+ return instance;
1471
+ }
1472
+ public static getData(id: number) {
1473
+ return schema()[LOCAL_DOMAIN].qreact.components[id];
1474
+ }
1475
+ public static getInstanceAllowedUndefined(id: number) {
1476
+ return renderClasses.get(id);
1477
+ }
1478
+ public static getInstanceFromDOM(element: HTMLElement) {
1479
+ let id = "";
1480
+ while (!id && element) {
1481
+ id = element.getAttribute("data-qreact-id") || "";
1482
+ element = element.parentElement as HTMLElement || undefined;
1483
+ }
1484
+ return __INTERNAL__QRenderClass.getInstanceAllowedUndefined(Number(id));
1485
+ }
1486
+ public getParent() {
1487
+ let parentId = atomic(this.data().parentComponentId);
1488
+ if (parentId === undefined) return undefined;
1489
+ return QRenderClass.getInstance(parentId);
1490
+ }
1491
+
1492
+ public getVSCodeLink(): string | undefined {
1493
+ return getSourceVSCodeLink(this.internalRootDOMNodes?.[0]);
1494
+
1495
+ }
1496
+
1497
+ public getDebugComponentStack(): QRenderClass[] {
1498
+ let stack: QRenderClass[] = [];
1499
+ let cur: number | undefined = this.id;
1500
+ while (cur !== undefined) {
1501
+ let instance = QRenderClass.getInstanceAllowedUndefined(cur || -1);
1502
+ if (!instance) break;
1503
+ stack.push(instance);
1504
+ cur = instance.config.parentId;
1505
+ }
1506
+ return stack;
1507
+ }
1508
+ }
1509
+
1510
+ export const __INTERNAL__QRenderClass = QRenderClass;
1511
+
1512
+ let defaultErrorHandler: ErrorHandler = ({ error, debugName }) => {
1513
+ if (Querysub.isAllSynced()) {
1514
+ console.error(`Render error in ${debugName}`, error);
1515
+ // Throw, so we get a good callstack
1516
+ setImmediate(() => { throw error; });
1517
+ }
1518
+ return (
1519
+ createElement("div", {
1520
+ style: {
1521
+ background: "#fdd",
1522
+ color: `hsl(0, 0%, 7%)`,
1523
+ whiteSpace: "pre-wrap",
1524
+ padding: "5px 10px",
1525
+ }
1526
+ },
1527
+ canHaveChildren(error) ? String(error.stack) : String(error)
1528
+ )
1529
+ );
1530
+ };
1531
+ qreact.errorHandler = defaultErrorHandler;
1532
+
1533
+ export function isFragment(value: VirtualDOM): boolean {
1534
+ if (!isVNode(value)) return false;
1535
+ return (
1536
+ value.type === qreact.Fragment
1537
+ || (
1538
+ typeof value.type === "function"
1539
+ && value.type.constructor === Function
1540
+ && (
1541
+ value.type.toString() === "function d(n) { return n.children; }"
1542
+ // function v(e){return e.children}
1543
+ || /function .\(.\)\ ?{ ?return .\.children\ ?}/.test(value.type.toString())
1544
+ )
1545
+ )
1546
+ );
1547
+ }
1548
+
1549
+ let unsafeVirtualDOM = false;
1550
+ function temporaryUnsafeVirtualDOM<T>(fnc: () => T): T {
1551
+ let prev = unsafeVirtualDOM;
1552
+ unsafeVirtualDOM = true;
1553
+ try {
1554
+ return fnc();
1555
+ } finally {
1556
+ unsafeVirtualDOM = prev;
1557
+ }
1558
+ }
1559
+ function isVNode(vNode: VirtualDOM): vNode is VirtualDOMElement {
1560
+ if (
1561
+ !isValueProxy2(vNode) && typeof vNode === "object" && vNode && "type" in vNode
1562
+ || false
1563
+ ) {
1564
+ if (unsafeVirtualDOM) return true;
1565
+ if (vNode.allowedToRender === ALLOWED_TO_RENDER) {
1566
+ return true;
1567
+ }
1568
+ if (typeof vNode.type === "function") {
1569
+ return true;
1570
+ }
1571
+ if (vNode.constructor === undefined) {
1572
+ // preact.createElement isn't secure enough. It protects against JSON.stringify
1573
+ // XSS attacks, but... we don't use JSON.stringify, and we plan to preserve
1574
+ // the constructor, so it isn't a sufficient check for serialized values.
1575
+ console.warn(`preact/React VNode detected. This is not supported. Use qreact.createElement instead.`, vNode);
1576
+ return false;
1577
+ }
1578
+ console.warn(`Invalid vNode detected. If this node has been stored in the database (and you want to render it), use qreact.rehydrateVirtualDOM before rendering it. Otherwise, if a user stored an object in the database where a string/number was required but they stored an object, just ignore this warning. It means we successfully blocked an XSS attempt.`, vNode);
1579
+ return false;
1580
+ }
1581
+ return false;
1582
+
1583
+ }
1584
+ function isVNodeClass(vNode: VirtualDOM): vNode is preact.VNode<{ [key: string]: unknown }> {
1585
+ return isVNode(vNode) && typeof vNode.type === "function";
1586
+ }
1587
+
1588
+ // NOTE: We accept anything for vNode, otherwise we run into compatibility issues related to the type,
1589
+ // and we want to be able to support at least any preact version.
1590
+ function mountToDOM(vNode: any, parent: DOMNode) {
1591
+ class RootComponent extends qreact.Component {
1592
+ public render() {
1593
+ return vNode;
1594
+ }
1595
+ }
1596
+ new QRenderClass({ vNode: createElement(RootComponent) as any, props: {}, rawProps: {}, masterRootDOMNode: parent });
1597
+ }
1598
+
1599
+
1600
+ function createDOMNode(vNode: VirtualDOM, isSVG: boolean): DOMNode {
1601
+ if (isVNode(vNode)) {
1602
+ if (typeof vNode.type !== "string") {
1603
+ throw new Error(`Cannot create DOM node for component ${vNode.type?.name}`);
1604
+ }
1605
+ if (vNode.type === "svg" || isSVG) {
1606
+ return document.createElementNS("http://www.w3.org/2000/svg", vNode.type);
1607
+ }
1608
+ return document.createElement(vNode.type);
1609
+ }
1610
+ return document.createTextNode(String(vNode)) as any;
1611
+ }
1612
+
1613
+ type EventHandlerType = DOMNode & {
1614
+ _eventHandlers?: {
1615
+ [key: string]: {
1616
+ underlyingCallback: Function;
1617
+ ready: boolean;
1618
+ wrappedCallback: EventListener;
1619
+ }
1620
+ }
1621
+ };
1622
+
1623
+ // https://github.com/preactjs/preact/blob/main/src/constants.js#L15C1-L16C70
1624
+ export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
1625
+ function updateDOMNodeFields(domNode: DOMNode, vNode: VirtualDOM, prevVNode: VirtualDOM, owner: QRenderClass): void {
1626
+ if (vNode === prevVNode) return;
1627
+
1628
+ if (isVNode(vNode) && vNode.sourceModuleId) {
1629
+ let sourceModule = require.cache[vNode.sourceModuleId];
1630
+ if (sourceModule) {
1631
+ if (qreact.INJECT_FILENAMES) {
1632
+ domNode.setAttribute("data-qreact-filename", sourceModule.filename);
1633
+ } else {
1634
+ (domNode as any).sourceFileName = sourceModule.filename;
1635
+ }
1636
+ }
1637
+ }
1638
+ if (qreact.INJECT_LINE_NUMBERS && isVNode(vNode)) {
1639
+ (domNode as any).callStack = vNode.callStack;
1640
+ let sourceModule = vNode.sourceModuleId && require.cache[vNode.sourceModuleId];
1641
+ if (sourceModule && sourceModule.source) {
1642
+ (domNode as any).codeSource = sourceModule.source;
1643
+ }
1644
+ }
1645
+
1646
+ if (isVNode(vNode)) {
1647
+ function updateField(key: string) {
1648
+ if (key === "children") return;
1649
+ if (key === "ref") return;
1650
+ if (key === "ref2") return;
1651
+ let value = isVNode(vNode) ? vNode.props[key] : undefined;
1652
+ let prevValue = isVNode(prevVNode) ? prevVNode.props[key] : undefined;
1653
+ if (
1654
+ // NOTE: Object.is is required to handle NaN case
1655
+ Object.is(value, prevValue)
1656
+ // NOTE: Some values are SUPPOSED to be mutated outside of our framework, and so we always need
1657
+ // to update them in case they changed (ex, if the user set an invalid value and we want to revert
1658
+ // it back to the previous value, in which case `value === prevValue`, but domNode.value is different).
1659
+ && !(
1660
+ key === "value"
1661
+ || key === "checked"
1662
+ )
1663
+
1664
+ ) return;
1665
+
1666
+ if (key === "style") {
1667
+ if (typeof value === "string") {
1668
+ // This clobbers previous style object writes
1669
+ domNode.style.cssText = value;
1670
+ return;
1671
+ }
1672
+ if (canHaveChildren(value)) {
1673
+ if (typeof prevValue === "string") {
1674
+ // We have to clobber cssText
1675
+ domNode.style.cssText = "";
1676
+ }
1677
+ for (let [key, v] of Object.entries(value as Record<string, string | number | undefined>)) {
1678
+ if (typeof v === "number" && !IS_NON_DIMENSIONAL.test(key)) {
1679
+ v = v + "px";
1680
+ }
1681
+ (domNode as any).style[key] = v ?? "";
1682
+ }
1683
+ if (canHaveChildren(prevValue)) {
1684
+ for (let key in prevValue) {
1685
+ if (!(key in value)) {
1686
+ (domNode as any).style[key] = "";
1687
+ }
1688
+
1689
+ }
1690
+ }
1691
+ }
1692
+ if (!value) {
1693
+ domNode.style.cssText = "";
1694
+ }
1695
+ return;
1696
+ }
1697
+
1698
+ if (key.startsWith("on")) {
1699
+ let eventNode = domNode as EventHandlerType;
1700
+ eventNode._eventHandlers = eventNode._eventHandlers || {};
1701
+ let name = key.slice(2);
1702
+ // Guess if we can toLowerCase it by checking if there is a corresponding "on" field (for the full name)
1703
+ // Sometimes we can't, ex "DOMContentLoaded"
1704
+ if (key.toLowerCase() in domNode) {
1705
+ name = name.toLowerCase();
1706
+ }
1707
+ if (typeof value !== "function") {
1708
+ // Remove the callback entirely
1709
+ eventNode.removeEventListener(name, eventNode._eventHandlers[name].wrappedCallback);
1710
+ delete eventNode._eventHandlers[name];
1711
+ } else {
1712
+ let prevHandler = "";
1713
+ if (!eventNode._eventHandlers[name]) {
1714
+ eventNode._eventHandlers[name] = {
1715
+ underlyingCallback: value,
1716
+ ready: false,
1717
+ wrappedCallback: wrapEventCallback(`${owner.debugName}.eventHandlers.${key}`, name,
1718
+ function (this: any, ...args: any[]) {
1719
+ if (!eventNode._eventHandlers?.[name].ready && name === "click") {
1720
+ // UPDATE: ONLY for clicks, otherwise we break blur handlers.
1721
+ // IMPORTANT NOTE: IF you are inside of a click handler and add a handler to
1722
+ // the parent, the parent's handler will be triggered. An event might
1723
+ // trigger a re-render which reuses the parent node for something else,
1724
+ // causing an unrelated handler (which is for a div that is now positioned
1725
+ // in an entirely different UI location) to be triggerd.
1726
+ // - I observed this in chrome, with regular click handlers. BUT, even if the
1727
+ // all browsers paused immediate handlers for a bit... it is possible for us to add
1728
+ // a handler without "adding a handler", by setting the underlyingCallback.
1729
+ // So... we absolutely have to wait until the current handler has finished
1730
+ // before we start
1731
+ return;
1732
+ }
1733
+ let prevEvent = QRenderClass.eventComponentId;
1734
+ QRenderClass.eventComponentId = owner.id;
1735
+ try {
1736
+ eventNode._eventHandlers?.[name]?.underlyingCallback.apply(this, args as any);
1737
+ } finally {
1738
+ QRenderClass.eventComponentId = prevEvent;
1739
+ }
1740
+ }
1741
+ ),
1742
+ };
1743
+ eventNode.addEventListener(name, eventNode._eventHandlers[name].wrappedCallback);
1744
+ } else {
1745
+ prevHandler = eventNode._eventHandlers[name].underlyingCallback.toString();
1746
+ }
1747
+ eventNode._eventHandlers[name].underlyingCallback = value;
1748
+ // ONLY if the handler looks like it changes (and we have to use .toString, as it is almost certainly
1749
+ // a lambda, which will never ===), otherwise we run into issues related to call predictions... or something.
1750
+ if (prevHandler !== value.toString()) {
1751
+ eventNode._eventHandlers[name].ready = false;
1752
+ setTimeout(() => {
1753
+ let handler = eventNode._eventHandlers?.[name];
1754
+ if (handler) {
1755
+ handler.ready = true;
1756
+ }
1757
+ });
1758
+ }
1759
+ }
1760
+ return;
1761
+ }
1762
+
1763
+ let isSVG = domNode.namespaceURI === "http://www.w3.org/2000/svg";
1764
+ if (isSVG) {
1765
+ if (key === "className") {
1766
+ key = "class";
1767
+ }
1768
+ } else {
1769
+ // It's nice to allow "class" as a key name, as it is shorter than "className", and closer to HTML
1770
+ if (key === "class") {
1771
+ key = "className";
1772
+ }
1773
+ }
1774
+
1775
+ // TODO: Maybe add the extra logic from preact here
1776
+ // - ALTHOUGH, attributes aren't hugely important anyways. We do pretty much everything with className and events...
1777
+ // - https://github.com/preactjs/preact/blob/main/src/diff/props.js#L77
1778
+ if (key in domNode && !isSVG) {
1779
+ let nodeTyped = domNode as any;
1780
+ if (key === "title") {
1781
+ value = value ?? "";
1782
+ }
1783
+ if (key === "value" && domNode.tagName === "SELECT") {
1784
+ // HACK: We have to set select value later, because only values which are
1785
+ // equal to child options are allowed.
1786
+ void Promise.resolve().finally(() => {
1787
+ nodeTyped[key] = value;
1788
+ });
1789
+ } else {
1790
+ nodeTyped[key] = value;
1791
+ }
1792
+ } else {
1793
+ if (value === undefined || value === false) {
1794
+ domNode.removeAttribute(key);
1795
+ } else {
1796
+ domNode.setAttribute(key, String(value ?? ""));
1797
+ }
1798
+ }
1799
+ }
1800
+ // IMPORTANT! Remove first, so keys that map to the same value ("class" and "className" => "className"),
1801
+ // don't apply the removal of a "class" property to clobber a valid "className" value.
1802
+ if (isVNode(prevVNode)) {
1803
+ for (let key in prevVNode.props) {
1804
+ if (key in vNode.props) continue;
1805
+ updateField(key);
1806
+ }
1807
+ }
1808
+ for (let key in vNode.props) {
1809
+ updateField(key);
1810
+ }
1811
+ } else {
1812
+ // NOTE: I'm not sure if updating textContent is better or worse than creating a new Text node?
1813
+ // As in, should we use `prevNode.replaceWith(document.createTextNode(result))`?
1814
+ (domNode as any as Text).textContent = String(vNode);
1815
+ }
1816
+ }
1817
+
1818
+ function hasEventHandlers(node: DOMNode, type: string): boolean {
1819
+ let eventNode = node as EventHandlerType;
1820
+ return eventNode._eventHandlers?.[type] !== undefined;
1821
+ }
1822
+
1823
+ function wrapEventCallback(debugName: string, eventName: string, baseFunction: Function) {
1824
+ return function wrappedCallback(this: any, ...args: any[]) {
1825
+ let event = args[0];
1826
+ if (event && typeof event === "object" && event instanceof Event && event.currentTarget) {
1827
+ let currentTarget = event.currentTarget as HTMLInputElement;
1828
+ let value = currentTarget.value ?? "";
1829
+ let checked = currentTarget.checked ?? false;
1830
+
1831
+ let currentTargetTyped = currentTarget as any as { [key: string]: unknown };
1832
+
1833
+ let proxy = createPathValueProxy({
1834
+ getCallback(pathStr) {
1835
+ if (pathStr === getPathStr1("value")) {
1836
+ return { value };
1837
+ }
1838
+ if (pathStr === getPathStr1("checked")) {
1839
+ return { value: checked };
1840
+ }
1841
+ if (pathStr === getPathStr1("valueAsNumber")) {
1842
+ console.warn(yellow("Avoid using valueAsNumber, which conflicts with InputLabel display rounding (showing 3.0000000000000001 as 3). Instead us `Number(value)` or `+value`."));
1843
+ }
1844
+ let targetValue = currentTargetTyped[getPathIndexAssert(pathStr, 0)];
1845
+ if (typeof targetValue === "function") {
1846
+ targetValue = targetValue.bind(currentTarget);
1847
+ }
1848
+ return { value: targetValue };
1849
+ },
1850
+ setCallback(pathStr, newValue) {
1851
+ let key = getPathIndexAssert(pathStr, 0);
1852
+ if (key === "value") {
1853
+ value = newValue as any;
1854
+ } else if (key === "checked") {
1855
+ checked = newValue as any;
1856
+ }
1857
+ currentTargetTyped[key] = newValue;
1858
+ },
1859
+ getSymbol(pathStr, symbol) {
1860
+ if (symbol === getProxyBase) {
1861
+ return { value: currentTargetTyped };
1862
+ }
1863
+ return { value: currentTargetTyped[symbol as any] };
1864
+ },
1865
+ getKeys(pathStr) {
1866
+ return Object.keys(currentTarget);
1867
+ },
1868
+ });
1869
+ event = cloneEvent(event);
1870
+ event.currentTarget = proxy;
1871
+ args[0] = event;
1872
+ }
1873
+
1874
+ let run = () => {
1875
+ let result = baseFunction.apply(this, args);
1876
+ if (eventName === "mousedown") {
1877
+ blurFixOnMouseDownHack(event as MouseEvent);
1878
+ }
1879
+ return result;
1880
+ };
1881
+ if (proxyWatcher.inWatcher()) {
1882
+ if (proxyWatcher.getTriggeredWatcher().options.noLocks) {
1883
+ // A synchronous trigger that happens when we rearrange DOM. This triggers both
1884
+ // onBlur, and onChange. Because we are rearranging the DOM we shouldn't have any locks,
1885
+ // so we have to disconnect the event.
1886
+ void Promise.resolve().finally(() => {
1887
+ wrappedCallback.apply(this, args);
1888
+ });
1889
+ return;
1890
+ }
1891
+ return run();
1892
+ }
1893
+
1894
+ // TODO: Expose errors to the component in some way?
1895
+ // - Maybe via a global "componentError" callback, which is subscribed in their main.
1896
+ logErrors(proxyWatcher.commitFunction({
1897
+ debugName,
1898
+ watchFunction: run,
1899
+ synchronousInit: true,
1900
+ }));
1901
+ };
1902
+ }
1903
+
1904
+ function cloneEvent<T extends Event>(event: T): T {
1905
+ // Need to get every key, to preserve currentTarget correctly
1906
+ let keys = new Set<(keyof Event)>();
1907
+ let proto = event as any;
1908
+ while (proto && proto !== Object.prototype) {
1909
+ proto = Object.getPrototypeOf(proto);
1910
+ for (let key of Object.getOwnPropertyNames(proto)) {
1911
+ keys.add(key as any);
1912
+ }
1913
+ }
1914
+ let cloned = {} as any;
1915
+ for (let key of keys) {
1916
+ let value = event[key];
1917
+ if (typeof value === "function") {
1918
+ cloned[key] = (...args: unknown[]) => (event[key] as any)(...args);
1919
+ } else {
1920
+ cloned[key] = event[key];
1921
+ }
1922
+ }
1923
+ return cloned;
1924
+ }
1925
+
1926
+ const getProxyBase = Symbol.for("getProxyBase");
1927
+ function getCurrentTarget<T extends EventTarget>(target: T): T {
1928
+ return (target as any)[getProxyBase];
1929
+ };
1930
+
1931
+ function isTarget(event: preact.JSX.TargetedEvent): boolean {
1932
+ return event.target === getCurrentTarget(event.currentTarget);
1933
+ };
1934
+
1935
+ function getDebugContextComponent() {
1936
+ let componentId = getCurrentComponentId();
1937
+ if (!componentId) return undefined;
1938
+
1939
+
1940
+ let name = QRenderClass.getInstance(componentId).debugName;
1941
+
1942
+ let stack: string[] = [];
1943
+ let curId: number | undefined = componentId;
1944
+ while (curId) {
1945
+ let instance = QRenderClass.getInstance(curId);
1946
+ stack.push(instance.debugName);
1947
+ curId = instance.config.parentId;
1948
+ }
1949
+ stack.reverse();
1950
+ return { name, stack };
1951
+ }
1952
+ function getDebugContextEvent() {
1953
+ let rawReason = Querysub.getTriggerReason();
1954
+ return {
1955
+ currentFunction: proxyWatcher.getTriggeredWatcher().debugName,
1956
+ sources: Array.from(rawReason?.pathSources || []).map(x => x.source),
1957
+ rawReason,
1958
+ };
1959
+ }
1960
+ function getDebugContext() {
1961
+ return { ...getDebugContextComponent(), ...getDebugContextEvent() };
1962
+ }
1963
+
1964
+ /** The built in element.contains is bad and throws bad and pointless errors. This code is what it should be. */
1965
+ function contains(parent: HTMLElement, child: HTMLElement): boolean {
1966
+ let cur = child as HTMLElement | null;
1967
+ while (cur) {
1968
+ if (cur === parent) return true;
1969
+ cur = cur.parentElement;
1970
+ }
1971
+ return false;
1972
+ }
1973
+
1974
+ /** Blur fix.
1975
+ * - By default, blur happens after a delay from mousedown. However, selection (such as dragging to select
1976
+ * elements) often happen on mousedown, which can trigger a re-render.
1977
+ * This results in a situation where an input might be re-used for a different component, BEFORE we blur. This causes
1978
+ * the wrong component to blur. To fix this, we force blur to happen early, by trying to guess if we are going to blur.
1979
+ * - It should still happen AFTER the mousedown, just directly after, so that we don't have a change
1980
+ * to re-render and change the blur target.
1981
+ */
1982
+ function blurFixOnMouseDownHack(event: MouseEvent) {
1983
+ let selected = document.activeElement;
1984
+ if (selected === document.body || !selected) return;
1985
+ if (!(selected instanceof HTMLElement)) return;
1986
+ let clicked = event.currentTarget as HTMLElement | null;
1987
+ if (!clicked) return;
1988
+ if (selected === clicked) return;
1989
+
1990
+ let labelRoot = clicked.closest("label");
1991
+ if (labelRoot) {
1992
+ // If we clicked on a label, and the label is for the selected element, then focus won't change
1993
+ if (labelRoot === selected || contains(labelRoot, selected)) {
1994
+ return;
1995
+ }
1996
+ }
1997
+
1998
+ // If we clicked on a child of selected, then focus won't change
1999
+ if (contains(selected, clicked)) return;
2000
+
2001
+ // Looks like we are going to blur, so blur now
2002
+ if (selected instanceof HTMLElement && !selected.hasAttribute("data-no-early-blur")) {
2003
+ console.info(`Simulating early blur to prevent unblurred inputs from existing after mousedown. You can use data-no-early-blur to opt-out of this feature`, selected);
2004
+ selected.blur();
2005
+ }
2006
+ }
2007
+
2008
+ onHotReload(() => {
2009
+ triggerRerenderAll();
2010
+ });
2011
+ export function triggerRerenderAll() {
2012
+ Querysub.commit(() => {
2013
+ schema()[LOCAL_DOMAIN].hotreloadSeqNum++;
2014
+ });
2015
+ }
2016
+
2017
+ // #region Go to source
2018
+
2019
+ export function getSourceInfo(element: DOMNode | undefined) {
2020
+ let sourceFileName = (element as any)?.sourceFileName;
2021
+ let callStack = (element as any)?.callStack;
2022
+ let codeSource = (element as any)?.codeSource;
2023
+ if (!sourceFileName) return undefined;
2024
+ let callerInfo = getCallerInfo({
2025
+ sourceCode: codeSource,
2026
+ callStack: callStack,
2027
+ });
2028
+ return {
2029
+ sourceFileName: sourceFileName,
2030
+ lineNumber: callerInfo?.lineNumber,
2031
+ columnNumber: callerInfo?.columnNumber,
2032
+ };
2033
+ }
2034
+ export function getSourceVSCodeLink(element: DOMNode | undefined) {
2035
+ let sourceInfo = getSourceInfo(element);
2036
+ if (!sourceInfo?.sourceFileName) return undefined;
2037
+ /*
2038
+ Claude.ai says there are more parameters we can use. As it was right about selection, these might work too.
2039
+ selection: Specifies the line and column to focus on when opening the file. Format: "line,column".
2040
+ readonly: When set to true, opens the file in read-only mode.
2041
+ viewColumn: Determines which editor group to open the file in. Values can be 1, 2, or 3, corresponding to the left, center, and right groups respectively.
2042
+ files: Allows opening multiple files at once. Provide a comma-separated list of file paths relative to the workspace root.
2043
+ folder: Opens a specific folder in the workspace.
2044
+ newWindow: When set to true, opens the file or folder in a new window instead of the current one.
2045
+
2046
+ Additional notes:
2047
+
2048
+ The diff scheme: VS Code also supports a vscode://file/file1/file2 URL format for opening the diff view between two files.
2049
+ Command palette: You can trigger VS Code commands using vscode://file/{file}?command={commandId}.
2050
+ Extensions: Some VS Code extensions may add support for additional URL parameters.
2051
+ */
2052
+ let path = `vscode://file/${sourceInfo.sourceFileName}`;
2053
+ if (sourceInfo.lineNumber) {
2054
+ path += `:${sourceInfo.lineNumber}:${sourceInfo.columnNumber}`;
2055
+ path += `?selection=${sourceInfo.lineNumber},${sourceInfo.columnNumber}`;
2056
+ }
2057
+ return path;
2058
+ }
2059
+
2060
+ interface DecodedMapping {
2061
+ generatedLine: number;
2062
+ generatedColumn: number;
2063
+ sourceIndex: number;
2064
+ sourceLine: number;
2065
+ sourceColumn: number;
2066
+ nameIndex?: number;
2067
+ }
2068
+
2069
+ function decodeVLQ(str: string): number[] {
2070
+ const vlqMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2071
+ const decoded: number[] = [];
2072
+ let shift = 0;
2073
+ let value = 0;
2074
+
2075
+ for (let i = 0; i < str.length; i++) {
2076
+ const integer = vlqMap.indexOf(str[i]);
2077
+ value += (integer & 31) << shift;
2078
+ if (integer & 32) {
2079
+ shift += 5;
2080
+ } else {
2081
+ const shouldNegate = value & 1;
2082
+ value >>>= 1;
2083
+ decoded.push(shouldNegate ? -value : value);
2084
+ shift = 0;
2085
+ value = 0;
2086
+ }
2087
+ }
2088
+
2089
+ return decoded;
2090
+ }
2091
+
2092
+ function decodeSourceMapString(mappings: string): DecodedMapping[] {
2093
+ const lines = mappings.split(";");
2094
+ const decodedMappings: DecodedMapping[] = [];
2095
+ let generatedLine = 0;
2096
+ let generatedColumn = 0;
2097
+ let sourceIndex = 0;
2098
+ let sourceLine = 0;
2099
+ let sourceColumn = 0;
2100
+ let nameIndex = 0;
2101
+
2102
+ for (const line of lines) {
2103
+ generatedColumn = 0;
2104
+ const segments = line.split(",");
2105
+
2106
+ for (const segment of segments) {
2107
+ if (segment.length === 0) continue;
2108
+
2109
+ const decoded = decodeVLQ(segment);
2110
+ generatedColumn += decoded[0];
2111
+
2112
+ const mapping: DecodedMapping = {
2113
+ generatedLine,
2114
+ generatedColumn,
2115
+ sourceIndex,
2116
+ sourceLine,
2117
+ sourceColumn,
2118
+ };
2119
+
2120
+ if (decoded.length > 1) {
2121
+ sourceIndex += decoded[1];
2122
+ sourceLine += decoded[2];
2123
+ sourceColumn += decoded[3];
2124
+ mapping.sourceIndex = sourceIndex;
2125
+ mapping.sourceLine = sourceLine;
2126
+ mapping.sourceColumn = sourceColumn;
2127
+ }
2128
+
2129
+ if (decoded.length > 4) {
2130
+ nameIndex += decoded[4];
2131
+ mapping.nameIndex = nameIndex;
2132
+ }
2133
+
2134
+ decodedMappings.push(mapping);
2135
+ }
2136
+
2137
+ generatedLine++;
2138
+ }
2139
+
2140
+ return decodedMappings;
2141
+ }
2142
+
2143
+
2144
+ function findMappedPosition(sourceMap: SourceMap, position: {
2145
+ line: number;
2146
+ column: number;
2147
+ }): {
2148
+ line: number;
2149
+ column: number;
2150
+ } | null {
2151
+ const decodedMappings = decodeSourceMapString(sourceMap.mappings);
2152
+ let indexAfter = decodedMappings.findIndex(
2153
+ ({ generatedLine, generatedColumn }) =>
2154
+ generatedLine > position.line || (generatedLine === position.line && generatedColumn >= position.column)
2155
+ );
2156
+ if (indexAfter === -1) {
2157
+ indexAfter = decodedMappings.length;
2158
+ }
2159
+ const mapping = decodedMappings[indexAfter - 1];
2160
+ if (!mapping) {
2161
+ return null;
2162
+ }
2163
+ return {
2164
+ line: mapping.sourceLine,
2165
+ column: mapping.sourceColumn,
2166
+ };
2167
+ }
2168
+
2169
+
2170
+ interface SourceMap {
2171
+ version: number;
2172
+ file?: string;
2173
+ sourceRoot?: string;
2174
+ sources: string[];
2175
+ names: string[];
2176
+ mappings: string;
2177
+ sourcesContent?: string[];
2178
+ }
2179
+ function parseSourceMap(code: string): SourceMap | null {
2180
+ const sourceMappingURL = "//# sourceMappingURL=data:application/json;base64,";
2181
+ const index = code.lastIndexOf(sourceMappingURL);
2182
+
2183
+ if (index === -1) {
2184
+ return null;
2185
+ }
2186
+ let endIndex = code.indexOf("\n", index);
2187
+
2188
+ const base64Start = index + sourceMappingURL.length;
2189
+ const base64Data = code.slice(base64Start, endIndex).trim();
2190
+
2191
+ try {
2192
+ const sourceMapData = Buffer.from(base64Data, "base64").toString("utf-8");
2193
+ return JSON.parse(sourceMapData) as SourceMap;
2194
+ } catch (error) {
2195
+ console.error("Failed to decode or parse source map:", error);
2196
+ return null;
2197
+ }
2198
+ }
2199
+
2200
+ function getCallerInfo(config: {
2201
+ sourceCode?: string;
2202
+ callStack?: string;
2203
+ }): {
2204
+ columnNumber: number;
2205
+ lineNumber: number;
2206
+ } | undefined {
2207
+ let stack = config.callStack;
2208
+ if (!stack) return undefined;
2209
+ let lines = stack.split("\n");
2210
+ let line = lines[3];
2211
+ let match = line.match(/(?::(\d+):(\d+))(?!.*:\d+:\d+)/);
2212
+ if (!match) return { columnNumber: 0, lineNumber: 0 };
2213
+ // Apply the sourcemap, if it exists
2214
+ let lineNumber = Number(match[1]);
2215
+ let columnNumber = Number(match[2]);
2216
+ if (!lineNumber || !columnNumber) return undefined;
2217
+ let sourceCode = config.sourceCode;
2218
+ let sourceMap = parseSourceMap(sourceCode || "");
2219
+ if (sourceMap) {
2220
+ let position = findMappedPosition(sourceMap, { line: lineNumber - 1, column: columnNumber - 1 });
2221
+ if (position) {
2222
+ lineNumber = position.line + 1;
2223
+ columnNumber = position.column + 1;
2224
+ }
2225
+ }
2226
+ return { columnNumber, lineNumber };
2227
+ }
2228
+
2229
+ // #endregion
2230
+
2231
+
2232
+ // #region Unsynced
2233
+
2234
+ let unsyncedComponents = new Set<QRenderClass>();
2235
+ let triggeringUnsynced = false;
2236
+ function triggerUnsyncedNow() {
2237
+ if (triggeringUnsynced) return;
2238
+ triggeringUnsynced = true;
2239
+ setImmediate(() => {
2240
+ triggeringUnsynced = false;
2241
+ Querysub.commit(() => {
2242
+ schema()[LOCAL_DOMAIN].unsyncedSeqNum++;
2243
+ });
2244
+ });
2245
+ }
2246
+ function watchUnsyncedComponents(): Set<ExternalRenderClass> {
2247
+ schema()[LOCAL_DOMAIN].unsyncedSeqNum;
2248
+ return unsyncedComponents;
2249
+ }
2250
+ function componentRendered(component: QRenderClass) {
2251
+ if (component.disposed || Querysub.isAllSynced()) {
2252
+ if (unsyncedComponents.has(component)) {
2253
+ unsyncedComponents.delete(component);
2254
+ triggerUnsyncedNow();
2255
+ }
2256
+ } else {
2257
+ // NOTE: By only checking for has it means if the synced path count changes
2258
+ // we won't trigger. This is fine, presently the indicator only uses the count, and
2259
+ // we don't want to spend extra time triggering when we are clearly already lagging
2260
+ // (because we have unsynced components).
2261
+ if (!unsyncedComponents.has(component)) {
2262
+ unsyncedComponents.add(component);
2263
+ triggerUnsyncedNow();
2264
+ }
2265
+ }
2266
+ }
2267
+
2268
+ // #endregion
2269
+
2270
+
2271
+ // #region Stats
2272
+ let pendingStats = new Map<string, {
2273
+ count: number;
2274
+ sum: number;
2275
+ }>();
2276
+ function registerComponentStats(type: string, time: number) {
2277
+ if (!qreact.DEBUG_TIME) return;
2278
+ let entry = pendingStats.get(type);
2279
+ if (!entry) {
2280
+ pendingStats.set(type, entry = { count: 0, sum: 0 });
2281
+ }
2282
+ entry.count++;
2283
+ entry.sum += time;
2284
+ }
2285
+
2286
+ if (isClient()) {
2287
+ setImmediate(() => {
2288
+ let loopStart = 0;
2289
+ clientWatcher.onLoopStart(() => {
2290
+ if (!qreact.DEBUG_TIME) return;
2291
+ loopStart = Date.now();
2292
+ });
2293
+ clientWatcher.onLoopEnd(info => {
2294
+ if (!qreact.DEBUG_TIME) return;
2295
+ let stats = Array.from(pendingStats.entries());
2296
+ if (stats.length === 0) return;
2297
+ sort(stats, x => -x[1].sum);
2298
+
2299
+ let totalTime = stats.reduce((total, [type, { sum }]) => total + sum, 0);
2300
+
2301
+ if (totalTime > qreact.DEBUG_TIME) {
2302
+
2303
+ let remaining = stats.splice(10, stats.length);
2304
+ let textParts: string[] = [];
2305
+ let loopTime = Date.now() - loopStart;
2306
+ textParts.push(`Render in ${blue(formatTime(totalTime))} (${green(formatPercent(totalTime / loopTime))} of ${formatTime(loopTime)} loop time)`);
2307
+ for (let stat of stats) {
2308
+ let [type, { count, sum }] = stat;
2309
+ textParts.push(`${type}(${count}) ${blue(formatTime(sum))} // ${green(formatPercent(sum / totalTime))}`);
2310
+ }
2311
+ let remainingSum = remaining.reduce((total, [type, { sum }]) => total + sum, 0);
2312
+ textParts.push(`Remaining ${blue(formatTime(remainingSum))} // ${green(formatPercent(remainingSum / totalTime))}`);
2313
+
2314
+ console.log(textParts.join(" | "));
2315
+ }
2316
+
2317
+ pendingStats.clear();
2318
+ });
2319
+ });
2320
+ }
2321
+
2322
+ // #endregion