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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.309.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.135.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
- for (let [video, viewTime] of viewedVideos) {
71
- changeStream.write(userId, viewTime);
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 MAX_TRIGGER_TIME = 1000 * 5;
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 > ClientWatcher.MAX_TRIGGER_TIME
379
+ Date.now() - time > MAX_TRIGGER_TIME
378
380
  && !isDevDebugbreak()
379
381
  ) {
380
382
  stoppedEarly = true;
@@ -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 behave as expected with respect to delete, even if the entries are not atomic.
113
- * As in, `delete lookup["a"]` will mean `lookup["a"] === undefined`, even
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.push(callback);
419
+ let innerDisposed = proxyWatcher.getTriggeredWatcher().onInnerDisposed;
420
+ if (innerDisposed.includes(callback)) return;
421
+ innerDisposed.push(callback);
419
422
  } else {
420
- proxyWatcher.getTriggeredWatcher().onAfterTriggered.push(callback);
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 result = modelObj.onClose?.();
67
- if (result === "abortClose") return;
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
- 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
- debugbreak(2);
320
- debugger;
321
- console.log(path);
322
- console.log(value);
323
- console.log(ourValue);
324
- console.log(logs.length);
325
- console.log(remoteLogs.length);
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
- {/* Monaco Editor */}
609
- <div key="editor" ref2={async element => {
610
- if (!element) return;
611
- let editorElement = element.children[0] as HTMLElement;
612
-
613
- Querysub.onCommitFinished(async () => {
614
-
615
- // Ensure Monaco is loaded
616
- await this.ensureMonacoLoaded();
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 + (1 + Math.random()), async function compressOldLogs() {
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
+ }
@@ -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
  );