homebridge-plugin-utils 1.35.0 → 2.0.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/README.md +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +94 -45
- package/dist/backpressure.js +229 -93
- package/dist/backpressure.js.map +1 -1
- package/dist/cli/index.d.ts +95 -0
- package/dist/cli/index.js +355 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -262
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +49 -2
- package/dist/ffmpeg/fmp4.js +47 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
- package/dist/ffmpeg/mp4-assembler.js +387 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +142 -96
- package/dist/ffmpeg/process.js +406 -278
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -567
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +144 -0
- package/dist/ffmpeg/recording-process-double.js +168 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +473 -251
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -2
- package/dist/ffmpeg/settings.js +20 -5
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -150
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +677 -0
- package/dist/ui/webUi.mjs +192 -87
- package/dist/util.d.ts +602 -45
- package/dist/util.js +783 -78
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure join of a REST history tail with a socket-seeded live buffer.
|
|
3
|
+
*
|
|
4
|
+
* The `follow-history` mode pulls two views of the same log: the REST whole-file download (history, exact but a one-time snapshot) and the socket's live stream, which
|
|
5
|
+
* begins with a ~500-line seed that overlaps the END of the history and then continues with genuinely new lines. {@link stitchLive} joins them at their overlap so the
|
|
6
|
+
* output reads as one continuous log: all of history, then exactly the live lines that history did not already contain.
|
|
7
|
+
*
|
|
8
|
+
* The join's correctness guarantee is asymmetric and deliberately so: it NEVER drops a distinct live line, at the cost of possibly emitting a bounded run of duplicate
|
|
9
|
+
* lines at the seam. The hazard is repeated/identical lines - when several adjacent lines share the same text, the longest suffix-equals-prefix match could pair a
|
|
10
|
+
* history line with a live line that is actually a new occurrence and silently swallow it. To avoid that, the join chooses the MINIMAL overlap among the valid matches
|
|
11
|
+
* (keeping the most live content), accepting bounded duplicate chatter rather than risking silent loss. Equality is by normalized {@link LogRecord.raw}. When no overlap
|
|
12
|
+
* is found at all, a single visible {@link gapMarker} record is emitted between history and live so the boundary discontinuity is never hidden.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import type { LogRecord } from "./types.ts";
|
|
17
|
+
/**
|
|
18
|
+
* Build the gap-marker record emitted between history and live when no overlap can be found.
|
|
19
|
+
*
|
|
20
|
+
* The marker is a real {@link LogRecord} (not a side channel) so it flows through the same iteration, filtering, and formatting as any other line - a consumer never has
|
|
21
|
+
* to special-case it, and it is plainly visible in every output mode. Its `level` is `null` (it is not a severity-bearing line), its `plugin`/`timestamp` are `null`,
|
|
22
|
+
* and its `message`/`raw` carry the human-readable discontinuity notice.
|
|
23
|
+
*
|
|
24
|
+
* @returns A fresh gap-marker {@link LogRecord}.
|
|
25
|
+
*
|
|
26
|
+
* @category Log Client
|
|
27
|
+
*/
|
|
28
|
+
export declare function gapMarker(): LogRecord;
|
|
29
|
+
/**
|
|
30
|
+
* Options for {@link stitchLive}.
|
|
31
|
+
*
|
|
32
|
+
* @property maxOverlap - The maximum number of trailing-history / leading-live records to consider when searching for the overlap, bounding the search to the seed
|
|
33
|
+
* window. Defaults to the full length of the shorter side. Capping this keeps the join's cost proportional to the seed size rather than to the
|
|
34
|
+
* (potentially multi-MB) history.
|
|
35
|
+
*
|
|
36
|
+
* @category Log Client
|
|
37
|
+
*/
|
|
38
|
+
export interface StitchOptions {
|
|
39
|
+
readonly maxOverlap?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Join a history tail and a live buffer at their overlap.
|
|
43
|
+
*
|
|
44
|
+
* Searches for valid overlap lengths `k` (where history's last `k` records equal live's first `k` records, by normalized `raw`) within the bounded window, then:
|
|
45
|
+
*
|
|
46
|
+
* - If the MAXIMAL valid overlap covers all of `live`, every live line is already in history (total overlap): the result is `history` unchanged.
|
|
47
|
+
* - Otherwise, if any overlap is valid, the join uses the MINIMAL valid overlap and returns `history` followed by `live` from that offset, so every distinct live line
|
|
48
|
+
* survives even when repeated lines make a longer match look valid.
|
|
49
|
+
* - If no head overlap is valid but history's tail appears as a contiguous block somewhere inside `live` (the within-buffer case, e.g. a small `-n N` whose live seed
|
|
50
|
+
* reaches further back than the requested window), the join locates that block and continues past it - returning `history` followed by `live` from that point, dropping
|
|
51
|
+
* the older seed prefix that predates the window.
|
|
52
|
+
* - If neither a head overlap nor a within-buffer alignment is found, the result is `history`, a single {@link gapMarker}, then all of `live`, so the discontinuity is
|
|
53
|
+
* visible and no live line is dropped.
|
|
54
|
+
*
|
|
55
|
+
* Either input being empty short-circuits: an empty `live` returns `history`, and an empty `history` returns `live` (nothing to align against, no marker needed).
|
|
56
|
+
*
|
|
57
|
+
* @param history - The trailing history records, in chronological order (oldest first).
|
|
58
|
+
* @param live - The seed-plus-live records, in chronological order (oldest first); its leading records are the seed that overlaps history's tail.
|
|
59
|
+
* @param options - Optional bounds. See {@link StitchOptions}.
|
|
60
|
+
*
|
|
61
|
+
* @returns The joined record list.
|
|
62
|
+
*
|
|
63
|
+
* @category Log Client
|
|
64
|
+
*/
|
|
65
|
+
export declare function stitchLive(history: readonly LogRecord[], live: readonly LogRecord[], options?: StitchOptions): LogRecord[];
|
|
66
|
+
/**
|
|
67
|
+
* Thin coordinator that joins an already-collected history tail and live buffer via {@link stitchLive}.
|
|
68
|
+
*
|
|
69
|
+
* This is the composition seam the client uses for `follow-history`: it takes the already-materialized history tail and the already-buffered seed-plus-live records and
|
|
70
|
+
* defers entirely to {@link stitchLive} for the join policy. It exists so callers have one named entry point for "merge history then live" rather than re-deriving the
|
|
71
|
+
* call, and so the join policy lives in exactly one place. It performs no I/O of its own - the caller is responsible for having collected both sides (the socket seeds
|
|
72
|
+
* into a bounded ring and the REST download streams into records before this is called).
|
|
73
|
+
*
|
|
74
|
+
* @param history - The trailing history records, oldest first.
|
|
75
|
+
* @param live - The seed-plus-live records, oldest first.
|
|
76
|
+
* @param options - Optional bounds forwarded to {@link stitchLive}.
|
|
77
|
+
*
|
|
78
|
+
* @returns The joined record list.
|
|
79
|
+
*
|
|
80
|
+
* @category Log Client
|
|
81
|
+
*/
|
|
82
|
+
export declare function mergeHistoryThenLive(history: readonly LogRecord[], live: readonly LogRecord[], options?: StitchOptions): LogRecord[];
|
|
83
|
+
//# sourceMappingURL=stitch.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* logclient/stitch.ts: Pure history-plus-live join for the follow-history mode.
|
|
4
|
+
*/
|
|
5
|
+
// The sentinel text carried by the gap marker's `raw`/`message`. Distinct, unambiguous, and unlikely to collide with a real log line so a downstream consumer (or a
|
|
6
|
+
// human reading `--raw` output) can recognize the discontinuity. Lives here as the single source of truth for the marker wording.
|
|
7
|
+
const GAP_MARKER_TEXT = "--- log history and live stream could not be aligned; some lines may be missing or duplicated ---";
|
|
8
|
+
/**
|
|
9
|
+
* Build the gap-marker record emitted between history and live when no overlap can be found.
|
|
10
|
+
*
|
|
11
|
+
* The marker is a real {@link LogRecord} (not a side channel) so it flows through the same iteration, filtering, and formatting as any other line - a consumer never has
|
|
12
|
+
* to special-case it, and it is plainly visible in every output mode. Its `level` is `null` (it is not a severity-bearing line), its `plugin`/`timestamp` are `null`,
|
|
13
|
+
* and its `message`/`raw` carry the human-readable discontinuity notice.
|
|
14
|
+
*
|
|
15
|
+
* @returns A fresh gap-marker {@link LogRecord}.
|
|
16
|
+
*
|
|
17
|
+
* @category Log Client
|
|
18
|
+
*/
|
|
19
|
+
export function gapMarker() {
|
|
20
|
+
return { level: null, message: GAP_MARKER_TEXT, plugin: null, raw: GAP_MARKER_TEXT, timestamp: null };
|
|
21
|
+
}
|
|
22
|
+
// Test whether the last `k` records of `history` equal the first `k` records of `live`, comparing by normalized `raw`. This is the overlap predicate the join searches
|
|
23
|
+
// over; it walks both windows forward from their respective anchor points (history's trailing-window start, live's head) so the comparison aligns the seam correctly.
|
|
24
|
+
function overlapsAt(history, live, k) {
|
|
25
|
+
const base = history.length - k;
|
|
26
|
+
for (let index = 0; index < k; index++) {
|
|
27
|
+
// `history[base + index]` walks forward from the start of history's trailing window; `live[index]` walks forward from the start of live. A single mismatch disproves
|
|
28
|
+
// this overlap length. The `noUncheckedIndexedAccess`-safe reads use optional chaining on `raw`; out-of-range would yield `undefined` on both sides, but `k` is
|
|
29
|
+
// always within bounds here by construction (`1 <= k <= min(history.length, live.length)`).
|
|
30
|
+
if (history[base + index]?.raw !== live[index]?.raw) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
// Find the leftmost end-index in `live` at which history's last `k` records occur as a contiguous block (compared by normalized `raw`), or -1 when no such block exists.
|
|
37
|
+
// This is the within-buffer counterpart to {@link overlapsAt}: where that anchors at live's HEAD, this locates history's tail ANYWHERE in live - which is what
|
|
38
|
+
// the small-`-n` case needs, where the seed reaches further back than the requested history window.
|
|
39
|
+
function alignedEnd(history, live, k) {
|
|
40
|
+
const tail = history.slice(history.length - k);
|
|
41
|
+
for (let end = k - 1; end < live.length; end++) {
|
|
42
|
+
const start = end - k + 1;
|
|
43
|
+
if (tail.every((record, index) => record.raw === live[start + index]?.raw)) {
|
|
44
|
+
return end;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
49
|
+
// Locate history WITHIN the live buffer when it does not overlap live's head. Find the LONGEST suffix of history present as a contiguous block in live (longest is the
|
|
50
|
+
// most confident anchor against repeats), take its leftmost occurrence (keeping the most live), and return history then the live lines after that block - so the seed
|
|
51
|
+
// lines that predate the requested window are dropped while no genuinely new line is lost. Returns null when no suffix of history appears in live.
|
|
52
|
+
function stitchWithin(history, live, bound) {
|
|
53
|
+
for (let k = Math.min(history.length, bound); k >= 1; k--) {
|
|
54
|
+
const end = alignedEnd(history, live, k);
|
|
55
|
+
if (end !== -1) {
|
|
56
|
+
return [...history, ...live.slice(end + 1)];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Join a history tail and a live buffer at their overlap.
|
|
63
|
+
*
|
|
64
|
+
* Searches for valid overlap lengths `k` (where history's last `k` records equal live's first `k` records, by normalized `raw`) within the bounded window, then:
|
|
65
|
+
*
|
|
66
|
+
* - If the MAXIMAL valid overlap covers all of `live`, every live line is already in history (total overlap): the result is `history` unchanged.
|
|
67
|
+
* - Otherwise, if any overlap is valid, the join uses the MINIMAL valid overlap and returns `history` followed by `live` from that offset, so every distinct live line
|
|
68
|
+
* survives even when repeated lines make a longer match look valid.
|
|
69
|
+
* - If no head overlap is valid but history's tail appears as a contiguous block somewhere inside `live` (the within-buffer case, e.g. a small `-n N` whose live seed
|
|
70
|
+
* reaches further back than the requested window), the join locates that block and continues past it - returning `history` followed by `live` from that point, dropping
|
|
71
|
+
* the older seed prefix that predates the window.
|
|
72
|
+
* - If neither a head overlap nor a within-buffer alignment is found, the result is `history`, a single {@link gapMarker}, then all of `live`, so the discontinuity is
|
|
73
|
+
* visible and no live line is dropped.
|
|
74
|
+
*
|
|
75
|
+
* Either input being empty short-circuits: an empty `live` returns `history`, and an empty `history` returns `live` (nothing to align against, no marker needed).
|
|
76
|
+
*
|
|
77
|
+
* @param history - The trailing history records, in chronological order (oldest first).
|
|
78
|
+
* @param live - The seed-plus-live records, in chronological order (oldest first); its leading records are the seed that overlaps history's tail.
|
|
79
|
+
* @param options - Optional bounds. See {@link StitchOptions}.
|
|
80
|
+
*
|
|
81
|
+
* @returns The joined record list.
|
|
82
|
+
*
|
|
83
|
+
* @category Log Client
|
|
84
|
+
*/
|
|
85
|
+
export function stitchLive(history, live, options = {}) {
|
|
86
|
+
// Empty-input short-circuits: with nothing on one side there is no seam to align, so we pass the other side through unchanged and emit no marker.
|
|
87
|
+
if (live.length === 0) {
|
|
88
|
+
return [...history];
|
|
89
|
+
}
|
|
90
|
+
if (history.length === 0) {
|
|
91
|
+
return [...live];
|
|
92
|
+
}
|
|
93
|
+
// Bound the search to the seed window: never consider an overlap longer than the shorter side, and honor an explicit `maxOverlap` cap so the cost is proportional to
|
|
94
|
+
// the seed rather than to the full history.
|
|
95
|
+
const naturalBound = Math.min(history.length, live.length);
|
|
96
|
+
const bound = (options.maxOverlap !== undefined) ? Math.min(naturalBound, Math.max(0, options.maxOverlap)) : naturalBound;
|
|
97
|
+
// Single pass over candidate overlap lengths, capturing both the minimal and maximal valid `k`. Minimal drives the join (keep the most live content -> never drop a
|
|
98
|
+
// distinct line); maximal drives the total-overlap test (all of live already present -> no new lines).
|
|
99
|
+
let minK = 0;
|
|
100
|
+
let maxK = 0;
|
|
101
|
+
for (let k = 1; k <= bound; k++) {
|
|
102
|
+
if (overlapsAt(history, live, k)) {
|
|
103
|
+
if (minK === 0) {
|
|
104
|
+
minK = k;
|
|
105
|
+
}
|
|
106
|
+
maxK = k;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Total overlap: the longest valid match consumes all of live, so there are no new live lines yet. Return history unchanged - no duplicates, no marker.
|
|
110
|
+
if (maxK === live.length) {
|
|
111
|
+
return [...history];
|
|
112
|
+
}
|
|
113
|
+
// Partial head overlap (the common case - history is a superset of the seed, e.g. `--all -f` or a large `-n`): join at the minimal valid offset, retaining every live
|
|
114
|
+
// line from that point on. This is where bounded duplicate chatter may appear at the seam, in exchange for never dropping a distinct live line.
|
|
115
|
+
if (minK > 0) {
|
|
116
|
+
return [...history, ...live.slice(minK)];
|
|
117
|
+
}
|
|
118
|
+
// No head overlap. This happens when the live seed reaches further back than the requested history window (a small `-n N`), so history sits WITHIN the live buffer
|
|
119
|
+
// rather than overlapping its head. Locate history's tail as a contiguous block in live and continue past it, dropping the older seed lines that predate the window.
|
|
120
|
+
const within = stitchWithin(history, live, bound);
|
|
121
|
+
if (within !== null) {
|
|
122
|
+
return within;
|
|
123
|
+
}
|
|
124
|
+
// Neither alignment held - history's tail appears nowhere in live: surface the discontinuity rather than hide it, and keep every live line.
|
|
125
|
+
return [...history, gapMarker(), ...live];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Thin coordinator that joins an already-collected history tail and live buffer via {@link stitchLive}.
|
|
129
|
+
*
|
|
130
|
+
* This is the composition seam the client uses for `follow-history`: it takes the already-materialized history tail and the already-buffered seed-plus-live records and
|
|
131
|
+
* defers entirely to {@link stitchLive} for the join policy. It exists so callers have one named entry point for "merge history then live" rather than re-deriving the
|
|
132
|
+
* call, and so the join policy lives in exactly one place. It performs no I/O of its own - the caller is responsible for having collected both sides (the socket seeds
|
|
133
|
+
* into a bounded ring and the REST download streams into records before this is called).
|
|
134
|
+
*
|
|
135
|
+
* @param history - The trailing history records, oldest first.
|
|
136
|
+
* @param live - The seed-plus-live records, oldest first.
|
|
137
|
+
* @param options - Optional bounds forwarded to {@link stitchLive}.
|
|
138
|
+
*
|
|
139
|
+
* @returns The joined record list.
|
|
140
|
+
*
|
|
141
|
+
* @category Log Client
|
|
142
|
+
*/
|
|
143
|
+
export function mergeHistoryThenLive(history, live, options = {}) {
|
|
144
|
+
return stitchLive(history, live, options);
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=stitch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stitch.js","sourceRoot":"","sources":["../../src/logclient/stitch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, CLI-layer parsing of the `hblog` `--since`/`--until` time expressions into an absolute epoch interval.
|
|
3
|
+
*
|
|
4
|
+
* {@link parseTimeExpression} turns a user-typed expression (`1d`, `7am`, `2026-06-29`, `"2026-06-29 6am"`, `now`/`today`/`yesterday`) into the `{ start, end }` interval
|
|
5
|
+
* the precision of the expression names. The lower edge (`start`) is what `--since` binds to and the upper edge (`end`) is what `--until` binds to, which is the whole
|
|
6
|
+
* point of returning an INTERVAL rather than a single instant: a date-only `--until 2026-06-29` then includes the entire named day (its `end` is the last millisecond of
|
|
7
|
+
* that day), while `--since 2026-06-29` starts at midnight (its `start`).
|
|
8
|
+
*
|
|
9
|
+
* The module lives at the CLI layer, exactly like `config.ts`: it is isolated, unit-tested against an injected `now`, and NOT part of the package barrel. It takes the
|
|
10
|
+
* resolved `now` (epoch milliseconds) as an argument rather than reading the clock itself, so resolution is deterministic under test. It is Node-native and depends on no
|
|
11
|
+
* date library (a house rule); the only shared dependency is {@link normalizeClock} in `parser.ts`, which owns the 12-hour-to-24-hour clock rule so this module does not
|
|
12
|
+
* re-implement it. DST-gap instants are best-effort, in the same spirit as `parseLogTimestamp`.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import type { Nullable } from "../util.ts";
|
|
17
|
+
/**
|
|
18
|
+
* Parse a `--since`/`--until` time expression into the absolute epoch interval it denotes, resolved against the injected `now`.
|
|
19
|
+
*
|
|
20
|
+
* The expression is matched in precedence order, first match wins (named tokens and the meridiem are case-insensitive):
|
|
21
|
+
*
|
|
22
|
+
* 1. A named token - `now` (a point), `today` (the whole local day), `yesterday` (the whole prior local day).
|
|
23
|
+
* 2. A relative age - one-or-more `<int><unit>` segments (`1d`, `2h`, `30m`, `90s`, `2h30m`), as `now - sum`; a bare unit-less integer does not match.
|
|
24
|
+
* 3. A `YYYY-MM-DD` date with an optional clock (`2026-06-29`, `2026-06-29T06:00`, `2026-06-29 06:00:00`, `"2026-06-29 6am"`); date-only spans the whole local day.
|
|
25
|
+
* 4. A standalone clock applied to NOW's local date (`7am`, `2pm`, `14:30`, `7:00:00`); a future clock is not rolled back, matching journalctl.
|
|
26
|
+
*
|
|
27
|
+
* The returned interval's lower edge (`start`) is what `--since` binds to and its upper edge (`end`) is what `--until` binds to, so the precision of the expression
|
|
28
|
+
* decides how much of a named day or second a bound covers. Returns `null` for any unrecognized or out-of-range input; the CLI maps `null` to a usage error listing the
|
|
29
|
+
* accepted forms.
|
|
30
|
+
*
|
|
31
|
+
* @param expr - The user-typed time expression.
|
|
32
|
+
* @param now - The reference instant in epoch milliseconds, against which named, relative, and clock-today expressions resolve.
|
|
33
|
+
*
|
|
34
|
+
* @returns The `{ start, end }` epoch-millisecond interval the expression denotes, or `null` when the expression is not recognized.
|
|
35
|
+
*
|
|
36
|
+
* @category Log Client
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseTimeExpression(expr: string, now: number): Nullable<{
|
|
39
|
+
end: number;
|
|
40
|
+
start: number;
|
|
41
|
+
}>;
|
|
42
|
+
//# sourceMappingURL=time-expression.d.ts.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* logclient/time-expression.ts: Pure CLI-layer parser turning a `--since`/`--until` time expression into an absolute epoch interval.
|
|
4
|
+
*/
|
|
5
|
+
import { normalizeClock } from "./parser.js";
|
|
6
|
+
// The millisecond magnitudes the relative-expression units and the precision spans are built from, named so the arithmetic reads by intent rather than by magic number.
|
|
7
|
+
const MS_PER_SECOND = 1000;
|
|
8
|
+
const MS_PER_MINUTE = 60000;
|
|
9
|
+
const MS_PER_HOUR = 3600000;
|
|
10
|
+
const MS_PER_DAY = 86400000;
|
|
11
|
+
// The per-unit millisecond multipliers for a relative ("ago") expression's `<int><unit>` segments. Frozen so the shared module-scope table cannot be mutated by a
|
|
12
|
+
// consumer, mirroring the parser's SGR map.
|
|
13
|
+
const UNIT_MS = Object.freeze({ d: MS_PER_DAY, h: MS_PER_HOUR, m: MS_PER_MINUTE, s: MS_PER_SECOND });
|
|
14
|
+
// A relative ("ago") expression is one-or-more `<int><unit>` segments and nothing else: `1d`, `2h30m`, `90s`. The full-string anchor rejects a bare unit-less integer
|
|
15
|
+
// (`5`) and any stray character, so a relative expression is never confused with a clock or a date. The segment pattern walks each segment to sum the offset.
|
|
16
|
+
const RELATIVE_PATTERN = /^(?:\d+[dhms])+$/i;
|
|
17
|
+
const RELATIVE_SEGMENT_PATTERN = /(\d+)([dhms])/gi;
|
|
18
|
+
// A `YYYY-MM-DD` date, optionally followed by a `T` or a space and a clock the {@link parseClock} helper interprets. Two-digit month/day, four-digit year. The trailing
|
|
19
|
+
// capture is the clock text when present.
|
|
20
|
+
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](.+))?$/;
|
|
21
|
+
// A 24-hour clock `HH:mm[:ss]` (`14:30`, `7:00:00`); the seconds group is optional. When seconds are absent the reading is minute-precise, when present second-precise.
|
|
22
|
+
const CLOCK_24_PATTERN = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/;
|
|
23
|
+
// A 12-hour clock `h[:mm[:ss]]am|pm` (`7am`, `2pm`, `6:30am`). The meridiem is mandatory and an unspecified minute/second defaults to 0; a meridiem-bearing reading is
|
|
24
|
+
// second-precise.
|
|
25
|
+
const CLOCK_12_PATTERN = /^(\d{1,2})(?::(\d{2})(?::(\d{2}))?)?\s*([AaPp][Mm])$/;
|
|
26
|
+
// Build the `{ start, end }` interval an expression denotes from its lower instant and its precision span. The interval is closed and inclusive on both ends: `end` is
|
|
27
|
+
// the last millisecond the precision covers (`start + spanMs - 1`), so a one-day span ends one millisecond before the next day's midnight, and a one-second span ends at
|
|
28
|
+
// `.999`. A point span (`1`) collapses the interval to a single instant (`end === start`).
|
|
29
|
+
function interval(start, spanMs) {
|
|
30
|
+
return { end: start + spanMs - 1, start };
|
|
31
|
+
}
|
|
32
|
+
// Compute local midnight of the calendar day `offsetDays` away from the day containing `now`. We read `now`'s LOCAL year/month/day and reconstruct midnight via the Date
|
|
33
|
+
// constructor, passing the offset through the day argument so the constructor normalizes a month/year rollover (day 0 is the prior month's last day). This is local,
|
|
34
|
+
// never UTC, so `today`/`yesterday` align with the host's own wall clock.
|
|
35
|
+
function startOfLocalDay(now, offsetDays) {
|
|
36
|
+
const reference = new Date(now);
|
|
37
|
+
return new Date(reference.getFullYear(), reference.getMonth(), reference.getDate() + offsetDays).getTime();
|
|
38
|
+
}
|
|
39
|
+
// Construct a LOCAL instant from explicit calendar and clock components, or `null` when the calendar names an impossible date. The calendar fields are round-tripped (the
|
|
40
|
+
// same guard as {@link parseLogTimestamp}) so `2026-02-30` is rejected rather than rolled into March; only the calendar is checked, so a DST-gap wall clock is
|
|
41
|
+
// best-effort accepted. `month` is 1-based here (the caller passes the human month), matching the regex captures.
|
|
42
|
+
function localInstant(year, month, day, hour, minute, second) {
|
|
43
|
+
const date = new Date(year, month - 1, day, hour, minute, second);
|
|
44
|
+
if ((date.getFullYear() !== year) || (date.getMonth() !== (month - 1)) || (date.getDate() !== day)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return date.getTime();
|
|
48
|
+
}
|
|
49
|
+
// Parse a standalone clock string into its normalized 24-hour components plus the precision span the clock's own form implies. A bare `HH:mm` is minute-precise; any form
|
|
50
|
+
// carrying seconds, or any 12-hour (meridiem) form, is second-precise. The 12-hour-to-24-hour conversion and the range check are delegated to the shared
|
|
51
|
+
// `normalizeClock`, so this helper never re-implements the meridiem rule. Returns `null` when the text is not a clock or when a component is out of range.
|
|
52
|
+
function parseClock(text) {
|
|
53
|
+
// 24-hour `HH:mm[:ss]`. The seconds group decides the precision: absent is minute-precise, present is second-precise.
|
|
54
|
+
const military = CLOCK_24_PATTERN.exec(text);
|
|
55
|
+
if (military !== null) {
|
|
56
|
+
const clock = normalizeClock({ hour: Number.parseInt(military[1] ?? "", 10), minute: Number.parseInt(military[2] ?? "", 10),
|
|
57
|
+
second: (military[3] !== undefined) ? Number.parseInt(military[3], 10) : 0 });
|
|
58
|
+
if (clock === null) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return { hour: clock.hour, minute: clock.minute, second: clock.second, spanMs: (military[3] !== undefined) ? MS_PER_SECOND : MS_PER_MINUTE };
|
|
62
|
+
}
|
|
63
|
+
// 12-hour `h[:mm[:ss]]am|pm`. A meridiem is always present, so the reading is second-precise; an unspecified minute/second defaults to 0.
|
|
64
|
+
const meridian = CLOCK_12_PATTERN.exec(text);
|
|
65
|
+
if (meridian !== null) {
|
|
66
|
+
const clock = normalizeClock({ hour: Number.parseInt(meridian[1] ?? "", 10), meridiem: meridian[4],
|
|
67
|
+
minute: (meridian[2] !== undefined) ? Number.parseInt(meridian[2], 10) : 0, second: (meridian[3] !== undefined) ? Number.parseInt(meridian[3], 10) : 0 });
|
|
68
|
+
if (clock === null) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return { hour: clock.hour, minute: clock.minute, second: clock.second, spanMs: MS_PER_SECOND };
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
// Arm 1: a named token. `now` is a point, `today` is the whole local calendar day, and `yesterday` is the whole prior local day. Matched case-insensitively. Returns
|
|
76
|
+
// `null` for any other text so the chain falls through to the next arm.
|
|
77
|
+
function parseNamed(text, now) {
|
|
78
|
+
switch (text.toLowerCase()) {
|
|
79
|
+
case "now": {
|
|
80
|
+
return interval(now, 1);
|
|
81
|
+
}
|
|
82
|
+
case "today": {
|
|
83
|
+
return interval(startOfLocalDay(now, 0), MS_PER_DAY);
|
|
84
|
+
}
|
|
85
|
+
case "yesterday": {
|
|
86
|
+
return interval(startOfLocalDay(now, -1), MS_PER_DAY);
|
|
87
|
+
}
|
|
88
|
+
default: {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Arm 2: a relative ("ago") expression - one-or-more `<int><unit>` segments summed into an offset subtracted from `now`. The result is a point instant. Returns `null`
|
|
94
|
+
// when the text is not a well-formed relative expression (a bare unit-less integer does not match), so the chain falls through.
|
|
95
|
+
function parseRelative(text, now) {
|
|
96
|
+
if (!RELATIVE_PATTERN.test(text)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// Sum every `<int><unit>` segment. The full-string guard above guarantees the string is one-or-more well-formed segments and nothing else, so the scan below cannot
|
|
100
|
+
// meet a stray character; each unit is one of d/h/m/s and so is always present in the frozen multiplier table.
|
|
101
|
+
let offsetMs = 0;
|
|
102
|
+
// Reset the shared global regex's cursor to the scan start. `RELATIVE_SEGMENT_PATTERN` carries the `g` flag so its `lastIndex` persists across calls; rewinding it here
|
|
103
|
+
// keeps the module-scope regex stateless across `parseRelative` invocations.
|
|
104
|
+
RELATIVE_SEGMENT_PATTERN.lastIndex = 0;
|
|
105
|
+
for (let match = RELATIVE_SEGMENT_PATTERN.exec(text); match !== null; match = RELATIVE_SEGMENT_PATTERN.exec(text)) {
|
|
106
|
+
const amount = Number.parseInt(match[1] ?? "", 10);
|
|
107
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
108
|
+
offsetMs += amount * (UNIT_MS[unit] ?? 0);
|
|
109
|
+
}
|
|
110
|
+
return interval(now - offsetMs, 1);
|
|
111
|
+
}
|
|
112
|
+
// Arm 3: a `YYYY-MM-DD` date with an optional clock. A date alone spans the whole local day; a date with a clock takes the clock's own precision (minute for a bare
|
|
113
|
+
// `HH:mm`, second otherwise). Always LOCAL, never `new Date("2026-06-29")` (which is UTC midnight by spec and would skew the window by the local offset). Returns `null`
|
|
114
|
+
// when the text is not a date, when the date is impossible, or when the trailing clock is unparseable.
|
|
115
|
+
function parseDateExpression(text) {
|
|
116
|
+
const match = DATE_PATTERN.exec(text);
|
|
117
|
+
if (match === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const year = Number.parseInt(match[1] ?? "", 10);
|
|
121
|
+
const month = Number.parseInt(match[2] ?? "", 10);
|
|
122
|
+
const day = Number.parseInt(match[3] ?? "", 10);
|
|
123
|
+
const clockText = match[4];
|
|
124
|
+
// Date only: the whole local calendar day, from local midnight, a day span.
|
|
125
|
+
if (clockText === undefined) {
|
|
126
|
+
const start = localInstant(year, month, day, 0, 0, 0);
|
|
127
|
+
return (start === null) ? null : interval(start, MS_PER_DAY);
|
|
128
|
+
}
|
|
129
|
+
// Date plus a clock: apply the clock to the named calendar day at the clock's own precision.
|
|
130
|
+
const clock = parseClock(clockText);
|
|
131
|
+
if (clock === null) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const start = localInstant(year, month, day, clock.hour, clock.minute, clock.second);
|
|
135
|
+
return (start === null) ? null : interval(start, clock.spanMs);
|
|
136
|
+
}
|
|
137
|
+
// Arm 4: a standalone clock applied to NOW's local calendar date, at the clock's OWN form-determined precision - the same span arm 3 uses for a date-plus-clock - so a
|
|
138
|
+
// bare 24-hour `14:30` covers its whole minute exactly as `2026-06-29 14:30`, while a meridiem time (`7am`) or an explicit-seconds time pins an exact second. A clock
|
|
139
|
+
// in the future relative to `now` is NOT rolled back to yesterday: if it is 3am and the user types `--since 7am`, the window starts at today 07:00 (an empty result until
|
|
140
|
+
// 7am), matching journalctl. Returns `null` when the text is not a clock.
|
|
141
|
+
function parseClockToday(text, now) {
|
|
142
|
+
const clock = parseClock(text);
|
|
143
|
+
if (clock === null) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const reference = new Date(now);
|
|
147
|
+
const start = localInstant(reference.getFullYear(), reference.getMonth() + 1, reference.getDate(), clock.hour, clock.minute, clock.second);
|
|
148
|
+
return (start === null) ? null : interval(start, clock.spanMs);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse a `--since`/`--until` time expression into the absolute epoch interval it denotes, resolved against the injected `now`.
|
|
152
|
+
*
|
|
153
|
+
* The expression is matched in precedence order, first match wins (named tokens and the meridiem are case-insensitive):
|
|
154
|
+
*
|
|
155
|
+
* 1. A named token - `now` (a point), `today` (the whole local day), `yesterday` (the whole prior local day).
|
|
156
|
+
* 2. A relative age - one-or-more `<int><unit>` segments (`1d`, `2h`, `30m`, `90s`, `2h30m`), as `now - sum`; a bare unit-less integer does not match.
|
|
157
|
+
* 3. A `YYYY-MM-DD` date with an optional clock (`2026-06-29`, `2026-06-29T06:00`, `2026-06-29 06:00:00`, `"2026-06-29 6am"`); date-only spans the whole local day.
|
|
158
|
+
* 4. A standalone clock applied to NOW's local date (`7am`, `2pm`, `14:30`, `7:00:00`); a future clock is not rolled back, matching journalctl.
|
|
159
|
+
*
|
|
160
|
+
* The returned interval's lower edge (`start`) is what `--since` binds to and its upper edge (`end`) is what `--until` binds to, so the precision of the expression
|
|
161
|
+
* decides how much of a named day or second a bound covers. Returns `null` for any unrecognized or out-of-range input; the CLI maps `null` to a usage error listing the
|
|
162
|
+
* accepted forms.
|
|
163
|
+
*
|
|
164
|
+
* @param expr - The user-typed time expression.
|
|
165
|
+
* @param now - The reference instant in epoch milliseconds, against which named, relative, and clock-today expressions resolve.
|
|
166
|
+
*
|
|
167
|
+
* @returns The `{ start, end }` epoch-millisecond interval the expression denotes, or `null` when the expression is not recognized.
|
|
168
|
+
*
|
|
169
|
+
* @category Log Client
|
|
170
|
+
*/
|
|
171
|
+
export function parseTimeExpression(expr, now) {
|
|
172
|
+
// Normalize surrounding whitespace once; an empty expression matches nothing.
|
|
173
|
+
const text = expr.trim();
|
|
174
|
+
if (text.length === 0) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
// Resolve in precedence order, first match wins. Each arm returns `null` when it does not apply, so the `??` chain falls through to the next arm and finally to a
|
|
178
|
+
// `null` "unrecognized expression" result the CLI maps to a usage error.
|
|
179
|
+
return parseNamed(text, now) ?? parseRelative(text, now) ?? parseDateExpression(text) ?? parseClockToday(text, now);
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=time-expression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-expression.js","sourceRoot":"","sources":["../../src/logclient/time-expression.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The internal time-window stream transform for the `hblog` CLI.
|
|
3
|
+
*
|
|
4
|
+
* {@link timeWindow} wraps a record stream and yields only the records whose instant falls inside an inclusive `[since, until]` epoch window, parsing each record's
|
|
5
|
+
* timestamp on demand via {@link parseLogTimestamp}. It is deliberately NOT shaped like `createLogFilter` (a reusable `(record) => boolean` predicate) and is NOT part of
|
|
6
|
+
* the package barrel: it carries per-stream state (the carry-forward instant), so a single async-generator transform - fresh state per call, structurally single-use -
|
|
7
|
+
* makes cross-stream reuse with stale state unrepresentable, whereas a reused predicate would be wrong on a second stream.
|
|
8
|
+
*
|
|
9
|
+
* The carry-forward is the load-bearing detail. A record with no parseable instant (a `null` timestamp, or text in an unrecognized locale) is a continuation line of a
|
|
10
|
+
* multi-line message (a stack trace whose only first line carries `[timestamp]`), a bare status line, or a parse failure. A naive "is this record's own epoch in range"
|
|
11
|
+
* test would shred every stack trace - the first line in, the traceback gone. Instead a null-epoch line inherits the instant of the most recent record that DID parse, so
|
|
12
|
+
* a continuation is kept iff its parent is. This is a log-semantics rule intrinsic to "filter records by time correctly," so it lives in this primitive. The generator
|
|
13
|
+
* necessarily processes records in arrival order, and every channel yields in order (history is file order, live is arrival order, the stitch preserves order), so the
|
|
14
|
+
* carry-forward is sound; the stitch's null-timestamp gap marker inherits and shows, because a window must never hide a discontinuity marker.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import type { LogRecord } from "./types.ts";
|
|
19
|
+
import type { Nullable } from "../util.ts";
|
|
20
|
+
/**
|
|
21
|
+
* Filter a record stream to an inclusive `[since, until]` epoch window, with carry-forward for records that carry no parseable instant.
|
|
22
|
+
*
|
|
23
|
+
* For each source record: derive its epoch on demand from its timestamp text; update the carried instant whenever a record DID parse (so a later continuation inherits
|
|
24
|
+
* the correct parent even across a skipped region); judge the record by its own epoch when it has one, otherwise by the carried instant. A record that arrives before ANY
|
|
25
|
+
* timestamp has been seen is treated as the oldest possible instant: a `since` lower bound excludes it, a pure `until` upper bound includes it. Both bounds are
|
|
26
|
+
* inclusive, and a `null` bound is unbounded on that side.
|
|
27
|
+
*
|
|
28
|
+
* @param source - The upstream record stream (a channel's `LogStream`, or any async iterable of records in arrival order).
|
|
29
|
+
* @param bounds - The window bounds in epoch milliseconds. `since` is the inclusive lower bound (`null` for unbounded-below); `until` is the inclusive upper bound
|
|
30
|
+
* (`null` for unbounded-above).
|
|
31
|
+
*
|
|
32
|
+
* @returns An async generator yielding only the records inside the window, in arrival order.
|
|
33
|
+
*/
|
|
34
|
+
export declare function timeWindow(source: AsyncIterable<LogRecord>, bounds: {
|
|
35
|
+
since: Nullable<number>;
|
|
36
|
+
until: Nullable<number>;
|
|
37
|
+
}): AsyncGenerator<LogRecord>;
|
|
38
|
+
//# sourceMappingURL=time-window.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* logclient/time-window.ts: Internal async-generator transform that filters a record stream to an inclusive epoch window with carry-forward.
|
|
4
|
+
*/
|
|
5
|
+
import { parseLogTimestamp } from "./parser.js";
|
|
6
|
+
/**
|
|
7
|
+
* Filter a record stream to an inclusive `[since, until]` epoch window, with carry-forward for records that carry no parseable instant.
|
|
8
|
+
*
|
|
9
|
+
* For each source record: derive its epoch on demand from its timestamp text; update the carried instant whenever a record DID parse (so a later continuation inherits
|
|
10
|
+
* the correct parent even across a skipped region); judge the record by its own epoch when it has one, otherwise by the carried instant. A record that arrives before ANY
|
|
11
|
+
* timestamp has been seen is treated as the oldest possible instant: a `since` lower bound excludes it, a pure `until` upper bound includes it. Both bounds are
|
|
12
|
+
* inclusive, and a `null` bound is unbounded on that side.
|
|
13
|
+
*
|
|
14
|
+
* @param source - The upstream record stream (a channel's `LogStream`, or any async iterable of records in arrival order).
|
|
15
|
+
* @param bounds - The window bounds in epoch milliseconds. `since` is the inclusive lower bound (`null` for unbounded-below); `until` is the inclusive upper bound
|
|
16
|
+
* (`null` for unbounded-above).
|
|
17
|
+
*
|
|
18
|
+
* @returns An async generator yielding only the records inside the window, in arrival order.
|
|
19
|
+
*/
|
|
20
|
+
export async function* timeWindow(source, bounds) {
|
|
21
|
+
const { since, until } = bounds;
|
|
22
|
+
// The carried epoch of the most recent record that parsed to an instant. A null-epoch line inherits this so it is kept iff its parent is.
|
|
23
|
+
let carried = null;
|
|
24
|
+
for await (const record of source) {
|
|
25
|
+
// Derive the epoch on demand; a null timestamp (or an unrecognized format) yields null and the record falls back to the carried parent instant.
|
|
26
|
+
const epoch = (record.timestamp !== null) ? parseLogTimestamp(record.timestamp) : null;
|
|
27
|
+
// Update the carry on EVERY real timestamp, in-window or not, so a later continuation inherits the correct parent instant even across a region the window skips.
|
|
28
|
+
if (epoch !== null) {
|
|
29
|
+
carried = epoch;
|
|
30
|
+
}
|
|
31
|
+
// The instant this record is judged by: its own epoch when it has one, otherwise the carried parent instant.
|
|
32
|
+
const effective = epoch ?? carried;
|
|
33
|
+
// A null effective instant means a null-epoch line arrived before ANY timestamp has been seen - a partial first line from the download, or a leading banner - so it
|
|
34
|
+
// has no placeable instant. We treat it as the oldest possible moment: a lower `since` bound excludes it (a leading orphan cannot be shown to be at or after `since`,
|
|
35
|
+
// and it is the oldest content in a chronological stream), while a pure upper `until` bound includes it (it precedes everything, so it is within "up to `until`").
|
|
36
|
+
if (effective === null) {
|
|
37
|
+
if (since !== null) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
yield record;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Apply the inclusive bounds. A record before `since` or after `until` is skipped; everything else is admitted.
|
|
44
|
+
if ((since !== null) && (effective < since)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if ((until !== null) && (effective > until)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
yield record;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=time-window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-window.js","sourceRoot":"","sources":["../../src/logclient/time-window.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAqBH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,SAAU,CAAC,CAAA,UAAU,CAAC,MAAgC,EAAE,MAA4D;IAE9H,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IAEhC,0IAA0I;IAC1I,IAAI,OAAO,GAAqB,IAAI,CAAC;IAErC,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,MAAM,EAAE,CAAC;QAElC,gJAAgJ;QAChJ,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAEvF,iKAAiK;QACjK,IAAG,KAAK,KAAK,IAAI,EAAE,CAAC;YAElB,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC;QAED,6GAA6G;QAC7G,MAAM,SAAS,GAAG,KAAK,IAAI,OAAO,CAAC;QAEnC,oKAAoK;QACpK,sKAAsK;QACtK,mKAAmK;QACnK,IAAG,SAAS,KAAK,IAAI,EAAE,CAAC;YAEtB,IAAG,KAAK,KAAK,IAAI,EAAE,CAAC;gBAElB,SAAS;YACX,CAAC;YAED,MAAM,MAAM,CAAC;YAEb,SAAS;QACX,CAAC;QAED,gHAAgH;QAChH,IAAG,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,EAAE,CAAC;YAE3C,SAAS;QACX,CAAC;QAED,IAAG,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,EAAE,CAAC;YAE3C,SAAS;QACX,CAAC;QAED,MAAM,MAAM,CAAC;IACf,CAAC;AACH,CAAC"}
|