@atolis-hq/wake 0.3.29 → 0.3.31
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/version.js +1 -1
- package/dist/src/integrations/github/application/inbound-translator.js +80 -16
- package/dist/src/integrations/github/contracts/events.js +16 -1
- package/dist/src/persistence/filesystem/file-checkpoint-store.js +19 -4
- package/dist/src/persistence/filesystem/file-projection-store.js +2 -1
- package/dist/src/resources/application/resource-lookup.js +30 -0
- package/dist/src/resources/application/resource-service.js +1 -0
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { ActivityEventType, createPullRequestService, } from '../../../activities/index.js';
|
|
3
|
-
import { UlidIdGenerator, } from '../../../kernel/index.js';
|
|
3
|
+
import { createEventDraft, EventActorKind, EventSourceKind, UlidIdGenerator, WrongExpectedSequenceError, } from '../../../kernel/index.js';
|
|
4
4
|
import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, ResourceEventType, resourceId, resourceStream, } from '../../../resources/index.js';
|
|
5
5
|
import { workItemId } from '../../../work/index.js';
|
|
6
6
|
import { admitObservedWork } from '../../application/work-admission.js';
|
|
7
7
|
import { concludeObservedWork } from '../../application/work-conclusion.js';
|
|
8
8
|
import { evaluateIntakeRules } from '../../contracts/intake-rules.js';
|
|
9
|
-
import { deliveryStream } from '../../contracts/streams.js';
|
|
9
|
+
import { deliveryStream, integrationStream } from '../../contracts/streams.js';
|
|
10
10
|
import { DeliveryEventType, selectDeliveryEvent } from '../../delivery/contracts/events.js';
|
|
11
11
|
import { GitHubEventType, selectGitHubAdapterEvent } from '../contracts/events.js';
|
|
12
12
|
import { GitHubAdapter } from '../contracts/vocabulary.js';
|
|
@@ -117,6 +117,10 @@ export class InboundTranslator {
|
|
|
117
117
|
const identity = await this.resolveIdentity({ adapter: this.adapter, key: payload.externalKey }, intake.admitted);
|
|
118
118
|
if (identity === null)
|
|
119
119
|
return;
|
|
120
|
+
if (identity.deleted) {
|
|
121
|
+
await this.recordDeletedWorkObservation(event, identity.workItemId);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
120
124
|
if (!identity.created) {
|
|
121
125
|
await this.applyReobservation({
|
|
122
126
|
payload,
|
|
@@ -244,25 +248,85 @@ export class InboundTranslator {
|
|
|
244
248
|
const key = `${externalKey.adapter}:${externalKey.key}`;
|
|
245
249
|
const inBatch = this.minted.get(key);
|
|
246
250
|
if (inBatch !== undefined)
|
|
247
|
-
return
|
|
251
|
+
return this.inBatchIdentity(inBatch);
|
|
248
252
|
if (this.lookup === undefined)
|
|
249
253
|
throw new Error('InboundTranslator lookup is required');
|
|
250
254
|
const resourceIdValue = await this.lookup.resourceIdForExternalKey(externalKey);
|
|
251
|
-
if (resourceIdValue !== null)
|
|
252
|
-
|
|
253
|
-
throw new Error('InboundTranslator resources are required');
|
|
254
|
-
const correlation = (await this.resources.correlations(resourceIdValue)).find((value) => value.role === ResourceCorrelationRole.Primary);
|
|
255
|
-
// A deleted WorkItem retracts its primary correlation while retaining the external
|
|
256
|
-
// resource as a durable tombstone. Do not remint it or let it block later intake.
|
|
257
|
-
if (correlation === undefined)
|
|
258
|
-
return null;
|
|
259
|
-
const identity = { resourceId: resourceIdValue, workItemId: correlation.workItemId };
|
|
260
|
-
this.minted.set(key, identity);
|
|
261
|
-
return { ...identity, created: false };
|
|
262
|
-
}
|
|
255
|
+
if (resourceIdValue !== null)
|
|
256
|
+
return this.correlatedIdentity(key, resourceIdValue);
|
|
263
257
|
// An ineligible object Wake has never seen produces no WorkItem, Run, or effect.
|
|
264
258
|
if (!admitted)
|
|
265
259
|
return null;
|
|
266
|
-
return { ...this.mintIdentity(externalKey), created: true };
|
|
260
|
+
return { ...this.mintIdentity(externalKey), created: true, deleted: false };
|
|
261
|
+
}
|
|
262
|
+
async inBatchIdentity(identity) {
|
|
263
|
+
const existing = await this.work?.get(identity.workItemId);
|
|
264
|
+
return { ...identity, created: false, deleted: existing?.deleted === true };
|
|
265
|
+
}
|
|
266
|
+
async correlatedIdentity(key, resourceIdValue) {
|
|
267
|
+
if (this.resources === undefined)
|
|
268
|
+
throw new Error('InboundTranslator resources are required');
|
|
269
|
+
const correlation = (await this.resources.correlations(resourceIdValue)).find((value) => value.role === ResourceCorrelationRole.Primary);
|
|
270
|
+
if (correlation === undefined)
|
|
271
|
+
return this.historicalIdentity(resourceIdValue);
|
|
272
|
+
const identity = { resourceId: resourceIdValue, workItemId: correlation.workItemId };
|
|
273
|
+
this.minted.set(key, identity);
|
|
274
|
+
return { ...identity, created: false, deleted: false };
|
|
275
|
+
}
|
|
276
|
+
async historicalIdentity(resourceIdValue) {
|
|
277
|
+
const historical = await this.resources.primaryCorrelation(resourceIdValue);
|
|
278
|
+
if (historical === null)
|
|
279
|
+
throw new Error(`Resource ${resourceIdValue} has no primary correlation`);
|
|
280
|
+
const historicalWork = await this.work?.get(historical.workItemId);
|
|
281
|
+
if (historicalWork?.deleted)
|
|
282
|
+
return {
|
|
283
|
+
resourceId: resourceIdValue,
|
|
284
|
+
workItemId: historical.workItemId,
|
|
285
|
+
created: false,
|
|
286
|
+
deleted: true,
|
|
287
|
+
};
|
|
288
|
+
throw new Error(`Resource ${resourceIdValue} has no active primary correlation`);
|
|
289
|
+
}
|
|
290
|
+
async recordDeletedWorkObservation(event, workItemId) {
|
|
291
|
+
const stream = integrationStream(this.adapter);
|
|
292
|
+
const eventId = `github:deleted-work-skip:${event.eventId}:${workItemId}`;
|
|
293
|
+
const payload = {
|
|
294
|
+
externalKey: event.payload.externalKey,
|
|
295
|
+
workItemId,
|
|
296
|
+
sourceEventId: event.eventId,
|
|
297
|
+
revision: event.payload.revision,
|
|
298
|
+
reason: 'work-item-deleted',
|
|
299
|
+
};
|
|
300
|
+
for (;;) {
|
|
301
|
+
const existing = await this.journal.readStream(stream);
|
|
302
|
+
const recorded = existing.find((candidate) => candidate.eventId === eventId);
|
|
303
|
+
if (recorded !== undefined) {
|
|
304
|
+
const diagnostic = selectGitHubAdapterEvent(recorded);
|
|
305
|
+
if (diagnostic?.eventType !== GitHubEventType.DeletedWorkObservationSkipped ||
|
|
306
|
+
JSON.stringify(diagnostic.payload) !== JSON.stringify(payload))
|
|
307
|
+
throw new Error(`Event id ${eventId} has already been used with different content`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
await this.journal.append(stream, existing.length, [
|
|
312
|
+
createEventDraft({
|
|
313
|
+
eventId,
|
|
314
|
+
eventType: GitHubEventType.DeletedWorkObservationSkipped,
|
|
315
|
+
occurredAt: event.occurredAt,
|
|
316
|
+
correlationId: event.correlationId,
|
|
317
|
+
causationId: event.eventId,
|
|
318
|
+
actor: { kind: EventActorKind.Integration, id: this.adapter },
|
|
319
|
+
source: { kind: EventSourceKind.Internal, id: 'github-inbound-translator' },
|
|
320
|
+
stream,
|
|
321
|
+
payload,
|
|
322
|
+
}),
|
|
323
|
+
]);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (!(error instanceof WrongExpectedSequenceError))
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
267
331
|
}
|
|
268
332
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ProviderPermission, PullRequestCheckState, PullRequestState, ReviewActorKind, ReviewerAuthorizationSource, } from '../../../activities/index.js';
|
|
3
|
-
import { eventEnvelopeSchema, } from '../../../kernel/index.js';
|
|
3
|
+
import { brandedStringSchema, eventEnvelopeSchema, } from '../../../kernel/index.js';
|
|
4
|
+
import { workItemId } from '../../../work/index.js';
|
|
4
5
|
import { adapterId } from '../../contracts/identifiers.js';
|
|
5
6
|
import { ExternalWorkOutcome } from '../../contracts/outcome-vocabulary.js';
|
|
6
7
|
import { IntegrationStreamKind } from '../../contracts/streams.js';
|
|
@@ -8,6 +9,7 @@ export const GitHubEventType = {
|
|
|
8
9
|
WorkObserved: 'integration.github.work-observed',
|
|
9
10
|
CommentObserved: 'integration.github.comment-observed',
|
|
10
11
|
DeliveryObserved: 'integration.github.delivery-observed',
|
|
12
|
+
DeletedWorkObservationSkipped: 'integration.github.deleted-work-observation-skipped',
|
|
11
13
|
};
|
|
12
14
|
const streamSchema = z
|
|
13
15
|
.object({
|
|
@@ -114,6 +116,19 @@ const eventSchema = z.discriminatedUnion('eventType', [
|
|
|
114
116
|
stream: streamSchema,
|
|
115
117
|
payload: z.object({ deliveryId: z.string(), raw: rawSchema }).strict(),
|
|
116
118
|
}),
|
|
119
|
+
eventEnvelopeSchema.extend({
|
|
120
|
+
eventType: z.literal(GitHubEventType.DeletedWorkObservationSkipped),
|
|
121
|
+
stream: streamSchema,
|
|
122
|
+
payload: z
|
|
123
|
+
.object({
|
|
124
|
+
externalKey: z.string(),
|
|
125
|
+
workItemId: brandedStringSchema(workItemId),
|
|
126
|
+
sourceEventId: z.string(),
|
|
127
|
+
revision: z.string(),
|
|
128
|
+
reason: z.literal('work-item-deleted'),
|
|
129
|
+
})
|
|
130
|
+
.strict(),
|
|
131
|
+
}),
|
|
117
132
|
]);
|
|
118
133
|
export function decodeGitHubAdapterEvent(event) {
|
|
119
134
|
const result = eventSchema.safeParse(event);
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { atomicJson, encode } from './file-projection-store.js';
|
|
4
4
|
export class FileCheckpointStore {
|
|
5
5
|
root;
|
|
6
|
+
mutations = new Map();
|
|
6
7
|
constructor(root) {
|
|
7
8
|
this.root = root;
|
|
8
9
|
}
|
|
@@ -22,12 +23,26 @@ export class FileCheckpointStore {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
async save(consumer, globalPosition) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
await this.mutate(consumer, async () => {
|
|
27
|
+
if (globalPosition < (await this.load(consumer)))
|
|
28
|
+
throw new Error(`Checkpoint regression for ${consumer}`);
|
|
29
|
+
await atomicJson(this.path(consumer), { consumer, globalPosition });
|
|
30
|
+
});
|
|
28
31
|
}
|
|
29
32
|
async reset(consumer) {
|
|
30
|
-
await rm(this.path(consumer), { force: true });
|
|
33
|
+
await this.mutate(consumer, () => rm(this.path(consumer), { force: true }));
|
|
34
|
+
}
|
|
35
|
+
async mutate(consumer, operation) {
|
|
36
|
+
const prior = this.mutations.get(consumer) ?? Promise.resolve();
|
|
37
|
+
const current = prior.catch(() => undefined).then(operation);
|
|
38
|
+
this.mutations.set(consumer, current);
|
|
39
|
+
try {
|
|
40
|
+
return await current;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
if (this.mutations.get(consumer) === current)
|
|
44
|
+
this.mutations.delete(consumer);
|
|
45
|
+
}
|
|
31
46
|
}
|
|
32
47
|
path(consumer) {
|
|
33
48
|
return join(this.root, 'checkpoints', `${encode(consumer)}.json`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { mkdir, open, readFile, readdir, rename, rm } from 'node:fs/promises';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
export class FileProjectionStore {
|
|
@@ -46,7 +47,7 @@ export function encode(value) {
|
|
|
46
47
|
}
|
|
47
48
|
export async function atomicJson(path, value) {
|
|
48
49
|
await mkdir(dirname(path), { recursive: true });
|
|
49
|
-
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
50
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
50
51
|
try {
|
|
51
52
|
const handle = await open(temporary, 'wx');
|
|
52
53
|
try {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ResourceEventType, selectResourceEvent } from '../contracts/events.js';
|
|
2
|
+
import { resourceStream } from '../contracts/streams.js';
|
|
3
|
+
import { ResourceCorrelationRole } from '../contracts/vocabulary.js';
|
|
1
4
|
import { externalKeyProjectionKey, resourcesByExternalKeyProjection, workCorrelationsProjection, } from './lookup-projections.js';
|
|
2
5
|
export function createResourceLookup(dependencies) {
|
|
3
6
|
const seeded = async (definition, key) => {
|
|
@@ -12,5 +15,32 @@ export function createResourceLookup(dependencies) {
|
|
|
12
15
|
return {
|
|
13
16
|
resourceIdForExternalKey: (externalKey) => seeded(resourcesByExternalKeyProjection, externalKeyProjectionKey(externalKey)),
|
|
14
17
|
correlationsForWork: (workItemId) => seeded(workCorrelationsProjection, workItemId),
|
|
18
|
+
async primaryCorrelation(resourceId) {
|
|
19
|
+
const active = new Map();
|
|
20
|
+
const retracted = [];
|
|
21
|
+
for (const event of await dependencies.journal.readStream(resourceStream(resourceId))) {
|
|
22
|
+
const owned = selectResourceEvent(event);
|
|
23
|
+
if (owned?.eventType === ResourceEventType.WorkCorrelationEstablished) {
|
|
24
|
+
active.set(owned.payload.workItemId, {
|
|
25
|
+
resourceId,
|
|
26
|
+
workItemId: owned.payload.workItemId,
|
|
27
|
+
role: owned.payload.role,
|
|
28
|
+
provenance: owned.payload.provenance,
|
|
29
|
+
establishedByEventId: owned.eventId,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (owned?.eventType === ResourceEventType.WorkCorrelationRetracted) {
|
|
33
|
+
const correlation = active.get(owned.payload.workItemId);
|
|
34
|
+
if (correlation !== undefined) {
|
|
35
|
+
active.delete(owned.payload.workItemId);
|
|
36
|
+
if (correlation.role === ResourceCorrelationRole.Primary)
|
|
37
|
+
retracted.push(correlation);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return ([...active.values()].find((value) => value.role === ResourceCorrelationRole.Primary) ??
|
|
42
|
+
retracted.at(-1) ??
|
|
43
|
+
null);
|
|
44
|
+
},
|
|
15
45
|
};
|
|
16
46
|
}
|
|
@@ -17,6 +17,7 @@ export function createResourceService(journal, lookup) {
|
|
|
17
17
|
async correlations(resourceId) {
|
|
18
18
|
return (await repository.load(resourceId)).resource?.correlations ?? [];
|
|
19
19
|
},
|
|
20
|
+
primaryCorrelation: (resourceId) => lookup.primaryCorrelation(resourceId),
|
|
20
21
|
correlationsForWork: (workItemId) => lookup.correlationsForWork(workItemId),
|
|
21
22
|
correlate: (resourceId, workItemId, role, context, provenance) => correlateResource(repository, resourceId, workItemId, role, context, provenance),
|
|
22
23
|
async retract(resourceId, workItemId, context) {
|