@atolis-hq/wake 0.3.76 → 0.3.77
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/dist/src/bootstrap/persistence-composition.js +1 -0
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/execution/application/run-repository.js +16 -10
- package/dist/src/integrations/application/artifact-registration-reactor.js +15 -9
- package/dist/src/integrations/github/contracts/config.js +8 -1
- package/dist/src/integrations/github/infrastructure/source.js +10 -1
- package/dist/src/kernel/index.js +1 -0
- package/dist/src/kernel/infrastructure/cached-journal-view.js +21 -0
- package/dist/src/orchestration/application/orchestration-repository.js +24 -19
- package/dist/src/persistence/filesystem/file-event-journal.js +4 -0
- package/dist/src/persistence/memory/in-memory-event-journal.js +3 -0
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ function serializeJournalAppends(journal) {
|
|
|
12
12
|
},
|
|
13
13
|
readStream: (stream) => journal.readStream(stream),
|
|
14
14
|
readAll: (afterGlobalPosition, limit) => journal.readAll(afterGlobalPosition, limit),
|
|
15
|
+
latestGlobalPosition: () => journal.latestGlobalPosition(),
|
|
15
16
|
...(journal.readLatest === undefined
|
|
16
17
|
? {}
|
|
17
18
|
: {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { cachedJournalView, } from '../../kernel/index.js';
|
|
1
2
|
import { decodeRunExecutionEvent, } from '../contracts/events.js';
|
|
2
3
|
import { runId } from '../contracts/identifiers.js';
|
|
3
4
|
import { isRunStream, runStream } from '../contracts/streams.js';
|
|
4
5
|
import { foldRun } from '../domain/run.js';
|
|
5
6
|
export class RunRepository {
|
|
6
7
|
journal;
|
|
8
|
+
runs;
|
|
7
9
|
constructor(journal) {
|
|
8
10
|
this.journal = journal;
|
|
11
|
+
this.runs = cachedJournalView(journal, deriveRuns);
|
|
9
12
|
}
|
|
10
13
|
async load(runId) {
|
|
11
14
|
const events = await this.journal.readStream(runStream(runId));
|
|
@@ -17,18 +20,21 @@ export class RunRepository {
|
|
|
17
20
|
return events.map(decodeRunExecutionEvent);
|
|
18
21
|
}
|
|
19
22
|
async list(activationId) {
|
|
20
|
-
const
|
|
21
|
-
for (const event of await this.journal.readAll(0)) {
|
|
22
|
-
if (!isRunStream(event.stream))
|
|
23
|
-
continue;
|
|
24
|
-
const id = runId(event.stream.id);
|
|
25
|
-
const events = grouped.get(id) ?? [];
|
|
26
|
-
events.push(decodeRunExecutionEvent(event));
|
|
27
|
-
grouped.set(id, events);
|
|
28
|
-
}
|
|
29
|
-
const runs = (await Promise.all([...grouped.values()].map(foldRun))).filter((run) => run !== null);
|
|
23
|
+
const runs = await this.runs.get();
|
|
30
24
|
return activationId === undefined
|
|
31
25
|
? runs
|
|
32
26
|
: runs.filter((run) => run.activationId === activationId);
|
|
33
27
|
}
|
|
34
28
|
}
|
|
29
|
+
function deriveRuns(events) {
|
|
30
|
+
const grouped = new Map();
|
|
31
|
+
for (const event of events) {
|
|
32
|
+
if (!isRunStream(event.stream))
|
|
33
|
+
continue;
|
|
34
|
+
const id = runId(event.stream.id);
|
|
35
|
+
const decoded = grouped.get(id) ?? [];
|
|
36
|
+
decoded.push(decodeRunExecutionEvent(event));
|
|
37
|
+
grouped.set(id, decoded);
|
|
38
|
+
}
|
|
39
|
+
return [...grouped.values()].map(foldRun).filter((run) => run !== null);
|
|
40
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { reportedArtifactSchema } from '../../activities/index.js';
|
|
2
|
-
import { createEventDraft, EventActorKind, EventSourceKind, } from '../../kernel/index.js';
|
|
2
|
+
import { cachedJournalView, createEventDraft, EventActorKind, EventSourceKind, } from '../../kernel/index.js';
|
|
3
3
|
import { OrchestrationEventType, selectWorkflowOrchestrationEvent, workflowInstanceStream, } from '../../orchestration/index.js';
|
|
4
4
|
import { ResourceCorrelationProvenance, ResourceCorrelationRole, resourceId, resourceKind, ResourceStreamKind, } from '../../resources/index.js';
|
|
5
5
|
import { ArtifactEventType, decodeArtifactEvent, } from '../contracts/artifact-events.js';
|
|
@@ -7,8 +7,10 @@ import { ArtifactVerificationResult, ArtifactVerificationStatus, } from '../cont
|
|
|
7
7
|
import { integrationStream } from '../contracts/streams.js';
|
|
8
8
|
export class ArtifactRegistrationReactor {
|
|
9
9
|
dependencies;
|
|
10
|
+
latestUnresolved;
|
|
10
11
|
constructor(dependencies) {
|
|
11
12
|
this.dependencies = dependencies;
|
|
13
|
+
this.latestUnresolved = cachedJournalView(dependencies.journal, deriveLatestUnresolved);
|
|
12
14
|
}
|
|
13
15
|
async runOnce(limit = 100) {
|
|
14
16
|
const consumer = 'reactor:artifact-registration';
|
|
@@ -43,14 +45,7 @@ export class ArtifactRegistrationReactor {
|
|
|
43
45
|
}, branch, workItemId, 1);
|
|
44
46
|
}
|
|
45
47
|
async reconcileAmbiguous() {
|
|
46
|
-
const latest =
|
|
47
|
-
for (const event of await this.dependencies.journal.readAll(0)) {
|
|
48
|
-
if (event.eventType !== ArtifactEventType.VerificationUnresolved)
|
|
49
|
-
continue;
|
|
50
|
-
const decoded = decodeArtifactEvent(event);
|
|
51
|
-
const key = `${decoded.causationId}:${decoded.payload.artifact.externalKey.adapter}:${decoded.payload.artifact.externalKey.key}`;
|
|
52
|
-
latest.set(key, decoded);
|
|
53
|
-
}
|
|
48
|
+
const latest = await this.latestUnresolved.get();
|
|
54
49
|
for (const event of latest.values()) {
|
|
55
50
|
if (event.payload.status !== ArtifactVerificationStatus.Ambiguous || event.payload.escalated)
|
|
56
51
|
continue;
|
|
@@ -137,6 +132,17 @@ export class ArtifactRegistrationReactor {
|
|
|
137
132
|
return started?.payload.workItemId ?? null;
|
|
138
133
|
}
|
|
139
134
|
}
|
|
135
|
+
function deriveLatestUnresolved(events) {
|
|
136
|
+
const latest = new Map();
|
|
137
|
+
for (const event of events) {
|
|
138
|
+
if (event.eventType !== ArtifactEventType.VerificationUnresolved)
|
|
139
|
+
continue;
|
|
140
|
+
const decoded = decodeArtifactEvent(event);
|
|
141
|
+
const key = `${decoded.causationId}:${decoded.payload.artifact.externalKey.adapter}:${decoded.payload.artifact.externalKey.key}`;
|
|
142
|
+
latest.set(key, decoded);
|
|
143
|
+
}
|
|
144
|
+
return latest;
|
|
145
|
+
}
|
|
140
146
|
function reportedArtifacts(value) {
|
|
141
147
|
if (typeof value !== 'object' || value === null || !('reportedArtifacts' in value))
|
|
142
148
|
return [];
|
|
@@ -37,9 +37,16 @@ export const gitHubConfigSchema = z
|
|
|
37
37
|
maxConcurrent: z.number().int().positive().default(4),
|
|
38
38
|
commentPageSize: z.number().int().positive().max(100).default(25),
|
|
39
39
|
lookbackMs: z.number().int().nonnegative().default(60_000),
|
|
40
|
+
intervalMs: z.number().int().positive().default(30_000),
|
|
40
41
|
})
|
|
41
42
|
.strict()
|
|
42
|
-
.default({
|
|
43
|
+
.default({
|
|
44
|
+
maxPerRepo: 25,
|
|
45
|
+
maxConcurrent: 4,
|
|
46
|
+
commentPageSize: 25,
|
|
47
|
+
lookbackMs: 60_000,
|
|
48
|
+
intervalMs: 30_000,
|
|
49
|
+
}),
|
|
43
50
|
intake: z.array(intakeRuleSchema).default([]),
|
|
44
51
|
publication: z
|
|
45
52
|
.object({ postStatusComments: z.boolean().default(true) })
|
|
@@ -16,8 +16,17 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
16
16
|
// so it's safe for it to reset on restart.
|
|
17
17
|
const lastEventIds = new Map();
|
|
18
18
|
const pendingWatermarks = new Map();
|
|
19
|
+
const now = state?.now ?? Date.now;
|
|
20
|
+
// In-memory only: an extra real poll after a process restart is harmless,
|
|
21
|
+
// the same tradeoff already made for lastEventIds above.
|
|
22
|
+
let lastPolledAt;
|
|
19
23
|
return {
|
|
20
24
|
async poll(signal) {
|
|
25
|
+
const currentTime = now();
|
|
26
|
+
if (lastPolledAt !== undefined && currentTime - lastPolledAt < config.polling.intervalMs) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
lastPolledAt = currentTime;
|
|
21
30
|
const perRepository = await Promise.all(config.repositories.map(async ({ owner, repo }) => pollRepository({
|
|
22
31
|
client: limitGitHubSourceClient(client, requests),
|
|
23
32
|
config,
|
|
@@ -26,7 +35,7 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
26
35
|
owner,
|
|
27
36
|
repo,
|
|
28
37
|
watermark: await loadWatermark(state?.checkpoints, adapter, owner, repo),
|
|
29
|
-
now
|
|
38
|
+
now,
|
|
30
39
|
health,
|
|
31
40
|
})));
|
|
32
41
|
for (const result of perRepository)
|
package/dist/src/kernel/index.js
CHANGED
|
@@ -13,5 +13,6 @@ export * from './contracts/vocabulary.js';
|
|
|
13
13
|
export * from './domain/event-envelope.js';
|
|
14
14
|
export * from './domain/match-mode.js';
|
|
15
15
|
export * from './domain/relation.js';
|
|
16
|
+
export * from './infrastructure/cached-journal-view.js';
|
|
16
17
|
export * from './infrastructure/system-clock.js';
|
|
17
18
|
export * from './infrastructure/ulid-id-generator.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Memoizes a full-journal derivation — a fold, decode, or index built from
|
|
2
|
+
// every event in history — so a resident loop calling it every tick only
|
|
3
|
+
// pays that cost when the journal has actually moved since the last call.
|
|
4
|
+
// Same gate ProjectionRunner keeps for itself (a last-seen global position),
|
|
5
|
+
// generalized for callers that want a materialized view rather than an
|
|
6
|
+
// incremental batch. Depends only on the EventJournal port, not on any
|
|
7
|
+
// concrete persistence implementation, so it lives in kernel rather than
|
|
8
|
+
// persistence — domain and adapter modules may call it directly.
|
|
9
|
+
export function cachedJournalView(journal, derive) {
|
|
10
|
+
let cache;
|
|
11
|
+
return {
|
|
12
|
+
async get() {
|
|
13
|
+
const position = await journal.latestGlobalPosition();
|
|
14
|
+
if (cache !== undefined && cache.position === position)
|
|
15
|
+
return cache.value;
|
|
16
|
+
const value = await derive(await journal.readAll(0));
|
|
17
|
+
cache = { position, value };
|
|
18
|
+
return value;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { cachedJournalView, } from '../../kernel/index.js';
|
|
1
2
|
import { decodeOrchestrationEvent, selectWorkflowOrchestrationEvent, } from '../contracts/event-decoder.js';
|
|
2
3
|
import { workflowInstanceId } from '../contracts/identifiers.js';
|
|
3
4
|
import { isWorkflowInstanceStream, workflowInstanceStream } from '../contracts/streams.js';
|
|
4
5
|
import { foldWorkflowInstance } from '../domain/workflow-instance.js';
|
|
5
6
|
export class OrchestrationRepository {
|
|
6
7
|
journal;
|
|
8
|
+
listed;
|
|
7
9
|
constructor(journal) {
|
|
8
10
|
this.journal = journal;
|
|
11
|
+
this.listed = cachedJournalView(journal, (events) => deriveList(events));
|
|
9
12
|
}
|
|
10
13
|
async load(id) {
|
|
11
14
|
const events = await this.journal.readStream(workflowInstanceStream(workflowInstanceId(id)));
|
|
@@ -25,27 +28,29 @@ export class OrchestrationRepository {
|
|
|
25
28
|
return events.map(decodeOrchestrationEvent).filter(isWorkflowEvent);
|
|
26
29
|
}
|
|
27
30
|
async list() {
|
|
28
|
-
|
|
29
|
-
const streams = new Map();
|
|
30
|
-
for (const event of events) {
|
|
31
|
-
if (!isWorkflowInstanceStream(event.stream))
|
|
32
|
-
continue;
|
|
33
|
-
const existing = streams.get(event.stream.id);
|
|
34
|
-
if (existing === undefined)
|
|
35
|
-
streams.set(event.stream.id, [event]);
|
|
36
|
-
else
|
|
37
|
-
existing.push(event);
|
|
38
|
-
}
|
|
39
|
-
// `sequence` counts every event on the stream, matching readStream, while the
|
|
40
|
-
// fold sees only owned orchestration events — exactly what load() computes.
|
|
41
|
-
return [...streams.values()].map((streamEvents) => ({
|
|
42
|
-
sequence: streamEvents.length,
|
|
43
|
-
view: foldWorkflowInstance(streamEvents
|
|
44
|
-
.map(selectWorkflowOrchestrationEvent)
|
|
45
|
-
.filter((event) => event !== null && isWorkflowInstanceStream(event.stream))),
|
|
46
|
-
}));
|
|
31
|
+
return this.listed.get();
|
|
47
32
|
}
|
|
48
33
|
}
|
|
49
34
|
function isWorkflowEvent(event) {
|
|
50
35
|
return isWorkflowInstanceStream(event.stream);
|
|
51
36
|
}
|
|
37
|
+
// `sequence` counts every event on the stream, matching readStream, while the
|
|
38
|
+
// fold sees only owned orchestration events — exactly what load() computes.
|
|
39
|
+
function deriveList(events) {
|
|
40
|
+
const streams = new Map();
|
|
41
|
+
for (const event of events) {
|
|
42
|
+
if (!isWorkflowInstanceStream(event.stream))
|
|
43
|
+
continue;
|
|
44
|
+
const existing = streams.get(event.stream.id);
|
|
45
|
+
if (existing === undefined)
|
|
46
|
+
streams.set(event.stream.id, [event]);
|
|
47
|
+
else
|
|
48
|
+
existing.push(event);
|
|
49
|
+
}
|
|
50
|
+
return [...streams.values()].map((streamEvents) => ({
|
|
51
|
+
sequence: streamEvents.length,
|
|
52
|
+
view: foldWorkflowInstance(streamEvents
|
|
53
|
+
.map(selectWorkflowOrchestrationEvent)
|
|
54
|
+
.filter((event) => event !== null && isWorkflowInstanceStream(event.stream))),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
@@ -73,6 +73,10 @@ export class FileEventJournal {
|
|
|
73
73
|
.slice(-limit)
|
|
74
74
|
.reverse();
|
|
75
75
|
}
|
|
76
|
+
async latestGlobalPosition() {
|
|
77
|
+
const events = await this.scan();
|
|
78
|
+
return events.at(-1)?.globalPosition ?? 0;
|
|
79
|
+
}
|
|
76
80
|
async scan() {
|
|
77
81
|
const directory = join(this.root, 'events');
|
|
78
82
|
let files;
|
|
@@ -55,6 +55,9 @@ export class InMemoryEventJournal {
|
|
|
55
55
|
.slice(-limit)
|
|
56
56
|
.reverse();
|
|
57
57
|
}
|
|
58
|
+
async latestGlobalPosition() {
|
|
59
|
+
return this.events.length;
|
|
60
|
+
}
|
|
58
61
|
rejectChangedEventIds(drafts, existingEvents) {
|
|
59
62
|
for (const [index, draft] of drafts.entries()) {
|
|
60
63
|
const existing = existingEvents[index];
|