querysub 0.509.0 → 0.511.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 +1 -1
- package/spec.txt +1 -4
- package/src/4-querysub/Querysub.ts +2 -0
- package/src/deployManager/components/MachineDetailPage.tsx +1 -1
- package/src/deployManager/components/ServiceDetailPage.tsx +311 -185
- package/src/deployManager/components/ServicesListPage.tsx +5 -4
- package/src/deployManager/components/Tools.tsx +6 -3
- package/src/deployManager/components/deployButtons.tsx +7 -1
- package/src/deployManager/machineApplyMainCode.ts +82 -75
- package/src/deployManager/machineController.ts +2 -2
- package/src/deployManager/machineSchema.ts +108 -106
- package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +1 -1
- package/src/library-components/TypedConfigEditor.tsx +16 -17
- package/src/misc.ts +30 -0
|
@@ -66,29 +66,37 @@ export type MachineInfo = {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
export type ServiceParameters = {
|
|
70
|
+
/** MUST be unique, and clean enough to be used as the screen/tmux name, and folder name */
|
|
71
|
+
key: string;
|
|
72
|
+
|
|
73
|
+
/** There can be duplicate machine IDs which just means we apply multiple times on that machine, as `${key}-${index}-dply`, each with their own screen and folder. Part of the parameters so machine changes are released on the same schedule as everything else (but they never restart already-running instances, see restartParametersKey). Optional, as already deployed configs predate the field (treat missing as []). */
|
|
74
|
+
machineIds?: string[];
|
|
75
|
+
|
|
76
|
+
repoUrl: string;
|
|
77
|
+
gitRef: string;
|
|
78
|
+
command: string;
|
|
79
|
+
/** Allows forcing an update */
|
|
80
|
+
poke?: number;
|
|
81
|
+
|
|
82
|
+
/** Not set by default, so we can setup the configuration before deploying it (or so we can undeploy easily without deleting it) */
|
|
83
|
+
deploy?: boolean;
|
|
84
|
+
|
|
85
|
+
/** How long the new and old instances run simultaneously during a release. The new instances start at releaseTime - overlapTime, and the old instances are shut down at releaseTime. */
|
|
86
|
+
overlapTime?: number;
|
|
87
|
+
|
|
88
|
+
/** When these parameters go live. Until then machines keep running oldParameters. Absent (or in the past) means live now. */
|
|
89
|
+
releaseTime?: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
69
92
|
export type ServiceConfig = {
|
|
70
93
|
/** Just a random id to manage the service */
|
|
71
94
|
serviceId: string;
|
|
72
95
|
|
|
73
|
-
/** There can be duplicate machine IDs which just means we apply multiple times on that machine, as `${key}-${index}-dply`, each with their own screen and folder */
|
|
74
|
-
machineIds: string[];
|
|
75
96
|
/** When parameters update, we restart it (and when info updates, we do not) */
|
|
76
|
-
parameters:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
repoUrl: string;
|
|
81
|
-
gitRef: string;
|
|
82
|
-
command: string;
|
|
83
|
-
/** Allows forcing an update */
|
|
84
|
-
poke?: number;
|
|
85
|
-
|
|
86
|
-
/** Not set by default, so we can setup the configuration before deploying it (or so we can undeploy easily without deleting it) */
|
|
87
|
-
deploy?: boolean;
|
|
88
|
-
|
|
89
|
-
/** How long the new and old instances run simultaneously during a switchover. The new instances start at switchTime - overlapTime, and the old instances are shut down at switchTime. This is the default; each switchover can override it. */
|
|
90
|
-
overlapTime?: number;
|
|
91
|
-
};
|
|
97
|
+
parameters: ServiceParameters;
|
|
98
|
+
/** What machines keep running until parameters.releaseTime passes. Set automatically on every update (never by callers). */
|
|
99
|
+
oldParameters?: ServiceParameters;
|
|
92
100
|
info: {
|
|
93
101
|
title: string;
|
|
94
102
|
notes: string;
|
|
@@ -96,25 +104,49 @@ export type ServiceConfig = {
|
|
|
96
104
|
};
|
|
97
105
|
};
|
|
98
106
|
|
|
99
|
-
export const DEFAULT_OVERLAP_TIME = timeInMinute;
|
|
107
|
+
export const DEFAULT_OVERLAP_TIME = timeInMinute * 10;
|
|
108
|
+
|
|
109
|
+
/** The parameters machines should actually be running right now. */
|
|
110
|
+
export function getLiveServiceParameters(config: ServiceConfig): ServiceParameters {
|
|
111
|
+
let releaseTime = config.parameters.releaseTime;
|
|
112
|
+
if (releaseTime && Date.now() < releaseTime && config.oldParameters) {
|
|
113
|
+
return config.oldParameters;
|
|
114
|
+
}
|
|
115
|
+
return config.parameters;
|
|
116
|
+
}
|
|
117
|
+
/** The full config as it is currently live (which parameters field that is depends on the current time). */
|
|
118
|
+
export function getLiveServiceConfig(config: ServiceConfig): ServiceConfig {
|
|
119
|
+
return { ...config, parameters: getLiveServiceParameters(config), oldParameters: undefined };
|
|
120
|
+
}
|
|
121
|
+
export function stripReleaseState(config: ServiceConfig): ServiceConfig {
|
|
122
|
+
return { ...config, oldParameters: undefined, parameters: { ...config.parameters, releaseTime: undefined } };
|
|
123
|
+
}
|
|
124
|
+
/** The parts of the parameters that a running instance actually depends on. machineIds / releaseTime / overlapTime don't change what an instance runs, so changes to only them must never restart already-running instances (ex, adding a machine must not restart the existing machines). */
|
|
125
|
+
export function restartParametersKey(parameters: ServiceParameters): string {
|
|
126
|
+
return JSON.stringify({ ...parameters, machineIds: undefined, releaseTime: undefined, overlapTime: undefined });
|
|
127
|
+
}
|
|
128
|
+
function getConfigMachineIds(config: ServiceConfig): string[] {
|
|
129
|
+
return [...new Set([...(config.parameters.machineIds || []), ...(config.oldParameters?.machineIds || [])])];
|
|
130
|
+
}
|
|
131
|
+
// Migrates records that stored machineIds at the top level of ServiceConfig (it now lives in parameters)
|
|
132
|
+
function normalizeServiceConfig(config: ServiceConfig): ServiceConfig {
|
|
133
|
+
let legacyMachineIds = (config as unknown as { machineIds?: string[] }).machineIds;
|
|
134
|
+
if (legacyMachineIds) {
|
|
135
|
+
config.parameters.machineIds = config.parameters.machineIds || legacyMachineIds;
|
|
136
|
+
if (config.oldParameters) {
|
|
137
|
+
config.oldParameters.machineIds = config.oldParameters.machineIds || legacyMachineIds;
|
|
138
|
+
}
|
|
139
|
+
delete (config as unknown as { machineIds?: string[] }).machineIds;
|
|
140
|
+
}
|
|
141
|
+
return config;
|
|
142
|
+
}
|
|
100
143
|
|
|
101
|
-
export type ServiceSwitchover = {
|
|
102
|
-
serviceId: string;
|
|
103
|
-
/** When the old instances are shut down and the pending config becomes live */
|
|
104
|
-
switchTime: number;
|
|
105
|
-
/** The new instances start at switchTime - overlapTime */
|
|
106
|
-
overlapTime: number;
|
|
107
|
-
scheduledAt: number;
|
|
108
|
-
};
|
|
109
144
|
export type MachineConfig = {
|
|
110
145
|
machineId: string;
|
|
111
146
|
disabled: boolean;
|
|
112
147
|
};
|
|
113
148
|
export const machineInfos = archiveJSONT<MachineInfo>(() => nestArchives("machines/machine-heartbeats/", getArchivesBackblaze(getDomain())));
|
|
114
149
|
export const serviceConfigs = archiveJSONT<ServiceConfig>(() => nestArchives("machines/service-configs/", getArchivesBackblaze(getDomain())));
|
|
115
|
-
// Edits in the UI save here (still on disk), and only become live when a switchover is deployed / scheduled
|
|
116
|
-
export const pendingServiceConfigs = archiveJSONT<ServiceConfig>(() => nestArchives("machines/service-configs-pending/", getArchivesBackblaze(getDomain())));
|
|
117
|
-
export const serviceSwitchovers = archiveJSONT<ServiceSwitchover>(() => nestArchives("machines/service-switchovers/", getArchivesBackblaze(getDomain())));
|
|
118
150
|
export const machineConfigs = archiveJSONT<MachineConfig>(() => nestArchives("machines/machine-configs/", getArchivesBackblaze(getDomain())));
|
|
119
151
|
|
|
120
152
|
export type LaunchRecord = {
|
|
@@ -234,7 +266,8 @@ export class MachineServiceControllerBase {
|
|
|
234
266
|
])];
|
|
235
267
|
}
|
|
236
268
|
public async getServiceConfig(serviceId: string): Promise<ServiceConfig | undefined> {
|
|
237
|
-
|
|
269
|
+
let config = await serviceConfigs.get(serviceId);
|
|
270
|
+
return config && normalizeServiceConfig(config);
|
|
238
271
|
}
|
|
239
272
|
|
|
240
273
|
public async getMachineConfigList() {
|
|
@@ -278,14 +311,14 @@ export class MachineServiceControllerBase {
|
|
|
278
311
|
throw new Error(`Service ${serviceId} already exists`);
|
|
279
312
|
}
|
|
280
313
|
await serviceConfigs.set(serviceId, config);
|
|
281
|
-
|
|
314
|
+
void this.notifyMachines(getConfigMachineIds(config), []);
|
|
282
315
|
}
|
|
283
316
|
|
|
284
317
|
public async setServiceConfigs(configs: ServiceConfig[]) {
|
|
285
318
|
let newMachines = new Set<string>();
|
|
286
319
|
let oldMachines = new Set<string>();
|
|
287
320
|
let usedKeys = new Set<string>();
|
|
288
|
-
|
|
321
|
+
await Promise.all(configs.map(async config => {
|
|
289
322
|
if (usedKeys.has(config.parameters.key)) {
|
|
290
323
|
console.warn(`Duplicate key: ${JSON.stringify(config.parameters.key)}. Not allowed, as this will break things! We are unduplicating the key.`);
|
|
291
324
|
config.parameters.key += `-${Math.random().toString().slice(2)}`;
|
|
@@ -299,19 +332,34 @@ export class MachineServiceControllerBase {
|
|
|
299
332
|
if (!serviceConfig) {
|
|
300
333
|
throw new Error(`Service ${serviceId} does not exist`);
|
|
301
334
|
}
|
|
335
|
+
normalizeServiceConfig(serviceConfig);
|
|
336
|
+
|
|
337
|
+
// The old parameters are whatever is live right now: the previous parameters if their release time passed, else the previous old parameters (an unreleased update was updated again, and machines just keep running what they were running).
|
|
338
|
+
if (config.parameters.releaseTime && Date.now() < config.parameters.releaseTime) {
|
|
339
|
+
let liveParameters = getLiveServiceParameters(serviceConfig);
|
|
340
|
+
if (JSON.stringify({ ...liveParameters, releaseTime: undefined }) === JSON.stringify({ ...config.parameters, releaseTime: undefined })) {
|
|
341
|
+
// The parameters aren't changing, so there is nothing to release (machineIds / info changes apply immediately, only parameters wait for the release time)
|
|
342
|
+
config.parameters.releaseTime = undefined;
|
|
343
|
+
config.oldParameters = undefined;
|
|
344
|
+
} else {
|
|
345
|
+
config.oldParameters = liveParameters;
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
config.oldParameters = undefined;
|
|
349
|
+
}
|
|
302
350
|
|
|
303
351
|
await serviceConfigs.set(serviceId, config);
|
|
304
352
|
// Only notify we were or are deployed. If it's not deployed, this will be ignored anyways.
|
|
305
353
|
if (config.parameters.deploy || serviceConfig.parameters.deploy) {
|
|
306
|
-
for (let machineId of config
|
|
354
|
+
for (let machineId of getConfigMachineIds(config)) {
|
|
307
355
|
newMachines.add(machineId);
|
|
308
356
|
}
|
|
309
|
-
for (let machineId of serviceConfig
|
|
357
|
+
for (let machineId of getConfigMachineIds(serviceConfig)) {
|
|
310
358
|
oldMachines.add(machineId);
|
|
311
359
|
}
|
|
312
360
|
}
|
|
313
|
-
}
|
|
314
|
-
|
|
361
|
+
}));
|
|
362
|
+
void this.notifyMachines([...newMachines], [...oldMachines]);
|
|
315
363
|
}
|
|
316
364
|
|
|
317
365
|
public async deleteServiceConfig(serviceId: string) {
|
|
@@ -323,62 +371,22 @@ export class MachineServiceControllerBase {
|
|
|
323
371
|
|
|
324
372
|
// Remove the service config
|
|
325
373
|
await serviceConfigs.delete(serviceId);
|
|
326
|
-
|
|
327
|
-
await serviceSwitchovers.delete(serviceId);
|
|
328
|
-
await this.notifyMachines([], serviceConfig.machineIds);
|
|
374
|
+
void this.notifyMachines([], getConfigMachineIds(normalizeServiceConfig(serviceConfig)));
|
|
329
375
|
}
|
|
330
376
|
|
|
331
|
-
/**
|
|
332
|
-
public async
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
let serviceConfig = await serviceConfigs.get(serviceId);
|
|
337
|
-
if (!serviceConfig) {
|
|
338
|
-
throw new Error(`Service ${serviceId} does not exist`);
|
|
339
|
-
}
|
|
340
|
-
await pendingServiceConfigs.set(serviceId, config);
|
|
377
|
+
/** Cancels parameters that are scheduled to deploy (releaseTime in the future), reverting to what is currently live. Returns the cancelled config, so the caller can keep the edits (ex, back in the editor). */
|
|
378
|
+
public async cancelScheduledDeploy(serviceId: string): Promise<ServiceConfig> {
|
|
379
|
+
let record = await serviceConfigs.get(serviceId);
|
|
380
|
+
if (!record) {
|
|
381
|
+
throw new Error(`Service ${serviceId} does not exist`);
|
|
341
382
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
public async discardPendingServiceConfig(serviceId: string) {
|
|
347
|
-
await pendingServiceConfigs.delete(serviceId);
|
|
348
|
-
await serviceSwitchovers.delete(serviceId);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** Deploys the pending config: the new instances start at switchTime - overlapTime, the old instances are told they will shut down at switchTime, and at switchTime the pending config becomes live. */
|
|
352
|
-
public async scheduleSwitchover(config: {
|
|
353
|
-
serviceId: string;
|
|
354
|
-
switchTime: number;
|
|
355
|
-
overlapTime: number;
|
|
356
|
-
}) {
|
|
357
|
-
let pending = await pendingServiceConfigs.get(config.serviceId);
|
|
358
|
-
if (!pending) {
|
|
359
|
-
throw new Error(`No pending config for service ${config.serviceId}. Save changes first, then deploy them.`);
|
|
360
|
-
}
|
|
361
|
-
let live = await serviceConfigs.get(config.serviceId);
|
|
362
|
-
if (!live) {
|
|
363
|
-
throw new Error(`Service ${config.serviceId} does not exist`);
|
|
364
|
-
}
|
|
365
|
-
await serviceSwitchovers.set(config.serviceId, {
|
|
366
|
-
serviceId: config.serviceId,
|
|
367
|
-
switchTime: config.switchTime,
|
|
368
|
-
overlapTime: config.overlapTime,
|
|
369
|
-
scheduledAt: Date.now(),
|
|
370
|
-
});
|
|
371
|
-
await this.notifyMachines([...new Set([...live.machineIds, ...pending.machineIds])], []);
|
|
372
|
-
}
|
|
373
|
-
public async cancelSwitchover(serviceId: string) {
|
|
374
|
-
let live = await serviceConfigs.get(serviceId);
|
|
375
|
-
await serviceSwitchovers.delete(serviceId);
|
|
376
|
-
if (live) {
|
|
377
|
-
await this.notifyMachines(live.machineIds, []);
|
|
383
|
+
normalizeServiceConfig(record);
|
|
384
|
+
if (!record.parameters.releaseTime || Date.now() >= record.parameters.releaseTime || !record.oldParameters) {
|
|
385
|
+
throw new Error(`Service ${serviceId} has no scheduled deploy to cancel (it may have already gone live)`);
|
|
378
386
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return
|
|
387
|
+
await serviceConfigs.set(serviceId, { ...record, parameters: record.oldParameters, oldParameters: undefined });
|
|
388
|
+
void this.notifyMachines(getConfigMachineIds(record), []);
|
|
389
|
+
return stripReleaseState(record);
|
|
382
390
|
}
|
|
383
391
|
public async getServiceConfigType(): Promise<string> {
|
|
384
392
|
return extractType(module, "ServiceConfig");
|
|
@@ -552,10 +560,14 @@ export async function getEffectiveServiceConfigs(): Promise<ServiceConfig[]> {
|
|
|
552
560
|
}
|
|
553
561
|
}
|
|
554
562
|
|
|
555
|
-
return configs.map(config =>
|
|
556
|
-
|
|
557
|
-
machineIds
|
|
558
|
-
|
|
563
|
+
return configs.map(config => {
|
|
564
|
+
normalizeServiceConfig(config);
|
|
565
|
+
config.parameters.machineIds = (config.parameters.machineIds || []).filter(machineId => !disabledMachineIds.has(machineId));
|
|
566
|
+
if (config.oldParameters) {
|
|
567
|
+
config.oldParameters.machineIds = (config.oldParameters.machineIds || []).filter(machineId => !disabledMachineIds.has(machineId));
|
|
568
|
+
}
|
|
569
|
+
return config;
|
|
570
|
+
});
|
|
559
571
|
}
|
|
560
572
|
|
|
561
573
|
let deployWatchers = new Set<DeployProgress>();
|
|
@@ -607,12 +619,7 @@ export const MachineServiceController = getSyncedController(
|
|
|
607
619
|
setMachineConfig: {},
|
|
608
620
|
addServiceConfig: {},
|
|
609
621
|
setServiceConfigs: {},
|
|
610
|
-
|
|
611
|
-
getPendingServiceConfig: {},
|
|
612
|
-
discardPendingServiceConfig: {},
|
|
613
|
-
scheduleSwitchover: {},
|
|
614
|
-
cancelSwitchover: {},
|
|
615
|
-
getServiceSwitchover: {},
|
|
622
|
+
cancelScheduledDeploy: {},
|
|
616
623
|
getServiceConfigType: {},
|
|
617
624
|
deleteServiceConfig: {},
|
|
618
625
|
getGitInfo: {},
|
|
@@ -637,11 +644,8 @@ export const MachineServiceController = getSyncedController(
|
|
|
637
644
|
setMachineConfig: ["MachineConfig", "MachineConfigList"],
|
|
638
645
|
addServiceConfig: ["ServiceConfig", "ServiceConfigList"],
|
|
639
646
|
setServiceConfigs: ["ServiceConfig"],
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
scheduleSwitchover: ["ServiceSwitchover"],
|
|
643
|
-
cancelSwitchover: ["ServiceSwitchover"],
|
|
644
|
-
deleteServiceConfig: ["ServiceConfig", "ServiceConfigList", "PendingServiceConfig", "ServiceSwitchover"],
|
|
647
|
+
cancelScheduledDeploy: ["ServiceConfig"],
|
|
648
|
+
deleteServiceConfig: ["ServiceConfig", "ServiceConfigList"],
|
|
645
649
|
commitPushService: ["gitInfo", "ServiceConfig"],
|
|
646
650
|
commitPushAndPublishQuerysub: ["gitInfo", "ServiceConfig"],
|
|
647
651
|
deployFunctions: ["gitInfo"],
|
|
@@ -653,8 +657,6 @@ export const MachineServiceController = getSyncedController(
|
|
|
653
657
|
getMachineConfig: ["MachineConfig"],
|
|
654
658
|
getServiceList: ["ServiceConfigList"],
|
|
655
659
|
getServiceConfig: ["ServiceConfig"],
|
|
656
|
-
getPendingServiceConfig: ["PendingServiceConfig"],
|
|
657
|
-
getServiceSwitchover: ["ServiceSwitchover"],
|
|
658
660
|
getGitInfo: ["gitInfo"],
|
|
659
661
|
getPendingFunctions: ["gitInfo"],
|
|
660
662
|
getLiveFunctions: ["gitInfo"],
|
|
@@ -141,7 +141,7 @@ export class LogDatumRenderer extends qreact.Component<{
|
|
|
141
141
|
<div>{formatDateTime(datum.time)}</div>
|
|
142
142
|
</div>
|
|
143
143
|
{datum.__machineId && <MachineThreadInfo machineId={datum.__machineId} threadId={datum.__threadId} />}
|
|
144
|
-
<span className={css.ellipsis.flexFillWidth.colorhsl(0, 50, 50).boldStyle}>{mainMessage}</span>
|
|
144
|
+
<span className={css.ellipsis.flexFillWidth.colorhsl(0, 50, 50).boldStyle} title={mainMessage}>{mainMessage}</span>
|
|
145
145
|
</div>
|
|
146
146
|
|
|
147
147
|
{!this.props.inlineMode && this.state.expanded && (
|
|
@@ -34,11 +34,12 @@ let nextSeqNum = Math.floor(Date.now() / 1000);
|
|
|
34
34
|
export class TypedConfigEditor extends qreact.Component<TypedConfigEditorProps> {
|
|
35
35
|
state = t.state({
|
|
36
36
|
editorReady: t.type(false),
|
|
37
|
-
currentValue: t.type<any>(undefined)
|
|
38
37
|
});
|
|
39
38
|
|
|
40
39
|
private editor: any = null;
|
|
41
40
|
private model: any = null;
|
|
41
|
+
// The last value either typed into the editor or pushed into it (as 4-space JSON). A plain field (NOT state), so comparing/updating it can never trigger re-renders.
|
|
42
|
+
private lastKnownValueStr = "";
|
|
42
43
|
private uniqueId = nextSeqNum++;
|
|
43
44
|
|
|
44
45
|
private typeName = `Type${this.uniqueId}`;
|
|
@@ -83,10 +84,9 @@ export class TypedConfigEditor extends qreact.Component<TypedConfigEditorProps>
|
|
|
83
84
|
);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
private
|
|
87
|
+
private updateEditorContent(valueStr: string): void {
|
|
87
88
|
if (!this.model) return;
|
|
88
89
|
|
|
89
|
-
const valueStr = JSON.stringify(value, null, 4);
|
|
90
90
|
const content = `const ${this.valueName}: ${this.typeName} = ${valueStr}`;
|
|
91
91
|
|
|
92
92
|
// Only update if content has changed to avoid cursor jumping
|
|
@@ -96,15 +96,7 @@ export class TypedConfigEditor extends qreact.Component<TypedConfigEditorProps>
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
componentDidMount(): void {
|
|
99
|
-
this.
|
|
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
|
-
}
|
|
99
|
+
this.lastKnownValueStr = JSON.stringify(this.props.value ?? null, null, 4);
|
|
108
100
|
}
|
|
109
101
|
|
|
110
102
|
componentWillUnmount(): void {
|
|
@@ -121,6 +113,15 @@ export class TypedConfigEditor extends qreact.Component<TypedConfigEditorProps>
|
|
|
121
113
|
let valueName = this.valueName;
|
|
122
114
|
let typeDefinition = `type ${typeName} = ${this.props.typeDefinition}`;
|
|
123
115
|
|
|
116
|
+
// qreact has no componentDidUpdate, so external value changes (ex, the parent discarding edits) are detected here and pushed into the editor. Changes the user typed round-trip through onValueChange into lastKnownValueStr, so they compare equal and never clobber the editor.
|
|
117
|
+
let propsValueStr = JSON.stringify(this.props.value ?? null, null, 4);
|
|
118
|
+
if (this.state.editorReady && this.lastKnownValueStr !== propsValueStr) {
|
|
119
|
+
this.lastKnownValueStr = propsValueStr;
|
|
120
|
+
Querysub.onCommitFinished(() => {
|
|
121
|
+
this.updateEditorContent(propsValueStr);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
124
125
|
return <div
|
|
125
126
|
key={"Editor-" + this.uniqueId}
|
|
126
127
|
ref2={async element => {
|
|
@@ -149,7 +150,7 @@ export class TypedConfigEditor extends qreact.Component<TypedConfigEditorProps>
|
|
|
149
150
|
this.model = window.monaco.editor.getModel(configUri);
|
|
150
151
|
if (this.model) {
|
|
151
152
|
// Update existing model content
|
|
152
|
-
this.
|
|
153
|
+
this.updateEditorContent(valueStr);
|
|
153
154
|
} else {
|
|
154
155
|
// Create new model
|
|
155
156
|
this.model = window.monaco.editor.createModel(
|
|
@@ -176,9 +177,7 @@ export class TypedConfigEditor extends qreact.Component<TypedConfigEditorProps>
|
|
|
176
177
|
try {
|
|
177
178
|
// Try to parse the JSON
|
|
178
179
|
const parsedValue = JSON.parse(configMatch[1]);
|
|
179
|
-
|
|
180
|
-
this.state.currentValue = parsedValue;
|
|
181
|
-
});
|
|
180
|
+
this.lastKnownValueStr = JSON.stringify(parsedValue, null, 4);
|
|
182
181
|
|
|
183
182
|
Querysub.commit(() => {
|
|
184
183
|
this.props.onValueChange?.(parsedValue);
|
|
@@ -189,7 +188,7 @@ export class TypedConfigEditor extends qreact.Component<TypedConfigEditorProps>
|
|
|
189
188
|
}
|
|
190
189
|
});
|
|
191
190
|
|
|
192
|
-
Querysub.
|
|
191
|
+
Querysub.commit(() => {
|
|
193
192
|
this.state.editorReady = true;
|
|
194
193
|
});
|
|
195
194
|
});
|
package/src/misc.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canHaveChildren } from "socket-function/src/types";
|
|
2
2
|
import { delay } from "socket-function/src/batching";
|
|
3
|
+
import { timeInDay, timeInSecond } from "socket-function/src/misc";
|
|
3
4
|
|
|
4
5
|
// TIMING: About 20MB/s
|
|
5
6
|
export function createRandomText(count: number): string {
|
|
@@ -197,6 +198,35 @@ export function maybeUndefined<T>(value: T): T | undefined {
|
|
|
197
198
|
return value;
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
const PRECISE_TIMEOUT_MAX_SLEEP = timeInDay;
|
|
202
|
+
const PRECISE_TIMEOUT_MIN_SLEEP = timeInSecond;
|
|
203
|
+
/** Fires the callback at the given absolute time. A single setTimeout drifts over long waits (and overflows past ~24.8 days), so this re-arms, sleeping 90% of the remaining time each wake (re-reading the clock), and so hits the target within about a second even over multi-day waits. Returns a cancel function. */
|
|
204
|
+
export function setPreciseTimeout(config: {
|
|
205
|
+
time: number;
|
|
206
|
+
callback: () => void;
|
|
207
|
+
}): () => void {
|
|
208
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
209
|
+
let cancelled = false;
|
|
210
|
+
const arm = () => {
|
|
211
|
+
if (cancelled) return;
|
|
212
|
+
let remaining = config.time - Date.now();
|
|
213
|
+
if (remaining <= 0) {
|
|
214
|
+
timer = undefined;
|
|
215
|
+
config.callback();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
timer = setTimeout(arm, Math.min(Math.max(remaining * 0.9, PRECISE_TIMEOUT_MIN_SLEEP), PRECISE_TIMEOUT_MAX_SLEEP));
|
|
219
|
+
};
|
|
220
|
+
arm();
|
|
221
|
+
return () => {
|
|
222
|
+
cancelled = true;
|
|
223
|
+
if (timer) {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
timer = undefined;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
200
230
|
/** Runs `callback` for every item, but staggers the calls evenly across `totalTime` instead of
|
|
201
231
|
* firing them all at once, and awaits all of them before returning. Used inside a poll loop
|
|
202
232
|
* (with totalTime = the poll interval) so fanning out to many nodes doesn't spike — otherwise
|