@atolis-hq/wake 0.3.75 → 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/surface-api-applications.js +10 -1
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/execution/application/run-repository.js +16 -10
- package/dist/src/integrations/application/adapter-health-tracker.js +43 -0
- package/dist/src/integrations/application/artifact-registration-reactor.js +15 -9
- package/dist/src/integrations/contracts/provider.js +8 -5
- package/dist/src/integrations/github/contracts/config.js +8 -1
- package/dist/src/integrations/github/infrastructure/adapter-health-registry.js +0 -0
- package/dist/src/integrations/github/infrastructure/source.js +27 -6
- package/dist/src/integrations/github/provider.js +22 -6
- 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/dist/src/surfaces/web-assets/assets/{index-DHGUQVg7.js → index-CBE1yusZ.js} +1 -1
- package/dist/src/surfaces/web-assets/index.html +1 -1
- 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
|
: {
|
|
@@ -203,12 +203,21 @@ function createSystemApplications(root, now) {
|
|
|
203
203
|
detail: `rolled back from ${selfUpdateFailure.tag} at ${selfUpdateFailure.occurredAt}: ${selfUpdateFailure.message}`,
|
|
204
204
|
},
|
|
205
205
|
];
|
|
206
|
+
const adapters = root.providers.flatMap((instance) => (instance.health?.() ?? []).map((check) => ({
|
|
207
|
+
adapter: instance.adapter,
|
|
208
|
+
provider: instance.provider,
|
|
209
|
+
...check,
|
|
210
|
+
})));
|
|
206
211
|
return {
|
|
207
212
|
data: {
|
|
208
|
-
status: checks.some((check) => check.status === 'degraded')
|
|
213
|
+
status: checks.some((check) => check.status === 'degraded') ||
|
|
214
|
+
adapters.some((check) => check.status === 'degraded')
|
|
215
|
+
? 'degraded'
|
|
216
|
+
: 'ok',
|
|
209
217
|
version: wakeVersion,
|
|
210
218
|
checkedAt,
|
|
211
219
|
checks,
|
|
220
|
+
adapters,
|
|
212
221
|
},
|
|
213
222
|
meta: sampledMeta(checkedAt),
|
|
214
223
|
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const DEFINITIVE_STATUSES = new Set([401, 403, 429]);
|
|
2
|
+
export function createAdapterHealthTracker(options = {}) {
|
|
3
|
+
const now = options.now ?? Date.now;
|
|
4
|
+
const threshold = options.degradeAfterConsecutiveFailures ?? 3;
|
|
5
|
+
let successCount = 0;
|
|
6
|
+
let failureCount = 0;
|
|
7
|
+
let consecutiveFailures = 0;
|
|
8
|
+
let degradedDetail;
|
|
9
|
+
return {
|
|
10
|
+
recordSuccess() {
|
|
11
|
+
successCount += 1;
|
|
12
|
+
consecutiveFailures = 0;
|
|
13
|
+
degradedDetail = undefined;
|
|
14
|
+
},
|
|
15
|
+
recordFailure(error) {
|
|
16
|
+
failureCount += 1;
|
|
17
|
+
consecutiveFailures += 1;
|
|
18
|
+
const status = statusOf(error);
|
|
19
|
+
const occurredAt = new Date(now()).toISOString();
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
if (status !== undefined && DEFINITIVE_STATUSES.has(status)) {
|
|
22
|
+
degradedDetail = `${status} at ${occurredAt}: ${message}`;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (consecutiveFailures >= threshold) {
|
|
26
|
+
degradedDetail = `${consecutiveFailures} consecutive failures, last: ${status ?? 'error'} at ${occurredAt}: ${message}`;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
snapshot() {
|
|
30
|
+
return {
|
|
31
|
+
status: degradedDetail === undefined ? 'ok' : 'degraded',
|
|
32
|
+
...(degradedDetail === undefined ? {} : { detail: degradedDetail }),
|
|
33
|
+
successCount,
|
|
34
|
+
failureCount,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function statusOf(error) {
|
|
40
|
+
if (typeof error !== 'object' || error === null || !('status' in error))
|
|
41
|
+
return undefined;
|
|
42
|
+
return typeof error.status === 'number' ? error.status : undefined;
|
|
43
|
+
}
|
|
@@ -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 [];
|
|
@@ -18,11 +18,14 @@ export class ProviderRegistry {
|
|
|
18
18
|
throw new Error(`Provider ${provider} is not registered`);
|
|
19
19
|
const adapter = adapterId(name);
|
|
20
20
|
try {
|
|
21
|
-
instances.push(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
instances.push({
|
|
22
|
+
...definition.create({
|
|
23
|
+
adapter,
|
|
24
|
+
config: definition.parseConfig(entry),
|
|
25
|
+
...(services === undefined ? {} : { services }),
|
|
26
|
+
}),
|
|
27
|
+
provider,
|
|
28
|
+
});
|
|
26
29
|
}
|
|
27
30
|
catch (error) {
|
|
28
31
|
failures.push({
|
|
@@ -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) })
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GitHubEventType } from '../contracts/events.js';
|
|
2
|
+
import { createGitHubAdapterHealthRegistry, } from './adapter-health-registry.js';
|
|
2
3
|
import { issueCommentEventsFor, reviewCommentEventsFor, reviewEventsFor, } from './comment-source.js';
|
|
3
4
|
import { issueObservation } from './issue-source.js';
|
|
4
5
|
import { loadWatermark, overlapSince, reportPartialPollFailure, watermarkCheckpoint, } from './poll-watermark.js';
|
|
@@ -7,6 +8,7 @@ import { createGitHubRequestCoordinator, } from './request-coordinator.js';
|
|
|
7
8
|
export function createGitHubSource(config, client, adapter, requests = createGitHubRequestCoordinator({
|
|
8
9
|
maxConcurrent: config.polling.maxConcurrent,
|
|
9
10
|
}), state) {
|
|
11
|
+
const health = state?.health ?? createGitHubAdapterHealthRegistry(config.repositories);
|
|
10
12
|
// Draft eventIds are already content fingerprints (see issue-source.ts/pr-source.ts),
|
|
11
13
|
// so the journal itself is idempotent per item. This cache only avoids re-appending
|
|
12
14
|
// (and re-triggering downstream translation) an unchanged item on every poll within
|
|
@@ -14,8 +16,17 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
14
16
|
// so it's safe for it to reset on restart.
|
|
15
17
|
const lastEventIds = new Map();
|
|
16
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;
|
|
17
23
|
return {
|
|
18
24
|
async poll(signal) {
|
|
25
|
+
const currentTime = now();
|
|
26
|
+
if (lastPolledAt !== undefined && currentTime - lastPolledAt < config.polling.intervalMs) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
lastPolledAt = currentTime;
|
|
19
30
|
const perRepository = await Promise.all(config.repositories.map(async ({ owner, repo }) => pollRepository({
|
|
20
31
|
client: limitGitHubSourceClient(client, requests),
|
|
21
32
|
config,
|
|
@@ -24,7 +35,8 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
24
35
|
owner,
|
|
25
36
|
repo,
|
|
26
37
|
watermark: await loadWatermark(state?.checkpoints, adapter, owner, repo),
|
|
27
|
-
now
|
|
38
|
+
now,
|
|
39
|
+
health,
|
|
28
40
|
})));
|
|
29
41
|
for (const result of perRepository)
|
|
30
42
|
if (result.succeeded)
|
|
@@ -40,10 +52,11 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
40
52
|
});
|
|
41
53
|
},
|
|
42
54
|
async markPollPersisted() {
|
|
43
|
-
if (state === undefined)
|
|
55
|
+
if (state?.checkpoints === undefined)
|
|
44
56
|
return;
|
|
57
|
+
const checkpoints = state.checkpoints;
|
|
45
58
|
for (const [repository, watermark] of pendingWatermarks) {
|
|
46
|
-
await
|
|
59
|
+
await checkpoints.save(watermarkCheckpoint(adapter, repository), watermark);
|
|
47
60
|
pendingWatermarks.delete(repository);
|
|
48
61
|
}
|
|
49
62
|
},
|
|
@@ -72,7 +85,7 @@ function limitGitHubSourceClient(client, requests) {
|
|
|
72
85
|
};
|
|
73
86
|
}
|
|
74
87
|
async function pollRepository(input) {
|
|
75
|
-
const { client, config, adapter, signal, owner, repo } = input;
|
|
88
|
+
const { client, config, adapter, signal, owner, repo, health } = input;
|
|
76
89
|
const queriedAt = input.now();
|
|
77
90
|
const context = {
|
|
78
91
|
client,
|
|
@@ -87,10 +100,18 @@ async function pollRepository(input) {
|
|
|
87
100
|
client.listIssues(owner, repo, config.polling.maxPerRepo, since),
|
|
88
101
|
client.listPullRequests(owner, repo, config.polling.maxPerRepo),
|
|
89
102
|
]);
|
|
90
|
-
if (
|
|
103
|
+
if (isFulfilled(issuesResult))
|
|
104
|
+
health.recordSuccess(context.repository, 'poll');
|
|
105
|
+
else {
|
|
91
106
|
reportPartialPollFailure(context.repository, 'issues');
|
|
92
|
-
|
|
107
|
+
health.recordFailure(context.repository, 'poll', issuesResult.reason);
|
|
108
|
+
}
|
|
109
|
+
if (isFulfilled(pullRequestsResult))
|
|
110
|
+
health.recordSuccess(context.repository, 'poll');
|
|
111
|
+
else {
|
|
93
112
|
reportPartialPollFailure(context.repository, 'pull requests');
|
|
113
|
+
health.recordFailure(context.repository, 'poll', pullRequestsResult.reason);
|
|
114
|
+
}
|
|
94
115
|
const issues = isFulfilled(issuesResult) ? issuesResult.value : [];
|
|
95
116
|
const pullRequestPayloads = isFulfilled(pullRequestsResult)
|
|
96
117
|
? pullRequestsResult.value.filter((pullRequest) => since === undefined || pullRequest.updated_at >= since)
|
|
@@ -6,6 +6,7 @@ import { translateGitHubOutbound } from './application/outbound-translator.js';
|
|
|
6
6
|
import { createGitHubWakeLabelReconciler } from './application/wake-labels.js';
|
|
7
7
|
import { gitHubConfigSchema } from './contracts/config.js';
|
|
8
8
|
import { GitHubEventType } from './contracts/events.js';
|
|
9
|
+
import { createGitHubAdapterHealthRegistry } from './infrastructure/adapter-health-registry.js';
|
|
9
10
|
import { createGitHubClient } from './infrastructure/client.js';
|
|
10
11
|
import { createGitHubDelivery } from './infrastructure/delivery.js';
|
|
11
12
|
import { resolveGitHubCliToken } from './infrastructure/gh-auth.js';
|
|
@@ -24,12 +25,14 @@ export const gitHubProviderDefinition = {
|
|
|
24
25
|
const requests = createGitHubRequestCoordinator({
|
|
25
26
|
maxConcurrent: config.polling.maxConcurrent,
|
|
26
27
|
});
|
|
28
|
+
const health = createGitHubAdapterHealthRegistry(config.repositories);
|
|
27
29
|
return {
|
|
28
30
|
adapter,
|
|
29
31
|
eventTypes: Object.values(GitHubEventType),
|
|
30
32
|
source: createGitHubSource(config, client, adapter, requests, {
|
|
31
33
|
checkpoints: services.checkpoints,
|
|
32
34
|
now: () => services.clock.now().getTime(),
|
|
35
|
+
health,
|
|
33
36
|
}),
|
|
34
37
|
maintenance: createGitHubWakeLabelReconciler({
|
|
35
38
|
orchestration: services.orchestration,
|
|
@@ -39,16 +42,29 @@ export const gitHubProviderDefinition = {
|
|
|
39
42
|
setLabels: client.setIssueLabels,
|
|
40
43
|
requests,
|
|
41
44
|
}),
|
|
45
|
+
health: () => health.snapshotAll(),
|
|
42
46
|
delivery: createGitHubDelivery(async (intent, idempotencyKey) => {
|
|
43
47
|
const resource = await services.resources.get(resourceId(intent.resourceId));
|
|
44
48
|
if (resource === null)
|
|
45
49
|
throw new Error(`GitHub resource ${intent.resourceId} is unavailable`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const parsedKey = parsePullRequestKey(resource.externalKey.key);
|
|
51
|
+
const repository = parsedKey === null ? null : `${parsedKey.owner}/${parsedKey.repo}`;
|
|
52
|
+
try {
|
|
53
|
+
const externalId = await client.deliver({
|
|
54
|
+
...translateGitHubOutbound(resource, intent, {
|
|
55
|
+
publicUiUrl: services.publicUiUrl,
|
|
56
|
+
}),
|
|
57
|
+
idempotencyKey,
|
|
58
|
+
});
|
|
59
|
+
if (repository !== null)
|
|
60
|
+
health.recordSuccess(repository, 'deliver');
|
|
61
|
+
return externalId;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (repository !== null)
|
|
65
|
+
health.recordFailure(repository, 'deliver', error);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
52
68
|
}, async (intent) => {
|
|
53
69
|
if (intent.kind !== BuiltInActivityName.IssueComplete)
|
|
54
70
|
return null;
|
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];
|