@atolis-hq/wake 0.4.56 → 0.4.57
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 +4 -0
- package/dist/src/bootstrap/analytics-reports.d.ts +27 -0
- package/dist/src/bootstrap/analytics-reports.js +38 -0
- package/dist/src/bootstrap/surface-api-applications.js +2 -0
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/execution/application/run-reports.d.ts +29 -0
- package/dist/src/execution/application/run-reports.js +116 -0
- package/dist/src/execution/contracts/runner.js +1 -1
- package/dist/src/execution/domain/run.d.ts +2 -0
- package/dist/src/execution/domain/run.js +10 -0
- package/dist/src/execution/index.d.ts +1 -0
- package/dist/src/execution/index.js +1 -0
- package/dist/src/kernel/contracts/reports.d.ts +39 -0
- package/dist/src/kernel/contracts/reports.js +53 -0
- package/dist/src/kernel/index.d.ts +1 -0
- package/dist/src/kernel/index.js +1 -0
- package/dist/src/node_modules/@atolis-hq/eventing/dist/index.d.ts +1 -0
- package/dist/src/node_modules/@atolis-hq/eventing/dist/index.d.ts.map +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing/dist/index.js +1 -0
- package/dist/src/node_modules/@atolis-hq/eventing/dist/store/event-journal.d.ts +2 -0
- package/dist/src/node_modules/@atolis-hq/eventing/dist/store/event-journal.d.ts.map +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing/dist/store/scan-journal.d.ts +5 -0
- package/dist/src/node_modules/@atolis-hq/eventing/dist/store/scan-journal.d.ts.map +1 -0
- package/dist/src/node_modules/@atolis-hq/eventing/dist/store/scan-journal.js +34 -0
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-event-journal.d.ts +4 -0
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-event-journal.d.ts.map +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-event-journal.js +60 -2
- package/dist/src/orchestration/application/work-completion-summary.d.ts +6 -0
- package/dist/src/orchestration/application/work-completion-summary.js +49 -0
- package/dist/src/orchestration/index.d.ts +1 -0
- package/dist/src/orchestration/index.js +1 -0
- package/dist/src/surfaces/api/contracts/observability.d.ts +15 -0
- package/dist/src/surfaces/api/contracts/observability.js +1 -1
- package/dist/src/surfaces/api/http-server.d.ts +1 -1
- package/dist/src/surfaces/api/http-server.js +23 -1
- package/dist/src/surfaces/api/routes/applications.d.ts +7 -0
- package/dist/src/surfaces/api/routes/index.js +2 -2
- package/dist/src/surfaces/api/routes/observability.d.ts +1 -1
- package/dist/src/surfaces/api/routes/observability.js +5 -1
- package/dist/src/surfaces/api/routes/read.d.ts +1 -1
- package/dist/src/surfaces/api/routes/read.js +28 -1
- package/dist/src/surfaces/web-assets/assets/{index-B7SgB4eG.css → index-B5-ZKu_w.css} +1 -1
- package/dist/src/surfaces/web-assets/assets/index-CNG6RzFJ.js +1131 -0
- package/dist/src/surfaces/web-assets/index.html +2 -2
- package/package.json +1 -1
- package/dist/src/surfaces/web-assets/assets/index-DPaH6c6e.js +0 -1131
package/README.md
CHANGED
|
@@ -150,6 +150,10 @@ enter it in the UI. Successful logins persist for two years across browser and
|
|
|
150
150
|
Wake-container restarts. Use `wake ui token set <key>` to replace the durable
|
|
151
151
|
access key; replacement signs out existing sessions.
|
|
152
152
|
|
|
153
|
+
Analytics provides live reports with selectable windows, run outcome buckets,
|
|
154
|
+
and summary metrics alongside today's system activity counters. See
|
|
155
|
+
[Analytics API](docs/analytics-api.md) and [web UI](docs/web-ui.md) for definitions.
|
|
156
|
+
|
|
153
157
|
## Supported Agent CLIs
|
|
154
158
|
|
|
155
159
|
Wake wraps existing coding-agent CLIs rather than replacing them. Runner
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EventJournal } from '@atolis-hq/eventing';
|
|
2
|
+
import { type ReportWindowId } from '../kernel/index.js';
|
|
3
|
+
export declare function createAnalyticsReports(journal: EventJournal, now: () => string): {
|
|
4
|
+
catalogue: () => {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
}[];
|
|
8
|
+
report(id: string, windowId: ReportWindowId, signal?: AbortSignal): Promise<{
|
|
9
|
+
data: import("../kernel/index.js").TimeSeriesReport;
|
|
10
|
+
meta: import("../surfaces/index.js").ResponseMeta;
|
|
11
|
+
}>;
|
|
12
|
+
summary(windowId: ReportWindowId, signal?: AbortSignal): Promise<{
|
|
13
|
+
data: {
|
|
14
|
+
workItemsDone: number;
|
|
15
|
+
medianEndToEndDurationMs: number | null;
|
|
16
|
+
runs: number;
|
|
17
|
+
completed: number;
|
|
18
|
+
blockedOrAwaitingApproval: number;
|
|
19
|
+
failed: number;
|
|
20
|
+
totalTokens: number;
|
|
21
|
+
totalCostUsd: number;
|
|
22
|
+
medianRunDurationMs: number | null;
|
|
23
|
+
window: import("../kernel/index.js").ReportWindow;
|
|
24
|
+
};
|
|
25
|
+
meta: import("../surfaces/index.js").ResponseMeta;
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createRunOutcomesReport, queryRunAnalytics } from '../execution/index.js';
|
|
2
|
+
import { reportWindow } from '../kernel/index.js';
|
|
3
|
+
import { queryWorkCompletionSummary } from '../orchestration/index.js';
|
|
4
|
+
import { sampledMeta } from './surface-api-metadata.js';
|
|
5
|
+
export function createAnalyticsReports(journal, now) {
|
|
6
|
+
const definitions = [createRunOutcomesReport(journal)];
|
|
7
|
+
return {
|
|
8
|
+
catalogue: () => definitions.map(({ id, title }) => ({ id, title })),
|
|
9
|
+
async report(id, windowId, signal) {
|
|
10
|
+
const definition = definitions.find((report) => report.id === id);
|
|
11
|
+
if (definition === undefined)
|
|
12
|
+
throw new Error('Unknown report');
|
|
13
|
+
const collectedAt = now();
|
|
14
|
+
return {
|
|
15
|
+
data: await definition.query({
|
|
16
|
+
window: reportWindow(windowId, collectedAt),
|
|
17
|
+
...(signal === undefined ? {} : { signal }),
|
|
18
|
+
}),
|
|
19
|
+
meta: sampledMeta(collectedAt),
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
async summary(windowId, signal) {
|
|
23
|
+
const collectedAt = now();
|
|
24
|
+
const query = {
|
|
25
|
+
window: reportWindow(windowId, collectedAt),
|
|
26
|
+
...(signal === undefined ? {} : { signal }),
|
|
27
|
+
};
|
|
28
|
+
const [runs, work] = await Promise.all([
|
|
29
|
+
queryRunAnalytics(journal, query),
|
|
30
|
+
queryWorkCompletionSummary(journal, query),
|
|
31
|
+
]);
|
|
32
|
+
return {
|
|
33
|
+
data: { window: query.window, ...runs.summary, ...work },
|
|
34
|
+
meta: sampledMeta(collectedAt),
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ActivationSchedulerSubscriptionStatus, ControlStreamKind, } from '../control-plane/index.js';
|
|
2
2
|
import { ApiCommandStatus, fromWorkItemKey, presentBoardCard, presentResource, presentStatus, presentWorkflowInstance, redactConfiguration, } from '../surfaces/index.js';
|
|
3
3
|
import { analyticsProjection } from './analytics-projection.js';
|
|
4
|
+
import { createAnalyticsReports } from './analytics-reports.js';
|
|
4
5
|
import { boardConditionCounts, boardProjection, normalizeBoardActiveRunPhase, } from './board-projection.js';
|
|
5
6
|
import { primaryExternalRef } from './external-ref.js';
|
|
6
7
|
import { createSelfUpdateFailureLog, describeSelfUpdateFailure, } from './self-update-failure-log.js';
|
|
@@ -187,6 +188,7 @@ async function recentEvents(journal, beforeGlobalPosition, limit, include) {
|
|
|
187
188
|
}
|
|
188
189
|
function createObservabilityApplications(root, now) {
|
|
189
190
|
return {
|
|
191
|
+
reports: createAnalyticsReports(root.journal, now),
|
|
190
192
|
async metrics(query = { days: 7 }) {
|
|
191
193
|
const collectedAt = now();
|
|
192
194
|
const stored = await root.projections.read(analyticsProjection.name, 'global');
|
|
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
|
|
|
124
124
|
return `g${headHash.slice(0, 7)}`;
|
|
125
125
|
return '0.1.0-dev';
|
|
126
126
|
}
|
|
127
|
-
export const wakeVersion = "v0.4.
|
|
127
|
+
export const wakeVersion = "v0.4.57";
|
|
128
128
|
/** The registry-installable version used by packaged sandbox images. */
|
|
129
129
|
export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type EventJournal } from '@atolis-hq/eventing';
|
|
2
|
+
import { type ReportDefinition, type ReportQuery, type TimeSeriesReport, type ValueOf } from '../../kernel/index.js';
|
|
3
|
+
import type { RunView } from '../contracts/views.js';
|
|
4
|
+
export declare const RunReportId: {
|
|
5
|
+
readonly Outcomes: "run-outcomes-over-time";
|
|
6
|
+
};
|
|
7
|
+
export declare const RunReportOutcome: {
|
|
8
|
+
readonly Completed: "completed";
|
|
9
|
+
readonly Blocked: "blocked";
|
|
10
|
+
readonly AwaitingApproval: "awaiting-approval";
|
|
11
|
+
readonly Failed: "failed";
|
|
12
|
+
readonly Other: "other";
|
|
13
|
+
};
|
|
14
|
+
export type RunReportOutcome = ValueOf<typeof RunReportOutcome>;
|
|
15
|
+
export interface RunSummary {
|
|
16
|
+
readonly runs: number;
|
|
17
|
+
readonly completed: number;
|
|
18
|
+
readonly blockedOrAwaitingApproval: number;
|
|
19
|
+
readonly failed: number;
|
|
20
|
+
readonly totalTokens: number;
|
|
21
|
+
readonly totalCostUsd: number;
|
|
22
|
+
readonly medianRunDurationMs: number | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function runReportOutcome(run: RunView): RunReportOutcome;
|
|
25
|
+
export declare function createRunOutcomesReport(journal: EventJournal): ReportDefinition;
|
|
26
|
+
export declare function queryRunAnalytics(journal: EventJournal, query: ReportQuery): Promise<{
|
|
27
|
+
report: TimeSeriesReport;
|
|
28
|
+
summary: RunSummary;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { scanJournal, yieldScan } from '@atolis-hq/eventing';
|
|
2
|
+
import { ActivityOutcomeKind } from '../../activities/index.js';
|
|
3
|
+
import { defineClosedVocabulary, median, } from '../../kernel/index.js';
|
|
4
|
+
import { selectRunExecutionEvent } from '../contracts/events.js';
|
|
5
|
+
import { agentTokenUsage } from '../contracts/runner.js';
|
|
6
|
+
import { RunStatus } from '../contracts/vocabulary.js';
|
|
7
|
+
import { foldRunEvent } from '../domain/run.js';
|
|
8
|
+
export const RunReportId = defineClosedVocabulary({ Outcomes: 'run-outcomes-over-time' });
|
|
9
|
+
export const RunReportOutcome = defineClosedVocabulary({
|
|
10
|
+
Completed: 'completed',
|
|
11
|
+
Blocked: 'blocked',
|
|
12
|
+
AwaitingApproval: 'awaiting-approval',
|
|
13
|
+
Failed: 'failed',
|
|
14
|
+
Other: 'other',
|
|
15
|
+
});
|
|
16
|
+
const series = [
|
|
17
|
+
{ id: RunReportOutcome.Completed, title: 'Completed' },
|
|
18
|
+
{ id: RunReportOutcome.Blocked, title: 'Blocked' },
|
|
19
|
+
{ id: RunReportOutcome.AwaitingApproval, title: 'Awaiting approval' },
|
|
20
|
+
{ id: RunReportOutcome.Failed, title: 'Failed' },
|
|
21
|
+
{ id: RunReportOutcome.Other, title: 'Other' },
|
|
22
|
+
];
|
|
23
|
+
export function runReportOutcome(run) {
|
|
24
|
+
if (run.status === RunStatus.Failed)
|
|
25
|
+
return RunReportOutcome.Failed;
|
|
26
|
+
if (run.status !== RunStatus.Succeeded)
|
|
27
|
+
return RunReportOutcome.Other;
|
|
28
|
+
switch (run.outcome?.kind) {
|
|
29
|
+
case ActivityOutcomeKind.Done:
|
|
30
|
+
return RunReportOutcome.Completed;
|
|
31
|
+
case ActivityOutcomeKind.Blocked:
|
|
32
|
+
return RunReportOutcome.Blocked;
|
|
33
|
+
case ActivityOutcomeKind.Waiting:
|
|
34
|
+
return RunReportOutcome.AwaitingApproval;
|
|
35
|
+
case ActivityOutcomeKind.Failed:
|
|
36
|
+
case ActivityOutcomeKind.Rejected:
|
|
37
|
+
return RunReportOutcome.Failed;
|
|
38
|
+
default:
|
|
39
|
+
return RunReportOutcome.Other;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function createRunOutcomesReport(journal) {
|
|
43
|
+
return {
|
|
44
|
+
id: RunReportId.Outcomes,
|
|
45
|
+
title: 'Run outcomes over time',
|
|
46
|
+
query: async (query) => (await queryRunAnalytics(journal, query)).report,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function queryRunAnalytics(journal, query) {
|
|
50
|
+
const grouped = new Map();
|
|
51
|
+
for await (const event of scanJournal(journal, query.signal)) {
|
|
52
|
+
const owned = selectRunExecutionEvent(event);
|
|
53
|
+
if (owned === null)
|
|
54
|
+
continue;
|
|
55
|
+
grouped.set(owned.stream.id, foldRunEvent(grouped.get(owned.stream.id) ?? null, owned));
|
|
56
|
+
}
|
|
57
|
+
const from = Date.parse(query.window.from);
|
|
58
|
+
const to = Date.parse(query.window.to);
|
|
59
|
+
const first = Math.floor(from / query.window.bucketMs) * query.window.bucketMs;
|
|
60
|
+
const zero = () => Object.fromEntries(series.map(({ id }) => [id, 0]));
|
|
61
|
+
const buckets = [];
|
|
62
|
+
for (let start = first; start < to; start += query.window.bucketMs)
|
|
63
|
+
buckets.push({
|
|
64
|
+
from: new Date(start).toISOString(),
|
|
65
|
+
to: new Date(start + query.window.bucketMs).toISOString(),
|
|
66
|
+
values: zero(),
|
|
67
|
+
});
|
|
68
|
+
const totals = zero();
|
|
69
|
+
const durations = [];
|
|
70
|
+
let runs = 0, totalTokens = 0, totalCostUsd = 0, processed = 0;
|
|
71
|
+
for (const run of grouped.values()) {
|
|
72
|
+
if (processed++ % 100 === 0)
|
|
73
|
+
await yieldScan(query.signal);
|
|
74
|
+
const start = Date.parse(run.startedAt);
|
|
75
|
+
if (!(start >= from && start < to))
|
|
76
|
+
continue;
|
|
77
|
+
const outcome = runReportOutcome(run);
|
|
78
|
+
totals[outcome]++;
|
|
79
|
+
buckets[Math.floor((start - first) / query.window.bucketMs)].values[outcome]++;
|
|
80
|
+
runs++;
|
|
81
|
+
const usage = agentTokenUsage(run.agent?.metadata);
|
|
82
|
+
totalTokens += finiteOrZero(usage.tokens);
|
|
83
|
+
totalCostUsd += finiteOrZero(usage.costUsd);
|
|
84
|
+
const duration = runDuration(run);
|
|
85
|
+
if (duration !== null)
|
|
86
|
+
durations.push(duration);
|
|
87
|
+
}
|
|
88
|
+
query.signal?.throwIfAborted();
|
|
89
|
+
return {
|
|
90
|
+
report: {
|
|
91
|
+
report: { id: RunReportId.Outcomes, title: 'Run outcomes over time' },
|
|
92
|
+
window: query.window,
|
|
93
|
+
series,
|
|
94
|
+
totals,
|
|
95
|
+
buckets,
|
|
96
|
+
},
|
|
97
|
+
summary: {
|
|
98
|
+
runs,
|
|
99
|
+
completed: totals[RunReportOutcome.Completed],
|
|
100
|
+
blockedOrAwaitingApproval: totals[RunReportOutcome.Blocked] + totals[RunReportOutcome.AwaitingApproval],
|
|
101
|
+
failed: totals[RunReportOutcome.Failed],
|
|
102
|
+
totalTokens,
|
|
103
|
+
totalCostUsd,
|
|
104
|
+
medianRunDurationMs: await median(durations, () => yieldScan(query.signal)),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function finiteOrZero(value) {
|
|
109
|
+
return Number.isFinite(value) ? value : 0;
|
|
110
|
+
}
|
|
111
|
+
function runDuration(run) {
|
|
112
|
+
if (run.finishedAt === undefined)
|
|
113
|
+
return null;
|
|
114
|
+
const duration = Date.parse(run.finishedAt) - Date.parse(run.startedAt);
|
|
115
|
+
return Number.isFinite(duration) && duration >= 0 ? duration : null;
|
|
116
|
+
}
|
|
@@ -5,7 +5,7 @@ export const WakeMcpServerName = 'wake';
|
|
|
5
5
|
export function agentTokenUsage(metadata) {
|
|
6
6
|
const numeric = (key) => {
|
|
7
7
|
const value = metadata?.[key];
|
|
8
|
-
return typeof value === 'number' ? value : 0;
|
|
8
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
9
9
|
};
|
|
10
10
|
return {
|
|
11
11
|
tokens: numeric('inputTokens') +
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { type RunExecutionEvent } from '../contracts/events.js';
|
|
2
2
|
import type { RunView } from '../contracts/views.js';
|
|
3
3
|
export declare function foldRun(events: readonly RunExecutionEvent[]): RunView | null;
|
|
4
|
+
/** Incremental replay for bounded live readers; persisted events must already be decoded. */
|
|
5
|
+
export declare function foldRunEvent(previous: RunView | null, event: RunExecutionEvent): RunView;
|
|
@@ -33,6 +33,16 @@ export function foldRun(events) {
|
|
|
33
33
|
applyRunEvent(state, event.event);
|
|
34
34
|
return state;
|
|
35
35
|
}
|
|
36
|
+
/** Incremental replay for bounded live readers; persisted events must already be decoded. */
|
|
37
|
+
export function foldRunEvent(previous, event) {
|
|
38
|
+
if (previous === null)
|
|
39
|
+
return foldRun([event]);
|
|
40
|
+
if (event.event.eventType === ExecutionEventType.RunPreparationStarted)
|
|
41
|
+
throw invalidRunStream(event.stream.id, 'RunPreparationStarted must be the first event');
|
|
42
|
+
const next = { ...previous };
|
|
43
|
+
applyRunEvent(next, event.event);
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
36
46
|
function creationWorkspace(event) {
|
|
37
47
|
if (event.eventType !== ExecutionEventType.RunStarted || event.payload.workspace === undefined)
|
|
38
48
|
return {};
|
|
@@ -32,3 +32,4 @@ export * from './infrastructure/transcripts.js';
|
|
|
32
32
|
export * from './infrastructure/transcript-store.js';
|
|
33
33
|
export * from './infrastructure/workspace/fake-workspace.js';
|
|
34
34
|
export * from './infrastructure/workspace/git-workspace.js';
|
|
35
|
+
export * from './application/run-reports.js';
|
|
@@ -32,3 +32,4 @@ export * from './infrastructure/transcripts.js';
|
|
|
32
32
|
export * from './infrastructure/transcript-store.js';
|
|
33
33
|
export * from './infrastructure/workspace/fake-workspace.js';
|
|
34
34
|
export * from './infrastructure/workspace/git-workspace.js';
|
|
35
|
+
export * from './application/run-reports.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ValueOf } from './vocabulary.js';
|
|
2
|
+
export declare const ReportWindowId: {
|
|
3
|
+
readonly Day: "1d";
|
|
4
|
+
readonly ThreeDays: "3d";
|
|
5
|
+
readonly FiveDays: "5d";
|
|
6
|
+
readonly Week: "7d";
|
|
7
|
+
};
|
|
8
|
+
export type ReportWindowId = ValueOf<typeof ReportWindowId>;
|
|
9
|
+
export interface ReportWindow {
|
|
10
|
+
readonly id: ReportWindowId;
|
|
11
|
+
readonly from: string;
|
|
12
|
+
readonly to: string;
|
|
13
|
+
readonly bucketMs: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ReportQuery {
|
|
16
|
+
readonly window: ReportWindow;
|
|
17
|
+
readonly signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
export interface ReportDescriptor {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly title: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TimeSeriesReport {
|
|
24
|
+
readonly report: ReportDescriptor;
|
|
25
|
+
readonly window: ReportWindow;
|
|
26
|
+
readonly series: readonly ReportDescriptor[];
|
|
27
|
+
readonly totals: Readonly<Record<string, number>>;
|
|
28
|
+
readonly buckets: readonly {
|
|
29
|
+
readonly from: string;
|
|
30
|
+
readonly to: string;
|
|
31
|
+
readonly values: Readonly<Record<string, number>>;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
export interface ReportDefinition extends ReportDescriptor {
|
|
35
|
+
query(query: ReportQuery): Promise<TimeSeriesReport>;
|
|
36
|
+
}
|
|
37
|
+
export declare function reportWindow(id: ReportWindowId, now: string): ReportWindow;
|
|
38
|
+
/** Exact median with bounded sorting/merging work between caller-supplied pauses. */
|
|
39
|
+
export declare function median(values: readonly number[], pause: () => Promise<void>): Promise<number | null>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineClosedVocabulary } from './vocabulary.js';
|
|
2
|
+
export const ReportWindowId = defineClosedVocabulary({
|
|
3
|
+
Day: '1d',
|
|
4
|
+
ThreeDays: '3d',
|
|
5
|
+
FiveDays: '5d',
|
|
6
|
+
Week: '7d',
|
|
7
|
+
});
|
|
8
|
+
export function reportWindow(id, now) {
|
|
9
|
+
const day = 24 * 60 * 60 * 1000;
|
|
10
|
+
const days = {
|
|
11
|
+
[ReportWindowId.Day]: 1,
|
|
12
|
+
[ReportWindowId.ThreeDays]: 3,
|
|
13
|
+
[ReportWindowId.FiveDays]: 5,
|
|
14
|
+
[ReportWindowId.Week]: 7,
|
|
15
|
+
}[id];
|
|
16
|
+
const rolling = days <= 3;
|
|
17
|
+
const to = rolling ? Date.parse(now) : Math.floor(Date.parse(now) / day) * day;
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
from: new Date(to - days * day).toISOString(),
|
|
21
|
+
to: new Date(to).toISOString(),
|
|
22
|
+
bucketMs: days === 1 ? day / 24 : days === 3 ? day / 4 : day,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Exact median with bounded sorting/merging work between caller-supplied pauses. */
|
|
26
|
+
export async function median(values, pause) {
|
|
27
|
+
if (values.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
const batchSize = 1024;
|
|
30
|
+
let sorted = [];
|
|
31
|
+
for (let start = 0; start < values.length; start += batchSize) {
|
|
32
|
+
await pause();
|
|
33
|
+
sorted.push(...values.slice(start, start + batchSize).sort((a, b) => a - b));
|
|
34
|
+
}
|
|
35
|
+
for (let width = batchSize; width < values.length; width *= 2) {
|
|
36
|
+
const merged = [];
|
|
37
|
+
for (let start = 0; start < values.length; start += width * 2) {
|
|
38
|
+
const middle = Math.min(start + width, values.length), end = Math.min(start + width * 2, values.length);
|
|
39
|
+
let left = start, right = middle;
|
|
40
|
+
while (left < middle || right < end) {
|
|
41
|
+
if (merged.length % batchSize === 0)
|
|
42
|
+
await pause();
|
|
43
|
+
if (right === end || (left < middle && sorted[left] <= sorted[right]))
|
|
44
|
+
merged.push(sorted[left++]);
|
|
45
|
+
else
|
|
46
|
+
merged.push(sorted[right++]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
sorted = merged;
|
|
50
|
+
}
|
|
51
|
+
const middle = Math.floor(sorted.length / 2);
|
|
52
|
+
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
53
|
+
}
|
package/dist/src/kernel/index.js
CHANGED
|
@@ -16,4 +16,5 @@ export * from './store/checkpoint-store.js';
|
|
|
16
16
|
export * from './store/event-journal.js';
|
|
17
17
|
export * from './subscriptions/event-processor.js';
|
|
18
18
|
export * from './subscriptions/journal-change-signal.js';
|
|
19
|
+
export * from './store/scan-journal.js';
|
|
19
20
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AAExC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,6BAA6B,CAAC;AAE5C,cAAc,uBAAuB,CAAC;AAEtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAEjF,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAEtF,cAAc,sCAAsC,CAAC;AAErD,cAAc,uCAAuC,CAAC;AAEtD,cAAc,mCAAmC,CAAC;AAElD,cAAc,kCAAkC,CAAC;AAEjD,cAAc,oBAAoB,CAAC;AAEnC,cAAc,mCAAmC,CAAC;AAElD,cAAc,+BAA+B,CAAC;AAE9C,cAAc,uCAAuC,CAAC;AAEtD,cAAc,6BAA6B,CAAC;AAE5C,cAAc,0BAA0B,CAAC;AAEzC,cAAc,oCAAoC,CAAC;AAEnD,cAAc,0CAA0C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AAExC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,6BAA6B,CAAC;AAE5C,cAAc,uBAAuB,CAAC;AAEtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAEjF,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAEtF,cAAc,sCAAsC,CAAC;AAErD,cAAc,uCAAuC,CAAC;AAEtD,cAAc,mCAAmC,CAAC;AAElD,cAAc,kCAAkC,CAAC;AAEjD,cAAc,oBAAoB,CAAC;AAEnC,cAAc,mCAAmC,CAAC;AAElD,cAAc,+BAA+B,CAAC;AAE9C,cAAc,uCAAuC,CAAC;AAEtD,cAAc,6BAA6B,CAAC;AAE5C,cAAc,0BAA0B,CAAC;AAEzC,cAAc,oCAAoC,CAAC;AAEnD,cAAc,0CAA0C,CAAC;AAEzD,cAAc,yBAAyB,CAAC"}
|
|
@@ -5,6 +5,8 @@ export declare class WrongExpectedSequenceError extends Error {
|
|
|
5
5
|
export interface EventJournal {
|
|
6
6
|
appendToStream(stream: StreamRef, expectedSequence: number, events: readonly EventData[]): Promise<readonly EventEnvelope[]>;
|
|
7
7
|
readStream(stream: StreamRef): Promise<readonly EventEnvelope[]>;
|
|
8
|
+
/** Optional bounded, cancellable snapshot reader for adapters with a streaming source. */
|
|
9
|
+
scanAll?(signal?: AbortSignal): AsyncIterable<EventEnvelope>;
|
|
8
10
|
readAll(afterGlobalPosition: number, limit?: number): Promise<readonly EventEnvelope[]>;
|
|
9
11
|
readLatest?(beforeGlobalPosition?: number, limit?: number): Promise<readonly EventEnvelope[]>;
|
|
10
12
|
latestGlobalPosition(): Promise<number>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-journal.d.ts","sourceRoot":"","sources":["../../src/store/event-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAErF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AAExD,MAAM,WAAW,YAAY;IAC3B,cAAc,CACZ,MAAM,EAAE,SAAS,EACjB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACrC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACjE,OAAO,CAAC,mBAAmB,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACxF,UAAU,CAAC,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IAI9F,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,kBAAkB,CAChB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAAC;IAGjB,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAC5C"}
|
|
1
|
+
{"version":3,"file":"event-journal.d.ts","sourceRoot":"","sources":["../../src/store/event-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAErF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AAExD,MAAM,WAAW,YAAY;IAC3B,cAAc,CACZ,MAAM,EAAE,SAAS,EACjB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACrC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACjE,0FAA0F;IAC1F,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IAC7D,OAAO,CAAC,mBAAmB,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACxF,UAAU,CAAC,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IAI9F,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,kBAAkB,CAChB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAAC;IAGjB,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAC5C"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { EventJournal } from './event-journal.js';
|
|
2
|
+
/** A bounded snapshot scan that gives hosts time to serve other work between batches. */
|
|
3
|
+
export declare function scanJournal(journal: EventJournal, signal?: AbortSignal): AsyncGenerator<import("../index.js").EventEnvelope<import("../index.js").EventData<string, unknown>, import("../index.js").StreamRef<string, string>>, void, unknown>;
|
|
4
|
+
export declare function yieldScan(signal?: AbortSignal): Promise<void>;
|
|
5
|
+
//# sourceMappingURL=scan-journal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan-journal.d.ts","sourceRoot":"","sources":["../../src/store/scan-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,yFAAyF;AACzF,wBAAuB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,WAAW,yKAyB7E;AAED,wBAAsB,SAAS,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAInE"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** A bounded snapshot scan that gives hosts time to serve other work between batches. */
|
|
2
|
+
export async function* scanJournal(journal, signal) {
|
|
3
|
+
signal?.throwIfAborted();
|
|
4
|
+
if (journal.scanAll !== undefined) {
|
|
5
|
+
for await (const event of journal.scanAll(signal)) {
|
|
6
|
+
signal?.throwIfAborted();
|
|
7
|
+
yield event;
|
|
8
|
+
}
|
|
9
|
+
signal?.throwIfAborted();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const end = await journal.latestGlobalPosition();
|
|
13
|
+
let position = 0;
|
|
14
|
+
while (position < end) {
|
|
15
|
+
await yieldScan(signal);
|
|
16
|
+
const batch = await journal.readAll(position, 100);
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
if (batch.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
for (const event of batch) {
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
if (event.globalPosition > end)
|
|
23
|
+
return;
|
|
24
|
+
yield event;
|
|
25
|
+
position = event.globalPosition;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
signal?.throwIfAborted();
|
|
29
|
+
}
|
|
30
|
+
export async function yieldScan(signal) {
|
|
31
|
+
signal?.throwIfAborted();
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
}
|
|
@@ -28,6 +28,10 @@ export declare class FileEventJournal implements EventJournal {
|
|
|
28
28
|
appendToStream(stream: StreamRef, expectedSequence: number, drafts: readonly EventData[]): Promise<readonly EventEnvelope[]>;
|
|
29
29
|
private nextSegmentFile;
|
|
30
30
|
readStream(stream: StreamRef): Promise<readonly EventEnvelope<EventData<string, unknown>, StreamRef<string, string>>[]>;
|
|
31
|
+
/** Live readers bypass the full-history cache and stop at captured segment sizes. */
|
|
32
|
+
scanAll(signal?: AbortSignal): AsyncIterable<EventEnvelope>;
|
|
33
|
+
private scanSegment;
|
|
34
|
+
private decodeScanRecord;
|
|
31
35
|
readAll(after: number, limit?: number): Promise<EventEnvelope<EventData<string, unknown>, StreamRef<string, string>>[]>;
|
|
32
36
|
readLatest(beforeGlobalPosition?: number, limit?: number): Promise<EventEnvelope<EventData<string, unknown>, StreamRef<string, string>>[]>;
|
|
33
37
|
latestGlobalPosition(): Promise<number>;
|
package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-event-journal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-event-journal.d.ts","sourceRoot":"","sources":["../src/file-event-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,SAAS,EACV,MAAM,qBAAqB,CAAC;AAW7B,MAAM,WAAW,uBAAuB;IACtC,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CACrD;AAED,MAAM,MAAM,8BAA8B,GAAG,CAC3C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,IAAI,KACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEtC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,cAAc,CAAC,EAAE,8BAA8B,CAAC;IACzD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;CACvC;AAOD,qBAAa,gBAAiB,YAAW,YAAY;IAIjD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;gBAG1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,EACpB,OAAO,GAAE,uBAA4B;IAKxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAsC;IACzE,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,qBAAqB,CAA4B;IAEzD,IAAI,YAAY,IAAI,mBAAmB,CAEtC;IAED,OAAO,CAAC,MAAM,CAOA;IAWd,OAAO,CAAC,YAAY,CAC4E;IAEhG,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,gBAAgB,CAC4D;IAEpF,OAAO,CAAC,oBAAoB,CAA2C;IACvE,OAAO,CAAC,yBAAyB,CAAK;IAEhC,cAAc,CAClB,MAAM,EAAE,SAAS,EACjB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC;YAwCtB,eAAe;IAsBvB,UAAU,CAAC,MAAM,EAAE,SAAS;
|
|
1
|
+
{"version":3,"file":"file-event-journal.d.ts","sourceRoot":"","sources":["../src/file-event-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,SAAS,EACV,MAAM,qBAAqB,CAAC;AAW7B,MAAM,WAAW,uBAAuB;IACtC,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CACrD;AAED,MAAM,MAAM,8BAA8B,GAAG,CAC3C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,IAAI,KACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEtC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,cAAc,CAAC,EAAE,8BAA8B,CAAC;IACzD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;CACvC;AAOD,qBAAa,gBAAiB,YAAW,YAAY;IAIjD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;gBAG1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,EACpB,OAAO,GAAE,uBAA4B;IAKxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAsC;IACzE,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,qBAAqB,CAA4B;IAEzD,IAAI,YAAY,IAAI,mBAAmB,CAEtC;IAED,OAAO,CAAC,MAAM,CAOA;IAWd,OAAO,CAAC,YAAY,CAC4E;IAEhG,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,gBAAgB,CAC4D;IAEpF,OAAO,CAAC,oBAAoB,CAA2C;IACvE,OAAO,CAAC,yBAAyB,CAAK;IAEhC,cAAc,CAClB,MAAM,EAAE,SAAS,EACjB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC;YAwCtB,eAAe;IAsBvB,UAAU,CAAC,MAAM,EAAE,SAAS;IA4BlC,qFAAqF;IAC9E,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,aAAa,CAAC;YAcnD,WAAW;IAgC1B,OAAO,CAAC,gBAAgB;IAoBlB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAA2B;IAyBvD,UAAU,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,KAAK,SAA2B;IAW1E,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC;IAKvC,kBAAkB,CACtB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAWhB,OAAO,CAAC,mBAAmB;YAUb,kBAAkB;YA4BlB,WAAW;IAkDzB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,qBAAqB;YAQf,eAAe;YAMf,YAAY;YAYZ,IAAI;YAYJ,kBAAkB;YAmBlB,kBAAkB;IAkBhC,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,0BAA0B;YAQpB,mBAAmB;IA0BjC,OAAO,CAAC,mBAAmB;YAwBb,eAAe;CAkD9B"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { decodeEventEnvelope, WrongExpectedSequenceError } from '@atolis-hq/eventing';
|
|
1
|
+
import { decodeEventEnvelope, WrongExpectedSequenceError, yieldScan } from '@atolis-hq/eventing';
|
|
2
2
|
import { InProcessJournalChangeSignal } from '@atolis-hq/eventing/memory';
|
|
3
|
-
import { watch } from 'node:fs';
|
|
3
|
+
import { createReadStream, watch } from 'node:fs';
|
|
4
4
|
import { mkdir, open, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { performance } from 'node:perf_hooks';
|
|
@@ -115,6 +115,64 @@ export class FileEventJournal {
|
|
|
115
115
|
}
|
|
116
116
|
return (await this.scan(entries)).filter((event) => key(event.stream) === streamKey);
|
|
117
117
|
}
|
|
118
|
+
/** Live readers bypass the full-history cache and stop at captured segment sizes. */
|
|
119
|
+
async *scanAll(signal) {
|
|
120
|
+
signal?.throwIfAborted();
|
|
121
|
+
const entries = await this.readCurrentEntries();
|
|
122
|
+
let position = 0;
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
let lineNumber = 0;
|
|
125
|
+
for await (const line of this.scanSegment(entry, signal)) {
|
|
126
|
+
if (position % 100 === 0)
|
|
127
|
+
await yieldScan(signal);
|
|
128
|
+
signal?.throwIfAborted();
|
|
129
|
+
yield this.decodeScanRecord(entry.file, ++lineNumber, line, ++position);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async *scanSegment(entry, signal) {
|
|
134
|
+
if (entry.size === 0)
|
|
135
|
+
return;
|
|
136
|
+
const source = createReadStream(join(this.root, 'events', entry.file), {
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
end: entry.size - 1,
|
|
139
|
+
...(signal === undefined ? {} : { signal }),
|
|
140
|
+
});
|
|
141
|
+
let pending = '', bytes = 0;
|
|
142
|
+
try {
|
|
143
|
+
for await (const chunk of source) {
|
|
144
|
+
signal?.throwIfAborted();
|
|
145
|
+
const text = String(chunk);
|
|
146
|
+
bytes += Buffer.byteLength(text);
|
|
147
|
+
pending += text;
|
|
148
|
+
let offset = 0, newline = pending.indexOf('\n');
|
|
149
|
+
while (newline !== -1) {
|
|
150
|
+
yield pending.slice(offset, newline);
|
|
151
|
+
offset = newline + 1;
|
|
152
|
+
newline = pending.indexOf('\n', offset);
|
|
153
|
+
}
|
|
154
|
+
pending = pending.slice(offset);
|
|
155
|
+
}
|
|
156
|
+
signal?.throwIfAborted();
|
|
157
|
+
if (pending !== '' || bytes !== entry.size)
|
|
158
|
+
throw new Error(`Incomplete trailing line in ${entry.file}`);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
source.destroy();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
decodeScanRecord(file, lineNumber, line, position) {
|
|
165
|
+
let input;
|
|
166
|
+
try {
|
|
167
|
+
input = JSON.parse(line);
|
|
168
|
+
const event = decodeEventRecord(input);
|
|
169
|
+
validateEnvelope(event, position);
|
|
170
|
+
return event;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
throw new Error(`Corrupt event at ${file}:${lineNumber}${eventContext(input)}: ${error.message}`, { cause: error });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
118
176
|
async readAll(after, limit = Number.POSITIVE_INFINITY) {
|
|
119
177
|
const entries = await this.readEntriesForRead();
|
|
120
178
|
if (this.cached !== undefined && sameEntries(this.cached.entries, entries))
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type EventJournal } from '@atolis-hq/eventing';
|
|
2
|
+
import { type ReportQuery } from '../../kernel/index.js';
|
|
3
|
+
export declare function queryWorkCompletionSummary(journal: EventJournal, query: ReportQuery): Promise<{
|
|
4
|
+
workItemsDone: number;
|
|
5
|
+
medianEndToEndDurationMs: number | null;
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { scanJournal, yieldScan } from '@atolis-hq/eventing';
|
|
2
|
+
import { median } from '../../kernel/index.js';
|
|
3
|
+
import { selectWorkEvent, WorkEventType } from '../../work/index.js';
|
|
4
|
+
import { selectWorkflowOrchestrationEvent } from '../contracts/event-decoder.js';
|
|
5
|
+
import { OrchestrationEventType } from '../contracts/events.js';
|
|
6
|
+
export async function queryWorkCompletionSummary(journal, query) {
|
|
7
|
+
const facts = { created: new Map(), instances: new Map(), completed: new Map() };
|
|
8
|
+
for await (const event of scanJournal(journal, query.signal))
|
|
9
|
+
acceptFact(facts, event);
|
|
10
|
+
const from = Date.parse(query.window.from), to = Date.parse(query.window.to);
|
|
11
|
+
let workItemsDone = 0, processed = 0;
|
|
12
|
+
const durations = [];
|
|
13
|
+
for (const [id, end] of facts.completed) {
|
|
14
|
+
if (processed++ % 100 === 0)
|
|
15
|
+
await yieldScan(query.signal);
|
|
16
|
+
if (!(end >= from && end < to))
|
|
17
|
+
continue;
|
|
18
|
+
workItemsDone++;
|
|
19
|
+
const start = facts.created.get(id);
|
|
20
|
+
if (start !== undefined && Number.isFinite(start) && end >= start)
|
|
21
|
+
durations.push(end - start);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
workItemsDone,
|
|
25
|
+
medianEndToEndDurationMs: await median(durations, () => yieldScan(query.signal)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function acceptFact(facts, event) {
|
|
29
|
+
const work = selectWorkEvent(event);
|
|
30
|
+
if (work?.event.eventType === WorkEventType.ItemCreated)
|
|
31
|
+
facts.created.set(work.stream.id, Date.parse(work.event.occurredAt));
|
|
32
|
+
const workflow = selectWorkflowOrchestrationEvent(event);
|
|
33
|
+
if (workflow === null)
|
|
34
|
+
return;
|
|
35
|
+
switch (workflow.event.eventType) {
|
|
36
|
+
case OrchestrationEventType.InstanceStarted:
|
|
37
|
+
if (!('parentWorkflowInstanceId' in workflow.event.payload))
|
|
38
|
+
facts.instances.set(workflow.stream.id, workflow.event.payload.workItemId);
|
|
39
|
+
return;
|
|
40
|
+
case OrchestrationEventType.InstanceCompleted: {
|
|
41
|
+
const id = facts.instances.get(workflow.stream.id);
|
|
42
|
+
if (id !== undefined && !facts.completed.has(id))
|
|
43
|
+
facts.completed.set(id, Date.parse(workflow.event.occurredAt));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|