querysub 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- package/yarnSpec.txt +56 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { css } from "typesafecss";
|
|
2
|
+
import { qreact } from "../4-dom/qreact";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
import { formatDateTime, formatPercent } from "socket-function/src/formatting/format";
|
|
5
|
+
|
|
6
|
+
// TODO: When multiple of the previous range are selected, show two groups of ranges in the next
|
|
7
|
+
// group, AT LEAST.
|
|
8
|
+
// - Maybe add space so they can line up with the selector they are part of?
|
|
9
|
+
// - And somehow support this if it affects more levels. As in, 1 hour in one year, and a hour in another year,
|
|
10
|
+
// which would cover 2 years, 3 months, 2 days, and 2 hours
|
|
11
|
+
// - Maybe have a visual indication that a range opens up to the subvalues, centering them under it.
|
|
12
|
+
// - If the group isn't on the end, collapse the sub values by default. BUT, allow opening it up?
|
|
13
|
+
// - Allow clicking on the start/end of groups to select the start/end of the selected range?
|
|
14
|
+
// - Show a hover indicator above it that looks like it is holding the data
|
|
15
|
+
// TODO: Support multi-selection
|
|
16
|
+
export class TimeRangeSelector extends qreact.Component<{
|
|
17
|
+
start: { value: number | undefined };
|
|
18
|
+
end: { value: number | undefined };
|
|
19
|
+
defaultStart: number;
|
|
20
|
+
defaultEnd: number;
|
|
21
|
+
|
|
22
|
+
firstTime: number;
|
|
23
|
+
lastTime: number;
|
|
24
|
+
|
|
25
|
+
getIncrementLabel?: (range: { start: number; end: number; title: string }) => { title: string; };
|
|
26
|
+
}> {
|
|
27
|
+
render() {
|
|
28
|
+
let { start, end, defaultStart, defaultEnd, firstTime, lastTime } = this.props;
|
|
29
|
+
|
|
30
|
+
let startTime = start.value || defaultStart;
|
|
31
|
+
let endTime = end.value || defaultEnd;
|
|
32
|
+
if (endTime < startTime) startTime = endTime;
|
|
33
|
+
startTime = Math.max(startTime, firstTime);
|
|
34
|
+
endTime = Math.min(endTime, lastTime);
|
|
35
|
+
|
|
36
|
+
let increment = guessIncrement(startTime, endTime);
|
|
37
|
+
|
|
38
|
+
let getIncrementLabel = this.props.getIncrementLabel || (({ title }) => ({ title, }));
|
|
39
|
+
|
|
40
|
+
const viewTypes: IncrementType[] = [
|
|
41
|
+
"year",
|
|
42
|
+
"month",
|
|
43
|
+
"day",
|
|
44
|
+
"hour",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
let incrementRanges: {
|
|
48
|
+
incrementType: IncrementType;
|
|
49
|
+
ranges: { start: number; end: number; title: string }[];
|
|
50
|
+
}[] = [];
|
|
51
|
+
let lastSelectedRange: { start: number; end: number; } | undefined;
|
|
52
|
+
for (let type of viewTypes) {
|
|
53
|
+
let parentType = incrementUps[type];
|
|
54
|
+
let curFirstTime = getStartOfIncrement(Math.max(lastSelectedRange?.start || 0, firstTime), parentType);
|
|
55
|
+
let curLastTime = getEndOfIncrement(curFirstTime, parentType);
|
|
56
|
+
let ranges = getIncrementSubRangesBase(curFirstTime, curLastTime, type).ranges;
|
|
57
|
+
incrementRanges.push({
|
|
58
|
+
incrementType: type,
|
|
59
|
+
ranges: ranges.map(range => {
|
|
60
|
+
let title = formatSingleIncrement(range.start, type);
|
|
61
|
+
if (type === "hour") {
|
|
62
|
+
function formatHourShort(time: number): string {
|
|
63
|
+
// 12 PM
|
|
64
|
+
let d = new Date(time);
|
|
65
|
+
let hours = d.getHours();
|
|
66
|
+
let ampm = hours < 12 ? "AM" : "PM";
|
|
67
|
+
hours = hours % 12;
|
|
68
|
+
if (hours === 0) hours = 12;
|
|
69
|
+
return `${hours} ${ampm}`;
|
|
70
|
+
}
|
|
71
|
+
title = formatHourShort(range.start);
|
|
72
|
+
}
|
|
73
|
+
return { ...range, title };
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Drill down into the select range (or first if none are selected)
|
|
78
|
+
let selectedRange = ranges.find(range => startTime >= range.start && startTime < range.end) || ranges[0];
|
|
79
|
+
lastSelectedRange = selectedRange;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let incrementSubSize = incrementMedianSize(incrementSubs[increment].subType);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className={css.vbox(4)}>
|
|
86
|
+
{incrementRanges.map(({ incrementType, ranges }) => (
|
|
87
|
+
<div className={css.hbox(2).wrap}>
|
|
88
|
+
{ranges.map(range => {
|
|
89
|
+
// If it overlaps the selected range, select it
|
|
90
|
+
let selected = startTime < range.end && endTime > range.start;
|
|
91
|
+
|
|
92
|
+
let startF = (startTime - range.start) / (range.end - range.start);
|
|
93
|
+
let endF = (endTime - range.start) / (range.end - range.start);
|
|
94
|
+
|
|
95
|
+
let { title } = getIncrementLabel(range);
|
|
96
|
+
let renderRange = (startF: number, endF: number) => {
|
|
97
|
+
return <div className={
|
|
98
|
+
css.absolute.hsl(0, 0, 0)
|
|
99
|
+
.opacity(0.5)
|
|
100
|
+
.top0
|
|
101
|
+
.fillHeight
|
|
102
|
+
.left(`${startF * 100}%`)
|
|
103
|
+
.width(`${(endF - startF) * 100}%`)
|
|
104
|
+
} />;
|
|
105
|
+
};
|
|
106
|
+
return <Button
|
|
107
|
+
className={css.overflowHidden + (!selected && css.opacity(0.6))}
|
|
108
|
+
hue={
|
|
109
|
+
selected && 110
|
|
110
|
+
|| undefined
|
|
111
|
+
}
|
|
112
|
+
onClick={() => {
|
|
113
|
+
start.value = range.start;
|
|
114
|
+
end.value = range.end;
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<div className={css.relative}>{title}</div>
|
|
118
|
+
{renderRange(0, startF)}
|
|
119
|
+
{renderRange(endF, 1)}
|
|
120
|
+
</Button>;
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function guessIncrement(startTime: number, endTime: number): IncrementType {
|
|
130
|
+
let duration = endTime - startTime;
|
|
131
|
+
return incrementOrder.slice().reverse().find(type => incrementMedianSize(type) * 1.5 > duration) ?? incrementOrder[incrementOrder.length - 1];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type IncrementType = "second" | "minute" | "minute2" | "second2" | "hour" | "hour6" | "day" | "week" | "week2" | "month" | "year" | "decade";
|
|
135
|
+
let incrementOrder: IncrementType[] = [
|
|
136
|
+
"decade", "year", "month", "day", "hour", "hour6",
|
|
137
|
+
"week", "week2", "minute", "minute2", "second", "second2",
|
|
138
|
+
];
|
|
139
|
+
export let incrementSubs: {
|
|
140
|
+
[key in IncrementType]: { type: IncrementType; subType: IncrementType; }
|
|
141
|
+
} = {
|
|
142
|
+
second: { type: "second", subType: "second" },
|
|
143
|
+
second2: { type: "second2", subType: "second2" },
|
|
144
|
+
minute: { type: "minute", subType: "second" },
|
|
145
|
+
minute2: { type: "minute2", subType: "second2" },
|
|
146
|
+
hour: { type: "hour", subType: "minute2" },
|
|
147
|
+
hour6: { type: "hour6", subType: "hour" },
|
|
148
|
+
day: { type: "day", subType: "hour" },
|
|
149
|
+
week: { type: "week", subType: "hour6" },
|
|
150
|
+
week2: { type: "week2", subType: "day" },
|
|
151
|
+
month: { type: "month", subType: "day" },
|
|
152
|
+
year: { type: "year", subType: "week2" },
|
|
153
|
+
decade: { type: "decade", subType: "year" },
|
|
154
|
+
};
|
|
155
|
+
export let incrementUps: { [key in IncrementType]: IncrementType } = {
|
|
156
|
+
second: "minute",
|
|
157
|
+
second2: "minute",
|
|
158
|
+
minute: "hour",
|
|
159
|
+
minute2: "hour",
|
|
160
|
+
hour: "day",
|
|
161
|
+
hour6: "day",
|
|
162
|
+
day: "week",
|
|
163
|
+
week: "month",
|
|
164
|
+
week2: "month",
|
|
165
|
+
month: "year",
|
|
166
|
+
year: "decade",
|
|
167
|
+
decade: "decade",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export function getStartOfIncrement(time: number, type: IncrementType): number {
|
|
171
|
+
let d = new Date(time);
|
|
172
|
+
function resetMilli() {
|
|
173
|
+
// Just use times, as Date incorrectly handle DST
|
|
174
|
+
d = new Date(+d - d.getMilliseconds());
|
|
175
|
+
}
|
|
176
|
+
function resetSeconds() {
|
|
177
|
+
resetMilli();
|
|
178
|
+
d = new Date(+d - d.getSeconds() * 1000);
|
|
179
|
+
}
|
|
180
|
+
function resetMinutes() {
|
|
181
|
+
resetSeconds();
|
|
182
|
+
d = new Date(+d - d.getMinutes() * 60000);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (type === "second") {
|
|
186
|
+
resetMilli();
|
|
187
|
+
} else if (type === "second2") {
|
|
188
|
+
resetMilli();
|
|
189
|
+
// Round to nearest 2 seconds
|
|
190
|
+
let seconds = d.getSeconds();
|
|
191
|
+
let half = seconds % 2 === 1;
|
|
192
|
+
if (half) {
|
|
193
|
+
d.setSeconds(seconds - 1);
|
|
194
|
+
}
|
|
195
|
+
} else if (type === "minute") {
|
|
196
|
+
resetSeconds();
|
|
197
|
+
} else if (type === "minute2") {
|
|
198
|
+
resetSeconds();
|
|
199
|
+
// Round to nearest 2 minutes
|
|
200
|
+
let minutes = d.getMinutes();
|
|
201
|
+
let half = minutes % 2 === 1;
|
|
202
|
+
if (half) {
|
|
203
|
+
d = new Date(+d - 60000);
|
|
204
|
+
}
|
|
205
|
+
} else if (type === "hour") {
|
|
206
|
+
resetMinutes();
|
|
207
|
+
} else if (type === "hour6") {
|
|
208
|
+
d.setHours(Math.floor(d.getHours() / 6) * 6);
|
|
209
|
+
resetMinutes();
|
|
210
|
+
} else if (type === "day") {
|
|
211
|
+
d.setHours(0);
|
|
212
|
+
resetMinutes();
|
|
213
|
+
} else if (type === "week") {
|
|
214
|
+
d.setHours(0);
|
|
215
|
+
resetMinutes();
|
|
216
|
+
d.setDate(d.getDate() - d.getDay());
|
|
217
|
+
} else if (type === "week2") {
|
|
218
|
+
// Only land on even weeks, numbering weeks from the epoch
|
|
219
|
+
let epoch = new Date(0);
|
|
220
|
+
let epochWeek = Math.floor(epoch.getTime() / (86400000 * 7));
|
|
221
|
+
let week = Math.floor(d.getTime() / (86400000 * 7));
|
|
222
|
+
let diff = week - epochWeek;
|
|
223
|
+
let even = diff % 2 === 0;
|
|
224
|
+
if (!even) diff++;
|
|
225
|
+
if (diff > 0) diff -= 2;
|
|
226
|
+
d.setTime(epochWeek + diff * 86400000 * 7);
|
|
227
|
+
d.setHours(0);
|
|
228
|
+
resetMinutes();
|
|
229
|
+
} else if (type === "month") {
|
|
230
|
+
d.setHours(0);
|
|
231
|
+
resetMinutes();
|
|
232
|
+
d.setDate(1);
|
|
233
|
+
} else if (type === "year") {
|
|
234
|
+
d.setHours(0);
|
|
235
|
+
resetMinutes();
|
|
236
|
+
d.setMonth(0);
|
|
237
|
+
d.setDate(1);
|
|
238
|
+
} else if (type === "decade") {
|
|
239
|
+
d.setHours(0);
|
|
240
|
+
resetMinutes();
|
|
241
|
+
d.setMonth(0);
|
|
242
|
+
d.setDate(1);
|
|
243
|
+
d.setFullYear(Math.floor(d.getFullYear() / 10) * 10);
|
|
244
|
+
} else {
|
|
245
|
+
let unhandled: never = type;
|
|
246
|
+
}
|
|
247
|
+
return d.getTime();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function getEndOfIncrement(time: number, type: IncrementType): number {
|
|
251
|
+
let start = getStartOfIncrement(time, type);
|
|
252
|
+
let next = getNextIncrement(start, type);
|
|
253
|
+
return next;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function incrementMedianSize(type: IncrementType): number {
|
|
257
|
+
if (type === "second") return 1000;
|
|
258
|
+
if (type === "second2") return 2000;
|
|
259
|
+
if (type === "minute") return 60000;
|
|
260
|
+
if (type === "minute2") return 60000 * 2;
|
|
261
|
+
if (type === "hour") return 3600000;
|
|
262
|
+
if (type === "hour6") return 3600000 * 6;
|
|
263
|
+
if (type === "day") return 86400000;
|
|
264
|
+
if (type === "week") return 86400000 * 7;
|
|
265
|
+
if (type === "week2") return 86400000 * 14;
|
|
266
|
+
if (type === "month") return 86400000 * 30;
|
|
267
|
+
if (type === "year") return 86400000 * 365;
|
|
268
|
+
if (type === "decade") return 86400000 * 365 * 10;
|
|
269
|
+
let unhandled: never = type;
|
|
270
|
+
throw new Error("Unhandled type: " + unhandled);
|
|
271
|
+
}
|
|
272
|
+
export function getNextIncrement(time: number, type: IncrementType): number {
|
|
273
|
+
let baseTime = time;
|
|
274
|
+
time = getStartOfIncrement(time, type);
|
|
275
|
+
time += incrementMedianSize(type) * 1.5;
|
|
276
|
+
time = getStartOfIncrement(time, type);
|
|
277
|
+
if (time <= baseTime) {
|
|
278
|
+
// Happens due to daylight savings time
|
|
279
|
+
time = getStartOfIncrement(baseTime, type);
|
|
280
|
+
time += incrementMedianSize(type) * 2.5;
|
|
281
|
+
time = getStartOfIncrement(time, type);
|
|
282
|
+
}
|
|
283
|
+
if (time <= baseTime) {
|
|
284
|
+
// Time went backwards. We had issues related to DST here before, maybe this is similar?
|
|
285
|
+
debugger;
|
|
286
|
+
time = getStartOfIncrement(baseTime, type);
|
|
287
|
+
time += incrementMedianSize(type) * 2.5;
|
|
288
|
+
time = getStartOfIncrement(time, type);
|
|
289
|
+
}
|
|
290
|
+
return time;
|
|
291
|
+
}
|
|
292
|
+
export function getPrevIncrement(time: number, type: IncrementType): number {
|
|
293
|
+
time = getStartOfIncrement(time, type);
|
|
294
|
+
time--;
|
|
295
|
+
return getStartOfIncrement(time, type);
|
|
296
|
+
}
|
|
297
|
+
function hourShort(time: number): string {
|
|
298
|
+
// 12 PM
|
|
299
|
+
let d = new Date(time);
|
|
300
|
+
let hours = d.getHours();
|
|
301
|
+
let ampm = hours < 12 ? "AM" : "PM";
|
|
302
|
+
hours = hours % 12;
|
|
303
|
+
if (hours === 0) hours = 12;
|
|
304
|
+
return `${hours} ${ampm}`;
|
|
305
|
+
}
|
|
306
|
+
function timeOfDayTime(time: number): string {
|
|
307
|
+
let hours = new Date(time).getHours();
|
|
308
|
+
if (hours === 0) return "🌑"; //return "Midnight"; 🌃
|
|
309
|
+
if (hours === 6) return "🌅"; //return "Morning";
|
|
310
|
+
if (hours === 12) return "☀️"; //return "Noon";
|
|
311
|
+
if (hours === 18) return "🌇"; // return "Evening";
|
|
312
|
+
return hourShort(time);
|
|
313
|
+
}
|
|
314
|
+
function hourMinuteSecond(time: number): string {
|
|
315
|
+
// 12:00:00 PM
|
|
316
|
+
let d = new Date(time);
|
|
317
|
+
let hours = d.getHours();
|
|
318
|
+
let minutes = d.getMinutes();
|
|
319
|
+
let seconds = d.getSeconds();
|
|
320
|
+
let ampm = hours < 12 ? "AM" : "PM";
|
|
321
|
+
hours = hours % 12;
|
|
322
|
+
if (hours === 0) hours = 12;
|
|
323
|
+
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")} ${ampm}`;
|
|
324
|
+
}
|
|
325
|
+
let shortDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
326
|
+
let longDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
327
|
+
let shortMonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
328
|
+
let longMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
export function formatSingleIncrement(time: number, type: IncrementType, long?: "long"): string {
|
|
332
|
+
//time = getStartOfIncrement(time, type);
|
|
333
|
+
let d = new Date(time);
|
|
334
|
+
let days = long ? longDays : shortDays;
|
|
335
|
+
let months = long ? longMonths : shortMonths;
|
|
336
|
+
if (type === "second") return hourMinuteSecond(time);
|
|
337
|
+
if (type === "second2") return hourMinuteSecond(time);
|
|
338
|
+
if (type === "minute") return hourMinuteSecond(time);
|
|
339
|
+
if (type === "minute2") return formatSingleIncrement(time, "minute");
|
|
340
|
+
if (type === "hour") return hourMinuteSecond(time);
|
|
341
|
+
if (type === "hour6") {
|
|
342
|
+
return d.getDate() + " " + days[d.getDay()] + " " + timeOfDayTime(time);
|
|
343
|
+
}
|
|
344
|
+
if (type === "day") return d.getDate() + " " + days[d.getDay()];
|
|
345
|
+
// Month - Day
|
|
346
|
+
if (type === "week") return formatFullIncrement(time, "month") + " " + d.getDate();
|
|
347
|
+
if (type === "week2") return formatSingleIncrement(time, "week");
|
|
348
|
+
if (type === "month") return months[d.getMonth()];
|
|
349
|
+
if (type === "year") return d.getFullYear() + "";
|
|
350
|
+
if (type === "decade") return d.getFullYear() + "";
|
|
351
|
+
let unhandled: never = type;
|
|
352
|
+
throw new Error("Unhandled type: " + unhandled);
|
|
353
|
+
}
|
|
354
|
+
export function formatFullIncrementParts(time: number, type: IncrementType, long?: "long"): {
|
|
355
|
+
type: IncrementType;
|
|
356
|
+
value: string;
|
|
357
|
+
}[] {
|
|
358
|
+
let fullFormat = [
|
|
359
|
+
{ type: "year" as const, value: formatSingleIncrement(time, "year", long) },
|
|
360
|
+
{ type: "month" as const, value: formatSingleIncrement(time, "month", long) },
|
|
361
|
+
{ type: "day" as const, value: formatSingleIncrement(time, "day", long) },
|
|
362
|
+
{ type: "hour" as const, value: hourMinuteSecond(time) },
|
|
363
|
+
];
|
|
364
|
+
let count = fullFormat.length;
|
|
365
|
+
if (type === "second") count = 4;
|
|
366
|
+
if (type === "minute") count = 4;
|
|
367
|
+
if (type === "minute2") count = 4;
|
|
368
|
+
if (type === "hour") count = 4;
|
|
369
|
+
if (type === "hour6") count = 4;
|
|
370
|
+
if (type === "day") count = 3;
|
|
371
|
+
if (type === "week") count = 3;
|
|
372
|
+
if (type === "month") count = 2;
|
|
373
|
+
if (type === "year") count = 1;
|
|
374
|
+
if (type === "decade") count = 1;
|
|
375
|
+
return fullFormat.slice(0, count);
|
|
376
|
+
}
|
|
377
|
+
export function formatFullIncrement(time: number, type: IncrementType, long?: "long"): string {
|
|
378
|
+
return formatFullIncrementParts(time, type, long).map(x => x.value).join(" | ");
|
|
379
|
+
}
|
|
380
|
+
export function getIncrementSubRanges(time: number, type: IncrementType, subType = incrementSubs[type].subType): {
|
|
381
|
+
ranges: {
|
|
382
|
+
start: number;
|
|
383
|
+
end: number;
|
|
384
|
+
}[];
|
|
385
|
+
} {
|
|
386
|
+
let start = getStartOfIncrement(time, type);
|
|
387
|
+
let end = getNextIncrement(time, type);
|
|
388
|
+
return getIncrementSubRangesBase(start, end, subType);
|
|
389
|
+
}
|
|
390
|
+
export function getIncrementSubRangesBase(start: number, end: number, subType: IncrementType): {
|
|
391
|
+
ranges: {
|
|
392
|
+
start: number;
|
|
393
|
+
end: number;
|
|
394
|
+
}[];
|
|
395
|
+
} {
|
|
396
|
+
let cur = start;
|
|
397
|
+
let ranges: { start: number; end: number; }[] = [];
|
|
398
|
+
while (ranges.length < 240) {
|
|
399
|
+
let next = getNextIncrement(cur, subType);
|
|
400
|
+
ranges.push({ start: cur, end: next, });
|
|
401
|
+
cur = next;
|
|
402
|
+
if (cur >= end) break;
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
ranges,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// NOTE: This is basically a component, except it renders to the URL input.
|
|
2
|
+
|
|
3
|
+
import { deepCloneJSON, throttleFunction, timeInMinute } from "socket-function/src/misc";
|
|
4
|
+
import { isNode } from "typesafecss";
|
|
5
|
+
import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
|
|
6
|
+
import { atomicObjectRead, doAtomicWrites, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
|
|
7
|
+
import { syncSchema } from "../3-path-functions/syncSchema";
|
|
8
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
9
|
+
import { isDefined } from "../misc";
|
|
10
|
+
import { deserializeURLParam, serializeURLParam } from "./niceStringify";
|
|
11
|
+
import { logErrors } from "../errors";
|
|
12
|
+
import { canHaveChildren } from "socket-function/src/types";
|
|
13
|
+
import { URLOverride } from "./ATag";
|
|
14
|
+
|
|
15
|
+
export interface URLParam<T = unknown> {
|
|
16
|
+
value: T;
|
|
17
|
+
readonly urlKey: string;
|
|
18
|
+
default?: T;
|
|
19
|
+
reset(): void;
|
|
20
|
+
getOverride(value: T): URLOverride<T>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let localStorageKeys = new Set<string>();
|
|
24
|
+
|
|
25
|
+
declare global {
|
|
26
|
+
var url: { [key: string]: URLParam };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let globalURLLookup = globalThis.url || (globalThis.url = Object.create(null));
|
|
30
|
+
|
|
31
|
+
let loadSearchCache: { [key: string]: unknown } = Object.create(null);
|
|
32
|
+
if (!isNode()) {
|
|
33
|
+
loadSearchCache = parseSearchString(location.search);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export class URLParam<T = unknown> {
|
|
38
|
+
constructor(urlKey: string, defaultValue: T) {
|
|
39
|
+
return createURLSync(urlKey, defaultValue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** const myVariable = createURLSync("myvar", 0) */
|
|
43
|
+
export function createURLSync<T>(urlKey: string, defaultValue: T, config?: {
|
|
44
|
+
storage?: "url" | "localStorage";
|
|
45
|
+
}): URLParam<T> {
|
|
46
|
+
|
|
47
|
+
let prev = globalURLLookup[urlKey];
|
|
48
|
+
if (prev) {
|
|
49
|
+
// NOTE: This happens both during hot reloading AND when dynamically loading
|
|
50
|
+
// functions for input prediction.
|
|
51
|
+
return prev as any;
|
|
52
|
+
// if (isHotReloading?.()) return prev as any;
|
|
53
|
+
// throw new Error(`URL key ${urlKey} already exists`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let storage = config?.storage || "url";
|
|
57
|
+
// Clone the default, so it doesn't get changed
|
|
58
|
+
defaultValue = deepCloneJSON(defaultValue);
|
|
59
|
+
// undefined causes issues, making the returned value a proxy. It's better to make it null
|
|
60
|
+
if (defaultValue === undefined) {
|
|
61
|
+
defaultValue = null as any as T;
|
|
62
|
+
};
|
|
63
|
+
if (config?.storage === "localStorage") {
|
|
64
|
+
localStorageKeys.add(urlKey);
|
|
65
|
+
}
|
|
66
|
+
Querysub.localCommit(() => {
|
|
67
|
+
data().defaults[urlKey] = defaultValue;
|
|
68
|
+
if (!(urlKey in loadSearchCache)) {
|
|
69
|
+
data().params[urlKey] = deepCloneJSON(defaultValue);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
function deleteKeys(obj: any) {
|
|
73
|
+
if (!canHaveChildren(obj)) return;
|
|
74
|
+
for (let key in obj) {
|
|
75
|
+
deleteKeys(obj[key]);
|
|
76
|
+
delete obj[key];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// TODO: Check for duplicate keys. Which is hard, because we don't want hot reloading to give us false positives.
|
|
81
|
+
let param: URLParam<T> = {
|
|
82
|
+
urlKey,
|
|
83
|
+
default: defaultValue,
|
|
84
|
+
get value() {
|
|
85
|
+
if (!proxyWatcher.inWatcher()) {
|
|
86
|
+
// NOTE: This makes async functions a bit easier, although... async functions
|
|
87
|
+
// are almost never needed.
|
|
88
|
+
return Querysub.localRead(() => {
|
|
89
|
+
return data().params[urlKey] as T;
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
return data().params[urlKey] as T;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
set value(value: T) {
|
|
96
|
+
if ((value === undefined || value === null) && storage === "url") {
|
|
97
|
+
param.reset();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Recursively delete previous values, otherwise we will be merging values
|
|
101
|
+
deleteKeys(data().params[urlKey]);
|
|
102
|
+
data().params[urlKey] = value;
|
|
103
|
+
},
|
|
104
|
+
reset() {
|
|
105
|
+
deleteKeys(data().params[urlKey]);
|
|
106
|
+
data().params[urlKey] = deepCloneJSON(defaultValue);
|
|
107
|
+
},
|
|
108
|
+
getOverride(value: T) {
|
|
109
|
+
return { param, value, };
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
globalURLLookup[urlKey] = {
|
|
113
|
+
urlKey,
|
|
114
|
+
get value() {
|
|
115
|
+
return Querysub.localRead(() => param.value);
|
|
116
|
+
},
|
|
117
|
+
set value(value: T) {
|
|
118
|
+
Querysub.localCommit(() => param.value = value);
|
|
119
|
+
},
|
|
120
|
+
reset() {
|
|
121
|
+
Querysub.localCommit(() => param.reset());
|
|
122
|
+
},
|
|
123
|
+
getOverride(value: T) {
|
|
124
|
+
return { param, value, };
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return param;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// TODO: Support pathname keys (ex /name-value)
|
|
131
|
+
const { data } = syncSchema<{
|
|
132
|
+
params: {
|
|
133
|
+
[key: string]: unknown;
|
|
134
|
+
};
|
|
135
|
+
defaults: {
|
|
136
|
+
[key: string]: unknown;
|
|
137
|
+
};
|
|
138
|
+
}>()({
|
|
139
|
+
domainName: LOCAL_DOMAIN,
|
|
140
|
+
functions: {},
|
|
141
|
+
module,
|
|
142
|
+
moduleId: "urlSync",
|
|
143
|
+
permissions: {},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
function syncStateToURL() {
|
|
147
|
+
let targetURL = document.location.href;
|
|
148
|
+
const onTargetURLUpdated = throttleFunction(2000, () => {
|
|
149
|
+
if (targetURL === document.location.href) return;
|
|
150
|
+
window.history.pushState({}, "", targetURL);
|
|
151
|
+
});
|
|
152
|
+
Querysub.createWriteWatcher(function syncStateToURL() {
|
|
153
|
+
let urlObj = new URL(document.location.href);
|
|
154
|
+
urlObj.search = encodeSearchString(data().params, data().defaults);
|
|
155
|
+
targetURL = urlObj.toString();
|
|
156
|
+
logErrors(onTargetURLUpdated());
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function loadParamsFromURL() {
|
|
161
|
+
Querysub.localCommit(function loadParamsFromURL() {
|
|
162
|
+
let newUrlValues = Object.entries(parseSearchString(document.location.search));
|
|
163
|
+
for (let [key, value] of newUrlValues) {
|
|
164
|
+
if ((value === undefined || value === null)) {
|
|
165
|
+
value = data().defaults[key];
|
|
166
|
+
}
|
|
167
|
+
data().params[key] = value;
|
|
168
|
+
}
|
|
169
|
+
let newKeys = new Set(newUrlValues.map(([key]) => key));
|
|
170
|
+
for (let key of Object.keys(data().params)) {
|
|
171
|
+
if (localStorageKeys.has(key)) continue;
|
|
172
|
+
if (!newKeys.has(key)) {
|
|
173
|
+
data().params[key] = data().defaults[key];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function syncStateToStorage() {
|
|
180
|
+
Querysub.createWriteWatcher(function syncStateToURL() {
|
|
181
|
+
for (let [key, value] of Object.entries(data().params)) {
|
|
182
|
+
if (localStorageKeys.has(key)) {
|
|
183
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function loadFromLocalStorage() {
|
|
189
|
+
Querysub.localCommit(function loadParamsFromURL() {
|
|
190
|
+
for (let key of localStorageKeys) {
|
|
191
|
+
let value = localStorage.getItem(key);
|
|
192
|
+
if (value === null) continue;
|
|
193
|
+
data().params[key] = JSON.parse(value) ?? data().defaults[key];
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function watchURLForUpdates() {
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
200
|
+
let pushState = history.pushState.bind(history);
|
|
201
|
+
history.pushState = function (...args: any[]) {
|
|
202
|
+
(pushState as any)(...args);
|
|
203
|
+
loadParamsFromURL();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
window.addEventListener("popstate", () => {
|
|
207
|
+
loadParamsFromURL();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Load after startup (so localStorage is populated)
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
loadFromLocalStorage();
|
|
213
|
+
syncStateToStorage();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Eh... I'm not sure how else to do this. We have to poll, as it might change in
|
|
217
|
+
// another tab and we will forever be out of sync
|
|
218
|
+
setInterval(() => {
|
|
219
|
+
loadFromLocalStorage();
|
|
220
|
+
syncStateToStorage();
|
|
221
|
+
}, timeInMinute);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!isNode()) {
|
|
225
|
+
loadParamsFromURL();
|
|
226
|
+
watchURLForUpdates();
|
|
227
|
+
syncStateToURL();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function parseSearchString(search: string): { [key: string]: unknown } {
|
|
231
|
+
if (search.startsWith("?")) {
|
|
232
|
+
search = search.slice(1);
|
|
233
|
+
}
|
|
234
|
+
if (!search) return Object.create(null);
|
|
235
|
+
let parts = search.split("&");
|
|
236
|
+
let output: { [key: string]: string | undefined } = Object.create(null);
|
|
237
|
+
for (let part of parts) {
|
|
238
|
+
let equalIndex = part.indexOf("=");
|
|
239
|
+
if (equalIndex === -1) {
|
|
240
|
+
output[decodeURIComponent(part)] = undefined;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
let key = part.slice(0, equalIndex);
|
|
244
|
+
let value = part.slice(equalIndex + 1);
|
|
245
|
+
output[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
246
|
+
}
|
|
247
|
+
for (let [key, value] of Object.entries(output)) {
|
|
248
|
+
output[key] = deserializeURLParam(value);
|
|
249
|
+
}
|
|
250
|
+
return output;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function encodeSearchString(parts: { [key: string]: unknown }, defaults: { [key: string]: unknown } = {}) {
|
|
254
|
+
let encodedParts = Object.entries(parts).map(([key, value]) => {
|
|
255
|
+
if (localStorageKeys.has(key)) return undefined;
|
|
256
|
+
let valueSerialized = serializeURLParam(value);
|
|
257
|
+
if (valueSerialized === serializeURLParam(defaults[key])) return undefined;
|
|
258
|
+
if (valueSerialized === undefined) return key;
|
|
259
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(valueSerialized)}`;
|
|
260
|
+
}).filter(isDefined);
|
|
261
|
+
encodedParts.sort((a, b) => a.length - b.length);
|
|
262
|
+
return encodedParts.join("&");
|
|
263
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { css } from "../../src/4-dom/css";
|
|
2
|
+
|
|
3
|
+
export const redButton = css.hsl(0, 75, 50).bord(1, "hsl(0, 75%, 75%)").background("hsl(0, 75%, 75%)", "hover");
|
|
4
|
+
export const yellowButton = css.hsl(50, 90, 50).bord(1, "hsl(40, 75%, 75%)").color("hsl(0, 0%, 16%)!important").background("hsl(40, 75%, 75%)", "hover");
|
|
5
|
+
export const greenButton = css.hsl(110, 65, 45).bord(1, { h: 110, s: 65, l: 75 }).background("hsl(110, 65%, 90%)", "hover");
|
|
6
|
+
|
|
7
|
+
export const errorMessage = css.hsl(0, 75, 50).color("white")
|
|
8
|
+
.padding("4px 6px", "soft")
|
|
9
|
+
.whiteSpace("pre-wrap").display("inline-block", "soft")
|
|
10
|
+
;
|
|
11
|
+
export const warnMessage = css.hsl(50, 75, 50).color("hsl(0, 0%, 7%)", "important", "soft")
|
|
12
|
+
.padding("4px 6px", "soft")
|
|
13
|
+
.whiteSpace("pre-wrap").display("inline-block", "soft")
|
|
14
|
+
;
|