querysub 0.309.0 → 0.311.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/spec.txt +26 -3
- package/src/1-path-client/pathValueClientWatcher.ts +4 -2
- package/src/2-proxy/schema2.ts +222 -6
- package/src/4-querysub/Querysub.ts +11 -6
- package/src/5-diagnostics/Modal.tsx +5 -2
- package/src/5-diagnostics/memoryValueAudit.ts +46 -48
- package/src/deployManager/components/ServiceDetailPage.tsx +11 -151
- package/src/deployManager/machineApplyMainCode.ts +3 -3
- package/src/diagnostics/logs/diskLogger.ts +1 -1
- package/src/library-components/Button.tsx +1 -0
- package/src/library-components/TypedConfigEditor.tsx +205 -0
- package/src/niceStringify.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.311.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"js-sha512": "^0.9.0",
|
|
23
23
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
24
24
|
"pako": "^2.1.0",
|
|
25
|
-
"socket-function": "^0.
|
|
25
|
+
"socket-function": "^0.136.0",
|
|
26
26
|
"terser": "^5.31.0",
|
|
27
27
|
"typesafecss": "^0.22.0",
|
|
28
28
|
"yaml": "^2.5.0",
|
package/spec.txt
CHANGED
|
@@ -52,6 +52,26 @@ More corruption resistance files
|
|
|
52
52
|
- and... we might as well add support for short, int, float, and uint (uint is a good way to store a guid, via storing 8 uint variables).
|
|
53
53
|
|
|
54
54
|
Schema/binary PathValues accesses
|
|
55
|
+
0) First our stream/schema wrappers. Which at first just convert to raw path reads/writes, but once we add schemas they can efficiently create/use those
|
|
56
|
+
1) PathValue.path (string[]), need to be converted to an opaque type `Path`
|
|
57
|
+
- Helpers to manipulate it, and go back to string[], string, etc.
|
|
58
|
+
2) Have Path be able to store itself in schema + variables mode
|
|
59
|
+
- Where schema is shared across all Path instances
|
|
60
|
+
{ schema: { path: (string|Variable)[] }; variables: (string | number)[]; }
|
|
61
|
+
- Add functions to access the schema and variable values
|
|
62
|
+
3) Create an efficient `Map<Path, T>`, via using schemas from the paths
|
|
63
|
+
- By requiring Path as the input we can directly use the schema to narrow down the cases, and then within that we just have to lookup values by variables
|
|
64
|
+
4) Use Path everywhere, replacing PathValue.path, using our efficient lookup to manage it.
|
|
65
|
+
- I think a lookup should handle all the cases. We should be able to nest them as well?
|
|
66
|
+
5) This SHOULD let us entirely get rid of path joining, which should be SO much faster.
|
|
67
|
+
6) Update serialization to keep schemas, instead of converting them back to paths. This should be more efficient, and a lot smaller.
|
|
68
|
+
- Values serialization won't change, and we still need to encode the variables, but... it should still be a lot faster.
|
|
69
|
+
7) Try to remove as many Path conversions to string (and also to a lesser degree string[]), as possible, by making them schema aware.
|
|
70
|
+
8) Investigate further optimizations
|
|
71
|
+
- Replacing variables with numbers, so we our internals Maps can be replaced with arrays / typed arrays
|
|
72
|
+
|
|
73
|
+
IMPORTANT! Actually... a lot of the following is wrong. We should have this PER atomic value, and perhaps ONLY for paths!
|
|
74
|
+
|
|
55
75
|
Base code
|
|
56
76
|
reader
|
|
57
77
|
let viewTime = 0;
|
|
@@ -67,9 +87,11 @@ Schema/binary PathValues accesses
|
|
|
67
87
|
Schema optimization
|
|
68
88
|
writer
|
|
69
89
|
let changeStream = new SchemaPath(() => global().users[wildcard].viewTime);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
write(() => {
|
|
91
|
+
for (let [video, viewTime] of viewedVideos) {
|
|
92
|
+
changeStream.write(userId, viewTime);
|
|
93
|
+
}
|
|
94
|
+
})
|
|
73
95
|
reader
|
|
74
96
|
let schemaWatcher = new SchemaPath(() => global().users[wildcard].viewTime);
|
|
75
97
|
watch(() => {
|
|
@@ -108,6 +130,7 @@ Schema/binary PathValues accesses
|
|
|
108
130
|
- We also need to support partial key watching. Often we will watch a few keys (and then within them, maybe all keys at another level).
|
|
109
131
|
|
|
110
132
|
1) START by supporting write streams (but NOT read schemas), as this allows us to define our schemas.
|
|
133
|
+
IMPORTANT! Actually... a lot of the following is wrong. We should have this PER atomic value, and perhaps ONLY for paths!
|
|
111
134
|
- This will give us a big chunk, which we will pass around (even passing around arrays of chunks).
|
|
112
135
|
- The core will break this apart somewhat, with an object per schema, and then a tree of maps for the dynamicValues inside of it (and global lookups for the locks and values)
|
|
113
136
|
- We will never create this from PathValues, instead, we will append values to specific schemas as we build it
|
|
@@ -87,7 +87,8 @@ export class ClientWatcher {
|
|
|
87
87
|
|
|
88
88
|
/** How long we keep watching, despite a value no longer being needed. */
|
|
89
89
|
public static WATCH_STICK_TIME = MAX_CHANGE_AGE * 2;
|
|
90
|
-
public static
|
|
90
|
+
public static MAX_TRIGGER_TIME_BROWSER = 1000 * 5;
|
|
91
|
+
public static MAX_TRIGGER_TIME_NODEJS = 1000 * 300;
|
|
91
92
|
|
|
92
93
|
|
|
93
94
|
private valueFunctionWatchers = registerResource("paths|valueFunctionWatchers", new Map<string, Map<WatchSpec["callback"], WatchSpec>>());
|
|
@@ -270,6 +271,7 @@ export class ClientWatcher {
|
|
|
270
271
|
logErrors(e);
|
|
271
272
|
}
|
|
272
273
|
}
|
|
274
|
+
let MAX_TRIGGER_TIME = isNode() ? ClientWatcher.MAX_TRIGGER_TIME_NODEJS : ClientWatcher.MAX_TRIGGER_TIME_BROWSER;
|
|
273
275
|
|
|
274
276
|
let time = Date.now();
|
|
275
277
|
while (true) {
|
|
@@ -374,7 +376,7 @@ export class ClientWatcher {
|
|
|
374
376
|
this.activeWatchSpec = undefined;
|
|
375
377
|
}
|
|
376
378
|
if (
|
|
377
|
-
Date.now() - time >
|
|
379
|
+
Date.now() - time > MAX_TRIGGER_TIME
|
|
378
380
|
&& !isDevDebugbreak()
|
|
379
381
|
) {
|
|
380
382
|
stoppedEarly = true;
|
package/src/2-proxy/schema2.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cache } from "socket-function/src/caching";
|
|
2
|
-
import { nextId, recursiveFreeze } from "socket-function/src/misc";
|
|
2
|
+
import { nextId, recursiveFreeze, sort } from "socket-function/src/misc";
|
|
3
3
|
import { canHaveChildren } from "socket-function/src/types";
|
|
4
4
|
import { isDefined } from "../misc";
|
|
5
5
|
|
|
@@ -109,9 +109,8 @@ type TypeDefType<T = never, Optional = false> = {
|
|
|
109
109
|
* the resulting fields, or check a specific field, etc).
|
|
110
110
|
*/
|
|
111
111
|
atomicOnlyOnWrite<NewT>(defaultValue?: T | NewT): TypeDefType<T | NewT>;
|
|
112
|
-
/** Lookups
|
|
113
|
-
*
|
|
114
|
-
* if there are nested non-atomic values in the lookup.
|
|
112
|
+
/** Lookups only return undefined if the entry is a single atomic value. However, their keys work as expected, so you can us "in" to check for keys, delete to remove them an Object.keys() to get the values
|
|
113
|
+
* - undefined is not supported as a value, and will return false for "in" and Object.keys (deleting is mapped to setting to undefined).
|
|
115
114
|
*
|
|
116
115
|
* NOTE: There is no array. We don't support synchronized arrays. Use an object instead,
|
|
117
116
|
* even using numbers as keys if you want (they'll be casted to strings, but at least
|
|
@@ -244,8 +243,207 @@ export class Schema2Fncs {
|
|
|
244
243
|
};
|
|
245
244
|
}).filter(isDefined);
|
|
246
245
|
}
|
|
246
|
+
public static schema2ToTypeString(schema: Schema2): string {
|
|
247
|
+
return schema2ToTypeString(schema);
|
|
248
|
+
}
|
|
247
249
|
}
|
|
248
250
|
|
|
251
|
+
/*
|
|
252
|
+
todonext
|
|
253
|
+
- Fix t.lookup types
|
|
254
|
+
*/
|
|
255
|
+
|
|
256
|
+
// #region schema2ToTypeString
|
|
257
|
+
/*
|
|
258
|
+
{
|
|
259
|
+
x: t.number,
|
|
260
|
+
}
|
|
261
|
+
=>
|
|
262
|
+
`{
|
|
263
|
+
x: number;
|
|
264
|
+
}`
|
|
265
|
+
*/
|
|
266
|
+
export function schema2ToTypeString(schema: Schema2): string {
|
|
267
|
+
type TypePathValue = {
|
|
268
|
+
path: string[];
|
|
269
|
+
type: string;
|
|
270
|
+
isOptional: boolean;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Step 1: Transform defs to type path values (direct transformation)
|
|
274
|
+
let type = typeDefCached(schema);
|
|
275
|
+
let typePathValues: TypePathValue[] = [];
|
|
276
|
+
|
|
277
|
+
//return JSON.stringify(type.defs, null, 4);
|
|
278
|
+
|
|
279
|
+
for (let def of type.defs) {
|
|
280
|
+
// Convert path from SchemaPath to string[], replacing WildCard with actual string representation
|
|
281
|
+
let stringPath = def.path.map(p => p === WildCard ? "*" : p as string);
|
|
282
|
+
|
|
283
|
+
// Determine the type based on the def properties
|
|
284
|
+
let typeStr = "unknown";
|
|
285
|
+
if (def.isNotAtomic === false || def.defaultValue !== undefined) {
|
|
286
|
+
// This is a primitive type, determine which one based on default value
|
|
287
|
+
if (def.isNumber) {
|
|
288
|
+
typeStr = "number";
|
|
289
|
+
} else if (def.isString) {
|
|
290
|
+
typeStr = "string";
|
|
291
|
+
} else if (def.isBoolean) {
|
|
292
|
+
typeStr = "boolean";
|
|
293
|
+
} else if (def.isNull) {
|
|
294
|
+
typeStr = "null";
|
|
295
|
+
} else {
|
|
296
|
+
// For atomic types with complex default values, inspect the type
|
|
297
|
+
let defaultResult = defaultToType(def.defaultValue);
|
|
298
|
+
if (typeof defaultResult === "string") {
|
|
299
|
+
typeStr = defaultResult;
|
|
300
|
+
} else {
|
|
301
|
+
// It's an array of nested types, add them to our list with prepended paths
|
|
302
|
+
typeStr = "object"; // Will be clobbered by nested types
|
|
303
|
+
for (let nestedType of defaultResult) {
|
|
304
|
+
typePathValues.push({
|
|
305
|
+
path: [...stringPath, ...nestedType.path],
|
|
306
|
+
type: nestedType.type,
|
|
307
|
+
isOptional: nestedType.isOptional
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
typePathValues.push({
|
|
315
|
+
path: stringPath,
|
|
316
|
+
type: typeStr,
|
|
317
|
+
isOptional: !!def.isMaybeUndefined || !def.isNotAtomic && def.defaultValue === undefined
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
// Shortest first, so they get clobbered properly.
|
|
321
|
+
sort(typePathValues, x => x.path.length);
|
|
322
|
+
|
|
323
|
+
// Step 2: Build object from type path values
|
|
324
|
+
type TypeStructure = string | { [key: string]: TypeStructure };
|
|
325
|
+
|
|
326
|
+
function buildObjectFromTypePathValues(values: TypePathValue[]): TypeStructure {
|
|
327
|
+
let result: { [key: string]: TypeStructure } = {};
|
|
328
|
+
|
|
329
|
+
for (let value of values) {
|
|
330
|
+
let current: { [key: string]: TypeStructure } = result;
|
|
331
|
+
let path = value.path;
|
|
332
|
+
|
|
333
|
+
// Navigate/create the nested structure
|
|
334
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
335
|
+
let key = path[i];
|
|
336
|
+
if (!(key in current)) {
|
|
337
|
+
current[key] = {};
|
|
338
|
+
} else if (typeof current[key] === "string") {
|
|
339
|
+
// If there's a conflict (existing string type), convert to object
|
|
340
|
+
current[key] = {};
|
|
341
|
+
}
|
|
342
|
+
current = current[key] as { [key: string]: TypeStructure };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Set the final value
|
|
346
|
+
if (path.length > 0) {
|
|
347
|
+
let finalKey = path[path.length - 1];
|
|
348
|
+
if (value.isOptional) {
|
|
349
|
+
finalKey += "?";
|
|
350
|
+
}
|
|
351
|
+
let typeValue = value.type;
|
|
352
|
+
|
|
353
|
+
current[finalKey] = `__TYPE_START__${typeValue}__TYPE_END__`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let objectStructure = buildObjectFromTypePathValues(typePathValues);
|
|
361
|
+
|
|
362
|
+
let jsonString = JSON.stringify(objectStructure, null, 4);
|
|
363
|
+
|
|
364
|
+
jsonString = jsonString.replace(/"__TYPE_START__(.*?)__TYPE_END__"/g, "$1");
|
|
365
|
+
|
|
366
|
+
jsonString = jsonString.replaceAll(`"*":`, "[key: string]:");
|
|
367
|
+
|
|
368
|
+
jsonString = jsonString.replace(/"([^"]+)":/g, "$1:");
|
|
369
|
+
|
|
370
|
+
// Strip semicolons, because it's too hard to add them all the places they should be, so it's better to just never have them.
|
|
371
|
+
jsonString = jsonString.replace(/,(\s*\n)/g, "$1");
|
|
372
|
+
|
|
373
|
+
return jsonString;
|
|
374
|
+
|
|
375
|
+
function defaultToType(defaultValue: unknown): string | TypePathValue[] {
|
|
376
|
+
if (typeof defaultValue === "number") {
|
|
377
|
+
return "number";
|
|
378
|
+
} else if (typeof defaultValue === "string") {
|
|
379
|
+
return "string";
|
|
380
|
+
} else if (typeof defaultValue === "boolean") {
|
|
381
|
+
return "boolean";
|
|
382
|
+
} else if (defaultValue === null) {
|
|
383
|
+
return "null";
|
|
384
|
+
} else if (defaultValue === undefined) {
|
|
385
|
+
return "undefined";
|
|
386
|
+
} else if (typeof defaultValue === "object" && defaultValue !== null) {
|
|
387
|
+
// Handle nested objects
|
|
388
|
+
let nestedTypes: TypePathValue[] = [];
|
|
389
|
+
for (let [key, value] of Object.entries(defaultValue)) {
|
|
390
|
+
let nestedResult = defaultToType(value);
|
|
391
|
+
if (typeof nestedResult === "string") {
|
|
392
|
+
nestedTypes.push({
|
|
393
|
+
path: [key],
|
|
394
|
+
type: nestedResult,
|
|
395
|
+
isOptional: false // nested object values are not optional by default
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
// It's an array, so we need to prepend key to all paths
|
|
399
|
+
for (let nested of nestedResult) {
|
|
400
|
+
nestedTypes.push({
|
|
401
|
+
path: [key, ...nested.path],
|
|
402
|
+
type: nested.type,
|
|
403
|
+
isOptional: nested.isOptional
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return nestedTypes;
|
|
409
|
+
} else {
|
|
410
|
+
return "unknown";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// export let defaultVoiceParameters: VoiceParameters = {
|
|
416
|
+
// model: "fish-32129816c2e5474494e9dba4d9e92d1f",
|
|
417
|
+
// speed: 1,
|
|
418
|
+
// volumeAdjust: 0,
|
|
419
|
+
// // 1-6 supports (break) and (long-break), amongst others (see https://docs.fish.audio/text-to-speech/fine-grained-control)
|
|
420
|
+
// ttsModel: "speech-1.6",
|
|
421
|
+
// temperature: 0.7,
|
|
422
|
+
// top_p: 0.7,
|
|
423
|
+
// seed: 0,
|
|
424
|
+
// };
|
|
425
|
+
// export const characterVoice = {
|
|
426
|
+
// id: t.string,
|
|
427
|
+
// name: t.string,
|
|
428
|
+
// description: t.string,
|
|
429
|
+
// defaultParameters: t.atomic<VoiceParameters>(defaultVoiceParameters),
|
|
430
|
+
// public: t.boolean,
|
|
431
|
+
// deleted: t.boolean,
|
|
432
|
+
// // hashes of samples
|
|
433
|
+
// samples: t.atomic<string[]>([]),
|
|
434
|
+
// test: t.lookup({
|
|
435
|
+
// x: t.number,
|
|
436
|
+
// xOptional: t.numberOptional,
|
|
437
|
+
// nested: t.lookup({
|
|
438
|
+
// y: t.number,
|
|
439
|
+
// yOptional: t.numberOptional,
|
|
440
|
+
// defaultParameters: t.atomic<VoiceParameters>(defaultVoiceParameters),
|
|
441
|
+
// })
|
|
442
|
+
// })
|
|
443
|
+
// };
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
// #endregion
|
|
249
447
|
|
|
250
448
|
|
|
251
449
|
// #region Implementation
|
|
@@ -288,8 +486,6 @@ function typeDefProxy(typeDef: TypeDefValues) {
|
|
|
288
486
|
|
|
289
487
|
|
|
290
488
|
type InternalTypeDef = {
|
|
291
|
-
// NOTE: We don't store primitives, because, 1, we don't need to, and 2, because
|
|
292
|
-
// they could be added with the .type<number>() call, at which point we can't track them.
|
|
293
489
|
path: SchemaPath;
|
|
294
490
|
atomicOnlyOnWrite?: boolean;
|
|
295
491
|
|
|
@@ -299,6 +495,11 @@ type InternalTypeDef = {
|
|
|
299
495
|
|
|
300
496
|
isNotAtomic?: boolean;
|
|
301
497
|
|
|
498
|
+
isNumber?: boolean;
|
|
499
|
+
isString?: boolean;
|
|
500
|
+
isBoolean?: boolean;
|
|
501
|
+
isNull?: boolean;
|
|
502
|
+
|
|
302
503
|
gcDelay?: number;
|
|
303
504
|
|
|
304
505
|
defaultValue?: unknown;
|
|
@@ -316,6 +517,7 @@ function typeDefTypeToInternalType(
|
|
|
316
517
|
initialValues?: Partial<InternalTypeDef>
|
|
317
518
|
): InternalTypeDef[] {
|
|
318
519
|
let rootResult: InternalTypeDef = { path, ...initialValues };
|
|
520
|
+
|
|
319
521
|
// NOTE: By default functions cause atomic behavior ("atomic", "type", etc), and don't need special handler
|
|
320
522
|
// Most of the overloads are for purely type behavior, and not runtime behavior (or just
|
|
321
523
|
// for nice aliases, so it is more clear what the functions do).
|
|
@@ -416,6 +618,20 @@ function typeDefTypeToInternalType(
|
|
|
416
618
|
// - Basically, we often store very large objects in state, under the expectation it is never traversed. While defaults might not be large, the expectation that it isn't traversed is still there, and if we traverse it and freeze it, we are likely to break things.
|
|
417
619
|
rootResult.defaultValue = params[params.length - 1]?.callParams?.[0] ?? baseDefault;
|
|
418
620
|
}
|
|
621
|
+
|
|
622
|
+
if (typeDef.path.some(x => x.includes("number")) || typeof rootResult.defaultValue === "number") {
|
|
623
|
+
rootResult.isNumber = true;
|
|
624
|
+
}
|
|
625
|
+
if (typeDef.path.some(x => x.includes("string")) || typeof rootResult.defaultValue === "string") {
|
|
626
|
+
rootResult.isString = true;
|
|
627
|
+
}
|
|
628
|
+
if (typeDef.path.some(x => x.includes("boolean")) || typeof rootResult.defaultValue === "boolean") {
|
|
629
|
+
rootResult.isBoolean = true;
|
|
630
|
+
}
|
|
631
|
+
if (typeDef.path.some(x => x.includes("null")) || rootResult.defaultValue === null) {
|
|
632
|
+
rootResult.isNull = true;
|
|
633
|
+
}
|
|
634
|
+
|
|
419
635
|
return results;
|
|
420
636
|
}
|
|
421
637
|
function getTypeDefValues(typeDef: TypeDefObj) {
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// a file prevents it from transforming it, so we'll miss a lot of files doing it this late).
|
|
4
4
|
import "../inject";
|
|
5
5
|
|
|
6
|
+
// Shim Promise.race, as no one wants their promises to leak...
|
|
7
|
+
import { promiseRace } from "socket-function/src/promiseRace";
|
|
8
|
+
Promise.race = promiseRace;
|
|
9
|
+
|
|
6
10
|
import { shimDateNow } from "socket-function/time/trueTimeShim";
|
|
7
11
|
shimDateNow();
|
|
8
12
|
|
|
@@ -56,6 +60,7 @@ typesafecss.setDelayFnc(async fnc => {
|
|
|
56
60
|
fnc();
|
|
57
61
|
});
|
|
58
62
|
|
|
63
|
+
|
|
59
64
|
export { t };
|
|
60
65
|
|
|
61
66
|
let yargObj = parseArgsFactory()
|
|
@@ -191,10 +196,6 @@ export class Querysub {
|
|
|
191
196
|
*/
|
|
192
197
|
public static MAX_FUTURE_CALL_TIME = 10_000;
|
|
193
198
|
|
|
194
|
-
/** Maximum amount of synchronous time spend handling triggered operations until we
|
|
195
|
-
* abort them (with an error).
|
|
196
|
-
*/
|
|
197
|
-
public static SET_MAX_TRIGGER_TIME = (value: number) => ClientWatcher.MAX_TRIGGER_TIME = value;
|
|
198
199
|
|
|
199
200
|
/** Compression makes serialization about 2X slower, but reduces the size by about 2X (more or less if your
|
|
200
201
|
* data size is dominated by value size instead of key size). */
|
|
@@ -415,9 +416,13 @@ export class Querysub {
|
|
|
415
416
|
if (!proxyWatcher.getTriggeredWatcherMaybeUndefined()) {
|
|
416
417
|
void Promise.resolve().finally(callback);
|
|
417
418
|
} else if (proxyWatcher.getTriggeredWatcher().options.temporary) {
|
|
418
|
-
proxyWatcher.getTriggeredWatcher().onInnerDisposed
|
|
419
|
+
let innerDisposed = proxyWatcher.getTriggeredWatcher().onInnerDisposed;
|
|
420
|
+
if (innerDisposed.includes(callback)) return;
|
|
421
|
+
innerDisposed.push(callback);
|
|
419
422
|
} else {
|
|
420
|
-
proxyWatcher.getTriggeredWatcher().onAfterTriggered
|
|
423
|
+
let afterTriggered = proxyWatcher.getTriggeredWatcher().onAfterTriggered;
|
|
424
|
+
if (afterTriggered.includes(callback)) return;
|
|
425
|
+
afterTriggered.push(callback);
|
|
421
426
|
}
|
|
422
427
|
}
|
|
423
428
|
public static onCommitFinishedCommit(callback: () => void) {
|
|
@@ -63,8 +63,11 @@ export class Modal extends qreact.Component<{
|
|
|
63
63
|
function closeModal(id: string) {
|
|
64
64
|
let modelObj = data().modals[id];
|
|
65
65
|
if (!modelObj) return;
|
|
66
|
-
let
|
|
67
|
-
if (
|
|
66
|
+
let onClose = modelObj.onClose;
|
|
67
|
+
if (typeof onClose === "function") {
|
|
68
|
+
let result = onClose();
|
|
69
|
+
if (result === "abortClose") return;
|
|
70
|
+
}
|
|
68
71
|
delete data().modals[id];
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -277,54 +277,52 @@ async function auditSpecificPathsBase(config: {
|
|
|
277
277
|
let reason = remoteWatcher.getWatchReason(path);
|
|
278
278
|
if (!reason) continue;
|
|
279
279
|
|
|
280
|
-
if (isDevDebugbreak()) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
console.log(atTime);
|
|
327
|
-
}
|
|
280
|
+
// if (isDevDebugbreak()) {
|
|
281
|
+
// const ourNodeId = getOwnNodeId();
|
|
282
|
+
// let logs: DebugLog[] = [];
|
|
283
|
+
// let remoteLogs: DebugLog[] = [];
|
|
284
|
+
// // NOTE: Actually, even if we included it because of a parent watch, we still only
|
|
285
|
+
// // care about direct writes to this value.
|
|
286
|
+
// if (reason === "parent") {
|
|
287
|
+
// logs = getLogHistoryIncludes(path);
|
|
288
|
+
// remoteLogs = await DebugLogController.nodes[authorityNodeId].getLogHistoryEquals(path);
|
|
289
|
+
|
|
290
|
+
// let logs2 = getLogHistoryIncludes(getParentPathStr(path));
|
|
291
|
+
// let remoteLogs2 = await DebugLogController.nodes[authorityNodeId].getLogHistoryEquals(getParentPathStr(path));
|
|
292
|
+
// logs.push(...logs2);
|
|
293
|
+
// remoteLogs.push(...remoteLogs2);
|
|
294
|
+
// } else {
|
|
295
|
+
// logs = getLogHistoryEquals(path);
|
|
296
|
+
// remoteLogs = await DebugLogController.nodes[authorityNodeId].getLogHistoryEquals(path);
|
|
297
|
+
// }
|
|
298
|
+
// let atTime = Date.now();
|
|
299
|
+
// console.error(`Audit mismatch for ${path} from ${authorityNodeId} detected at ${atTime}. Watching because: ${reason}`);
|
|
300
|
+
// console.log(`Value Remote (${value?.valid ? "valid" : "invalid"}):`, pathValueSerializer.getPathValue(value));
|
|
301
|
+
// console.log(`Value Local (${ourValue?.valid ? "valid" : "invalid"}):`, pathValueSerializer.getPathValue(ourValue));
|
|
302
|
+
// console.log(`Time Remote:`, value?.time.time);
|
|
303
|
+
// console.log(`Time Local:`, ourValue?.time.time);
|
|
304
|
+
// console.log(`Our node id:`, ourNodeId);
|
|
305
|
+
// console.log(`Time now:`, atTime);
|
|
306
|
+
// console.log();
|
|
307
|
+
// let logObjs: { source: string, log: DebugLog }[] = [];
|
|
308
|
+
// for (let log of logs) {
|
|
309
|
+
// logObjs.push({ source: "local", log });
|
|
310
|
+
// }
|
|
311
|
+
// for (let log of remoteLogs) {
|
|
312
|
+
// logObjs.push({ source: "remote", log });
|
|
313
|
+
// }
|
|
314
|
+
// sort(logObjs, x => x.log.time);
|
|
315
|
+
// let now = Date.now();
|
|
316
|
+
// for (let { source, log } of logObjs.slice(-100)) {
|
|
317
|
+
// console.log(`${source.padEnd(8, " ")} ${log.type.padEnd(20, " ")}`, now - log.time, log.values, log.time);
|
|
318
|
+
// }
|
|
319
|
+
// console.log(path);
|
|
320
|
+
// console.log(value);
|
|
321
|
+
// console.log(ourValue);
|
|
322
|
+
// console.log(logs.length);
|
|
323
|
+
// console.log(remoteLogs.length);
|
|
324
|
+
// console.log(atTime);
|
|
325
|
+
// }
|
|
328
326
|
auditLog("Failed memory audit", { path, value, ourValue });
|
|
329
327
|
|
|
330
328
|
invalidValues.add(path);
|
|
@@ -19,17 +19,9 @@ import { StickyBottomScroll } from "../../library-components/StickyBottomScroll"
|
|
|
19
19
|
import { PrimitiveDisplay } from "../../diagnostics/logs/ObjectDisplay";
|
|
20
20
|
import { parseAnsiColors, rgbToHsl } from "../../diagnostics/logs/ansiFormat";
|
|
21
21
|
import { RenderGitRefInfo, UpdateServiceButtons, bigEmoji, buttonStyle } from "./deployButtons";
|
|
22
|
+
import { TypedConfigEditor } from "../../library-components/TypedConfigEditor";
|
|
23
|
+
|
|
22
24
|
|
|
23
|
-
// Type declarations for Monaco editor
|
|
24
|
-
declare global {
|
|
25
|
-
interface Window {
|
|
26
|
-
monaco: any;
|
|
27
|
-
require: {
|
|
28
|
-
config: (options: any) => void;
|
|
29
|
-
(modules: string[], callback: () => void): void;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
25
|
|
|
34
26
|
export class ServiceDetailPage extends qreact.Component {
|
|
35
27
|
state = t.state({
|
|
@@ -46,64 +38,12 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
46
38
|
}),
|
|
47
39
|
});
|
|
48
40
|
|
|
49
|
-
private async ensureMonacoLoaded(): Promise<void> {
|
|
50
|
-
// If Monaco is already loaded, return immediately
|
|
51
|
-
if (window.monaco) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Load Monaco from CDN
|
|
56
|
-
return new Promise<void>((resolve, reject) => {
|
|
57
|
-
const script = document.createElement("script");
|
|
58
|
-
script.src = "https://unpkg.com/monaco-editor@0.44.0/min/vs/loader.js";
|
|
59
|
-
script.onload = () => {
|
|
60
|
-
window.require.config({ paths: { vs: "https://unpkg.com/monaco-editor@0.44.0/min/vs" } });
|
|
61
|
-
window.require(["vs/editor/editor.main"], () => {
|
|
62
|
-
resolve();
|
|
63
|
-
});
|
|
64
|
-
};
|
|
65
|
-
script.onerror = () => {
|
|
66
|
-
reject(new Error("Failed to load Monaco editor"));
|
|
67
|
-
};
|
|
68
|
-
document.head.appendChild(script);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private setupMonacoWithTypes(serviceConfigType: string): string {
|
|
73
|
-
// Configure TypeScript compiler options - turn off default libs
|
|
74
|
-
window.monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
|
75
|
-
target: window.monaco.languages.typescript.ScriptTarget.ES2022,
|
|
76
|
-
strict: true,
|
|
77
|
-
lib: [] // Turn off all default libs
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Add global ServiceConfig type declaration
|
|
81
|
-
const globals = `
|
|
82
|
-
type ServiceConfig = ${serviceConfigType}
|
|
83
|
-
`;
|
|
84
|
-
|
|
85
|
-
window.monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
|
86
|
-
globals,
|
|
87
|
-
"serviceconfig-types.d.ts"
|
|
88
|
-
);
|
|
89
41
|
|
|
90
|
-
return globals;
|
|
91
|
-
}
|
|
92
42
|
|
|
93
43
|
private updateUnsavedChanges(updatedConfig: ServiceConfig) {
|
|
94
44
|
// Do not let them update the serviceId, as that would break things
|
|
95
45
|
updatedConfig.serviceId = selectedServiceIdParam.value;
|
|
96
|
-
const newConfigStr = JSON.stringify(updatedConfig, null, 4);
|
|
97
46
|
this.state.unsavedChanges = updatedConfig;
|
|
98
|
-
|
|
99
|
-
// Update Monaco model if it exists
|
|
100
|
-
const configUri = window.monaco?.Uri?.parse("inmemory://model/serviceconfig.ts");
|
|
101
|
-
if (configUri && window.monaco?.editor) {
|
|
102
|
-
const model = window.monaco.editor.getModel(configUri);
|
|
103
|
-
if (model) {
|
|
104
|
-
model.setValue(`const config: ServiceConfig = ${newConfigStr}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
47
|
}
|
|
108
48
|
private updateConfigSynced(updatedConfig: ServiceConfig) {
|
|
109
49
|
Querysub.onCommitFinished(() => {
|
|
@@ -174,16 +114,6 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
174
114
|
|
|
175
115
|
Querysub.onCommitFinished(async () => {
|
|
176
116
|
try {
|
|
177
|
-
|
|
178
|
-
// Update Monaco model with new config
|
|
179
|
-
const newConfigStr = JSON.stringify(updatedConfig, null, 4);
|
|
180
|
-
const configUri = window.monaco?.Uri?.parse("inmemory://model/serviceconfig.ts");
|
|
181
|
-
if (configUri && window.monaco?.editor) {
|
|
182
|
-
const model = window.monaco.editor.getModel(configUri);
|
|
183
|
-
if (model) {
|
|
184
|
-
model.setValue(`const config: ServiceConfig = ${newConfigStr}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
117
|
await MachineServiceController(SocketFunction.browserNodeId()).setServiceConfigs.promise([updatedConfig]);
|
|
188
118
|
} catch (error) {
|
|
189
119
|
Querysub.localCommit(() => {
|
|
@@ -502,16 +432,6 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
502
432
|
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(50, 80, 50)}
|
|
503
433
|
onClick={() => {
|
|
504
434
|
this.state.unsavedChanges = undefined;
|
|
505
|
-
|
|
506
|
-
// Update Monaco editor with original config
|
|
507
|
-
const newConfigStr = JSON.stringify(originalConfig, null, 4);
|
|
508
|
-
const configUri = window.monaco?.Uri?.parse("inmemory://model/serviceconfig.ts");
|
|
509
|
-
if (configUri && window.monaco?.editor) {
|
|
510
|
-
const model = window.monaco.editor.getModel(configUri);
|
|
511
|
-
if (model) {
|
|
512
|
-
model.setValue(`const config: ServiceConfig = ${newConfigStr}`);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
435
|
}}>
|
|
516
436
|
Discard Changes
|
|
517
437
|
</button>
|
|
@@ -605,75 +525,15 @@ type ServiceConfig = ${serviceConfigType}
|
|
|
605
525
|
</div>
|
|
606
526
|
)}
|
|
607
527
|
|
|
608
|
-
{/*
|
|
609
|
-
<
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// Setup TypeScript with ServiceConfig type
|
|
619
|
-
const typeDeclarations = this.setupMonacoWithTypes(serviceConfigType!);
|
|
620
|
-
|
|
621
|
-
const configStr = JSON.stringify(config, null, 4);
|
|
622
|
-
|
|
623
|
-
// Create hidden model for the type definitions
|
|
624
|
-
const typeUri = window.monaco.Uri.parse("inmemory://model/serviceconfig-types.d.ts");
|
|
625
|
-
let typeModel = window.monaco.editor.getModel(typeUri);
|
|
626
|
-
if (!typeModel) {
|
|
627
|
-
window.monaco.editor.createModel(typeDeclarations, "typescript", typeUri);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Create a TypeScript model with clean config assignment
|
|
631
|
-
const configUri = window.monaco.Uri.parse("inmemory://model/serviceconfig.ts");
|
|
632
|
-
let model = window.monaco.editor.getModel(configUri);
|
|
633
|
-
if (model) {
|
|
634
|
-
// Update existing model content
|
|
635
|
-
model.setValue(`const config: ServiceConfig = ${configStr}`);
|
|
636
|
-
} else {
|
|
637
|
-
// Create new model
|
|
638
|
-
model = window.monaco.editor.createModel(
|
|
639
|
-
`const config: ServiceConfig = ${configStr}`,
|
|
640
|
-
"typescript",
|
|
641
|
-
configUri
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Create the editor
|
|
646
|
-
const editor = window.monaco.editor.create(editorElement, {
|
|
647
|
-
model: model,
|
|
648
|
-
theme: "vs-dark",
|
|
649
|
-
automaticLayout: true,
|
|
650
|
-
minimap: { enabled: false },
|
|
651
|
-
wordWrap: "on",
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
// Listen for changes
|
|
655
|
-
editor.onDidChangeModelContent(() => {
|
|
656
|
-
const value = editor.getValue();
|
|
657
|
-
const configMatch = value.match(/const config: ServiceConfig = ([\s\S]*)/);
|
|
658
|
-
if (configMatch) {
|
|
659
|
-
try {
|
|
660
|
-
// Try to parse the JSON
|
|
661
|
-
const parsedConfig = JSON.parse(configMatch[1]);
|
|
662
|
-
Querysub.localCommit(() => {
|
|
663
|
-
this.state.unsavedChanges = parsedConfig;
|
|
664
|
-
});
|
|
665
|
-
} catch (error) {
|
|
666
|
-
// Ignore parsing errors, keep previous unsavedChanges
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
});
|
|
671
|
-
}}>
|
|
672
|
-
<div style={{
|
|
673
|
-
width: `${Math.min(window.innerWidth - 100, 1000)}px`,
|
|
674
|
-
height: "600px"
|
|
675
|
-
}} />
|
|
676
|
-
</div>
|
|
528
|
+
{/* Config Editor */}
|
|
529
|
+
<TypedConfigEditor
|
|
530
|
+
value={config}
|
|
531
|
+
onValueChange={(newValue) => {
|
|
532
|
+
this.state.unsavedChanges = newValue as ServiceConfig;
|
|
533
|
+
}}
|
|
534
|
+
typeDefinition={serviceConfigType}
|
|
535
|
+
sizeClassName={css.size(1000, 600)}
|
|
536
|
+
/>
|
|
677
537
|
</div>;
|
|
678
538
|
}
|
|
679
539
|
}
|
|
@@ -413,7 +413,7 @@ const runScreenCommand = measureWrap(async function runScreenCommand(config: {
|
|
|
413
413
|
rollingObj.pinnedDuration = config.rollingWindow;
|
|
414
414
|
}
|
|
415
415
|
let existingScreen = screens.find(x => x.screenName === screenName);
|
|
416
|
-
if (existingScreen && !rollingObj) {
|
|
416
|
+
if (existingScreen && !rollingObj && await isScreenRunningProcess(existingScreen.pid)) {
|
|
417
417
|
let nodeIdPath = os.homedir() + "/" + SERVICE_FOLDER + screenName + "/" + SERVICE_NODE_FILE_NAME;
|
|
418
418
|
let rollingFinalTime = Date.now() + config.rollingWindow;
|
|
419
419
|
if (fs.existsSync(nodeIdPath)) {
|
|
@@ -668,7 +668,7 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
668
668
|
continue;
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
-
console.log(`Resyncing service ${magenta(screenName)}, with ${JSON.stringify(config.parameters)}`);
|
|
671
|
+
console.log(`Resyncing service ${magenta(screenName)}, with ${JSON.stringify(config.parameters)}, isRunning = ${screenIsRunning}, sameParameters = ${sameParameters}`);
|
|
672
672
|
|
|
673
673
|
await fs.promises.writeFile(parameterPath, newParametersString);
|
|
674
674
|
|
|
@@ -728,7 +728,7 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
728
728
|
rollingInfo = undefined;
|
|
729
729
|
}
|
|
730
730
|
if (rollingInfo) {
|
|
731
|
-
console.log(green(`Skipping killing rolling screen ${screenName} because it's still alive at ${new Date().toLocaleString()}. Keeping it alive until ${new Date(rollingInfo.pinnedTime + rollingInfo.pinnedDuration).toLocaleString()}`));
|
|
731
|
+
console.log(green(`Skipping killing rolling screen ${screenName} because it's still alive at ${new Date().toLocaleString()}. Keeping it alive until ${new Date(rollingInfo.pinnedTime + rollingInfo.pinnedDuration).toLocaleString()}. Was pinned at ${new Date(rollingInfo.pinnedTime).toLocaleString()}, rolling duration is ${formatTime(rollingInfo.pinnedDuration)}`));
|
|
732
732
|
continue;
|
|
733
733
|
}
|
|
734
734
|
}
|
|
@@ -312,7 +312,7 @@ if (isNode()) {
|
|
|
312
312
|
}
|
|
313
313
|
});
|
|
314
314
|
// Wait a random time, so we hopefully don't synchronize with any other services on this machine
|
|
315
|
-
runInfinitePoll(timeInHour
|
|
315
|
+
runInfinitePoll(timeInHour * (1 + Math.random()), async function compressOldLogs() {
|
|
316
316
|
let logFiles = await fs.promises.readdir(folder);
|
|
317
317
|
let compressTime = Date.now() - LOG_FILE_DURATION * 2;
|
|
318
318
|
let filesCompressed = 0;
|
|
@@ -112,6 +112,7 @@ export function triggerKeyDown(e: KeyboardEvent, forceAmbient = true) {
|
|
|
112
112
|
|
|
113
113
|
function trigger(keyFull: string) {
|
|
114
114
|
keyFull = keyFull.toLowerCase();
|
|
115
|
+
console.log(`Triggering hotkey ${keyFull}`);
|
|
115
116
|
let listeners = Array.from(registeredWatches).map(x => {
|
|
116
117
|
let props = Querysub.localRead(() => ({ ...x.props }), { allowProxyResults: true });
|
|
117
118
|
let match = props.hotkeys?.find(x => {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { qreact } from "../4-dom/qreact";
|
|
2
|
+
import { t } from "../2-proxy/schema2";
|
|
3
|
+
import { css } from "typesafecss";
|
|
4
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
5
|
+
|
|
6
|
+
// Type declarations for Monaco editor
|
|
7
|
+
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
monaco: any;
|
|
10
|
+
require: {
|
|
11
|
+
config: (options: unknown) => void;
|
|
12
|
+
(modules: string[], callback: () => void): void;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export interface TypedConfigEditorProps {
|
|
19
|
+
value: any;
|
|
20
|
+
onValueChange?: (newValue: unknown) => void;
|
|
21
|
+
/**
|
|
22
|
+
Ex:
|
|
23
|
+
{
|
|
24
|
+
x: number;
|
|
25
|
+
y?: string;
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
typeDefinition: string;
|
|
29
|
+
sizeClassName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
let nextSeqNum = Math.floor(Date.now() / 1000);
|
|
34
|
+
export class TypedConfigEditor extends qreact.Component<TypedConfigEditorProps> {
|
|
35
|
+
state = t.state({
|
|
36
|
+
editorReady: t.type(false),
|
|
37
|
+
currentValue: t.type<any>(undefined)
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
private editor: any = null;
|
|
41
|
+
private model: any = null;
|
|
42
|
+
private uniqueId = nextSeqNum++;
|
|
43
|
+
|
|
44
|
+
private typeName = `Type${this.uniqueId}`;
|
|
45
|
+
private valueName = `config${this.uniqueId}`;
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
private async ensureMonacoLoaded(): Promise<void> {
|
|
49
|
+
// If Monaco is already loaded, return immediately
|
|
50
|
+
if (window.monaco) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Load Monaco from CDN
|
|
55
|
+
return new Promise<void>((resolve, reject) => {
|
|
56
|
+
const script = document.createElement("script");
|
|
57
|
+
script.src = "https://unpkg.com/monaco-editor@0.44.0/min/vs/loader.js";
|
|
58
|
+
script.onload = () => {
|
|
59
|
+
window.require.config({ paths: { vs: "https://unpkg.com/monaco-editor@0.44.0/min/vs" } });
|
|
60
|
+
window.require(["vs/editor/editor.main"], () => {
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
script.onerror = () => {
|
|
65
|
+
reject(new Error("Failed to load Monaco editor"));
|
|
66
|
+
};
|
|
67
|
+
document.head.appendChild(script);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private setupMonacoWithTypes(typeDefinition: string, typeName: string): void {
|
|
72
|
+
// Configure TypeScript compiler options - turn off default libs
|
|
73
|
+
window.monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
|
74
|
+
target: window.monaco.languages.typescript.ScriptTarget.ES2022,
|
|
75
|
+
strict: true,
|
|
76
|
+
lib: [] // Turn off all default libs
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Add type declarations
|
|
80
|
+
window.monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
|
81
|
+
typeDefinition,
|
|
82
|
+
`${typeName.toLowerCase()}-types.d.ts`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private updateEditorValue(value: any): void {
|
|
87
|
+
if (!this.model) return;
|
|
88
|
+
|
|
89
|
+
const valueStr = JSON.stringify(value, null, 4);
|
|
90
|
+
const content = `const ${this.valueName}: ${this.typeName} = ${valueStr}`;
|
|
91
|
+
|
|
92
|
+
// Only update if content has changed to avoid cursor jumping
|
|
93
|
+
if (this.model.getValue() !== content) {
|
|
94
|
+
this.model.setValue(content);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
componentDidMount(): void {
|
|
99
|
+
this.state.currentValue = this.props.value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
componentDidUpdate(prevProps: TypedConfigEditorProps): void {
|
|
103
|
+
// Update editor if value changed from props
|
|
104
|
+
if (this.props.value !== prevProps.value && this.props.value !== this.state.currentValue) {
|
|
105
|
+
this.state.currentValue = this.props.value;
|
|
106
|
+
this.updateEditorValue(this.props.value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
componentWillUnmount(): void {
|
|
111
|
+
if (this.editor) {
|
|
112
|
+
this.editor.dispose();
|
|
113
|
+
}
|
|
114
|
+
if (this.model) {
|
|
115
|
+
this.model.dispose();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render() {
|
|
120
|
+
let typeName = this.typeName;
|
|
121
|
+
let valueName = this.valueName;
|
|
122
|
+
let typeDefinition = `type ${typeName} = ${this.props.typeDefinition}`;
|
|
123
|
+
|
|
124
|
+
return <div
|
|
125
|
+
key={"Editor-" + this.uniqueId}
|
|
126
|
+
ref2={async element => {
|
|
127
|
+
if (!element) return;
|
|
128
|
+
let editorElement = element.children[0] as HTMLElement;
|
|
129
|
+
|
|
130
|
+
Querysub.onCommitFinished(async () => {
|
|
131
|
+
// Ensure Monaco is loaded
|
|
132
|
+
await this.ensureMonacoLoaded();
|
|
133
|
+
|
|
134
|
+
// Setup TypeScript with custom types
|
|
135
|
+
this.setupMonacoWithTypes(typeDefinition, typeName);
|
|
136
|
+
|
|
137
|
+
let propsValue = Querysub.localRead(() => this.props.value);
|
|
138
|
+
let valueStr = JSON.stringify(propsValue, null, 4);
|
|
139
|
+
|
|
140
|
+
// Create hidden model for the type definitions
|
|
141
|
+
const typeUri = window.monaco.Uri.parse(`inmemory://model/${this.uniqueId}-${typeName.toLowerCase()}-types.d.ts`);
|
|
142
|
+
let typeModel = window.monaco.editor.getModel(typeUri);
|
|
143
|
+
if (!typeModel) {
|
|
144
|
+
window.monaco.editor.createModel(typeDefinition, "typescript", typeUri);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Create a TypeScript model with clean config assignment
|
|
148
|
+
const configUri = window.monaco.Uri.parse(`inmemory://model/${this.uniqueId}-${valueName}.ts`);
|
|
149
|
+
this.model = window.monaco.editor.getModel(configUri);
|
|
150
|
+
if (this.model) {
|
|
151
|
+
// Update existing model content
|
|
152
|
+
this.updateEditorValue(propsValue);
|
|
153
|
+
} else {
|
|
154
|
+
// Create new model
|
|
155
|
+
this.model = window.monaco.editor.createModel(
|
|
156
|
+
`const ${valueName}: ${typeName} = ${valueStr}`,
|
|
157
|
+
"typescript",
|
|
158
|
+
configUri
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create the editor
|
|
163
|
+
this.editor = window.monaco.editor.create(editorElement, {
|
|
164
|
+
model: this.model,
|
|
165
|
+
theme: "vs-dark",
|
|
166
|
+
automaticLayout: true,
|
|
167
|
+
minimap: { enabled: false },
|
|
168
|
+
wordWrap: "on",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Listen for changes
|
|
172
|
+
this.editor.onDidChangeModelContent(() => {
|
|
173
|
+
const value = this.editor.getValue();
|
|
174
|
+
const configMatch = value.match(new RegExp(`const ${valueName}: ${typeName} = ([\\s\\S]*)`));
|
|
175
|
+
if (configMatch) {
|
|
176
|
+
try {
|
|
177
|
+
// Try to parse the JSON
|
|
178
|
+
const parsedValue = JSON.parse(configMatch[1]);
|
|
179
|
+
Querysub.localCommit(() => {
|
|
180
|
+
this.state.currentValue = parsedValue;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
Querysub.commit(() => {
|
|
184
|
+
this.props.onValueChange?.(parsedValue);
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Ignore parsing errors, keep previous value
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
Querysub.localCommit(() => {
|
|
193
|
+
this.state.editorReady = true;
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<div className={css
|
|
199
|
+
.width(Math.min(window.innerWidth - 100, 600), "soft")
|
|
200
|
+
.height(Math.min(window.innerHeight - 100, 1000), "soft")
|
|
201
|
+
+ this.props.sizeClassName}
|
|
202
|
+
/>
|
|
203
|
+
</div>;
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/niceStringify.ts
CHANGED
|
@@ -11,6 +11,9 @@ export const niceStringifyUndefined = `{Undefined}`;
|
|
|
11
11
|
// BUG: This is actually broken for hex strings. Hex strings may sometimes be entirely numbers,
|
|
12
12
|
// which means they will randomly change type.
|
|
13
13
|
function looksLikeJSON(str: string) {
|
|
14
|
+
let looksLikeNumber = (str: string) => {
|
|
15
|
+
return str.match(/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/);
|
|
16
|
+
};
|
|
14
17
|
return (
|
|
15
18
|
str === "null"
|
|
16
19
|
|| str === "true"
|
|
@@ -18,8 +21,7 @@ function looksLikeJSON(str: string) {
|
|
|
18
21
|
|| str[0] === `"` && str[str.length - 1] === `"`
|
|
19
22
|
|| str[0] === `[` && str[str.length - 1] === `]`
|
|
20
23
|
|| str[0] === `{` && str[str.length - 1] === `}`
|
|
21
|
-
|| (48 <= str.charCodeAt(0) && str.charCodeAt(0) <= 57)
|
|
22
|
-
|| str.length > 1 && str[0] === "-" && (48 <= str.charCodeAt(1) && str.charCodeAt(1) <= 57)
|
|
24
|
+
|| (48 <= str.charCodeAt(0) && str.charCodeAt(0) <= 57 || str[0] === "-") && looksLikeNumber(str)
|
|
23
25
|
|| str === niceStringifyTrue
|
|
24
26
|
|| str === niceStringifyUndefined
|
|
25
27
|
);
|