Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions packages/agent/src/dkg-agent-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ import {
pickNetworkTunables,
isSparqlUpdateOperation,
} from '@origintrail-official/dkg-core';
import { GraphManager, PrivateContentStore, SystemRecordLaneForwarderV1, captureStructuredMutationEffects, createTripleStore, isExternalBackend, type TripleStore, type TripleStoreConfig, type Quad, type LargeLiteralStorageConfig, type QueryOptions } from '@origintrail-official/dkg-storage';
import { GraphManager, PrivateContentStore, SystemRecordLaneForwarderV1, captureStructuredMutationSnapshot, createTripleStore, isExternalBackend, type TripleStore, type TripleStoreConfig, type Quad, type LargeLiteralStorageConfig, type QueryOptions } from '@origintrail-official/dkg-storage';
import { emptyRpcUsageWindow, EVMChainAdapter, NoChainAdapter, enrichEvmError, buildKnowledgeAssetUal, type EVMAdapterConfig, type ChainAdapter, type CreateContextGraphParams, type CreateOnChainContextGraphParams, type CreateOnChainContextGraphResult, type TxResult, type V10PublishingConvictionAccountInfo, type RpcUsageWindow } from '@origintrail-official/dkg-chain';
import {
DKGPublisher, PublishHandler, SharedMemoryHandler, UpdateHandler, ChainEventPoller, AccessHandler, AccessClient,
Expand Down Expand Up @@ -577,14 +577,17 @@ export function createListContextGraphsCacheInvalidatingStore(
)
: undefined,
structuredMutation: innerStore.structuredMutation
? (mutation, options) => {
const effects = captureStructuredMutationEffects(mutation);
return invalidateAfterMutation(
() => innerStore.structuredMutation!(mutation, options),
() => effects !== undefined,
() => effects?.touchedGraphs.forEach(
(graph) => markProjectionDirty?.(undefined, graph),
),
? async (mutation, options) => {
const snapshot = captureStructuredMutationSnapshot(mutation);
await invalidateAfterMutation(
() => innerStore.structuredMutation!(snapshot.mutation, options),
() => snapshot.outcome !== 'noop',
() => {
if (snapshot.outcome === 'noop') return;
snapshot.effects.touchedGraphs.forEach(
(graph) => markProjectionDirty?.(undefined, graph),
);
},
);
}
: undefined,
Expand Down
110 changes: 109 additions & 1 deletion packages/agent/test/replace-subject-agent-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createTripleStore,
tryReplaceSubjectAtomically,
type Quad,
type StructuredMutation,
type TripleStore,
} from '@origintrail-official/dkg-storage';
import { contextGraphCatalogUri, contextGraphMetaGraphUri } from '@origintrail-official/dkg-core';
Expand Down Expand Up @@ -156,13 +157,15 @@ describe('#1863 replaceSubject through the agent store wrapper', () => {
const inFlight = new Promise<void>((resolve) => { release = resolve; });
const options = { source: 'agent.test.structured-mutation-effects' };
let inner!: TripleStore;
let observedMutation: unknown;
const structuredMutation = vi.fn(function (
this: TripleStore,
_mutation: unknown,
receivedMutation: unknown,
receivedOptions: unknown,
) {
expect(this).toBe(inner);
expect(receivedOptions).toBe(options);
observedMutation = receivedMutation;
return inFlight;
});
inner = { structuredMutation } as unknown as TripleStore;
Expand Down Expand Up @@ -193,6 +196,92 @@ describe('#1863 replaceSubject through the agent store wrapper', () => {
expect(invalidate).toHaveBeenCalledOnce();
expect(markProjectionDirty).toHaveBeenCalledOnce();
expect(markProjectionDirty).toHaveBeenCalledWith(undefined, 'urn:test:target');
expect(observedMutation).toEqual({
kind: 'copy-subject-projection',
input: {
sourceGraphUris: ['urn:test:source'],
targetGraphUri: 'urn:test:target',
roots: ['urn:test:root'],
descendantSuffix: '/',
excludedPredicates: [],
},
});
expect(Object.isFrozen(observedMutation)).toBe(true);
});

it('forwards every structured mutation kind with exact scoped effects', async () => {
const structuredMutation = vi.fn(async () => undefined);
const inner = { structuredMutation } as unknown as TripleStore;
const invalidate = vi.fn();
const markProjectionDirty = vi.fn();
const store = createListContextGraphsCacheInvalidatingStore(
inner,
invalidate,
markProjectionDirty,
);
const options = { source: 'agent.test.all-structured-mutations' };
const graph = 'urn:test:agent:graph';
const target = 'urn:test:agent:target';
const predicate = 'urn:test:agent:predicate';
const fixtures: Array<{ mutation: StructuredMutation; touched: string }> = [
{ mutation: { kind: 'delete-subjects', input: {
graphUri: graph, subjects: ['urn:test:agent:subject'],
} }, touched: graph },
{ mutation: { kind: 'prune-ranked-subjects', input: {
graphUri: graph,
subjectPrefix: 'urn:test:agent:ranked:',
eligibilityPredicate: predicate,
eligibleObjects: ['approved'],
primaryRankPredicate: 'urn:test:agent:rank:primary',
secondaryRankPredicate: 'urn:test:agent:rank:secondary',
retainNewest: 1,
maxDelete: 1,
} }, touched: graph },
{ mutation: { kind: 'prune-linked-record-closures', input: {
graphUri: graph,
matchObjectIris: ['urn:test:agent:member'],
linkPredicates: [predicate],
recordParentPredicate: 'urn:test:agent:parent',
descendantSeparator: '/',
} }, touched: graph },
{ mutation: { kind: 'replace-subject-predicates', input: {
graphUri: graph,
subject: 'urn:test:agent:subject',
predicates: [predicate],
replacementQuads: [{
graph,
subject: 'urn:test:agent:subject',
predicate,
object: '"value"',
}],
} }, touched: graph },
{ mutation: { kind: 'replace-projection-from-graph', input: {
targetGraphUri: target,
stagingGraphUri: 'urn:test:agent:staging',
targetSubject: 'urn:test:agent:subject',
preservedTargetPredicates: [predicate],
targetSubjectPrefixes: [],
} }, touched: target },
{ mutation: { kind: 'copy-subject-projection', input: {
sourceGraphUris: [graph],
targetGraphUri: target,
roots: ['urn:test:agent:root'],
descendantSuffix: '/',
excludedPredicates: [],
} }, touched: target },
];

for (const [index, fixture] of fixtures.entries()) {
await store.structuredMutation!(fixture.mutation, options);
expect(structuredMutation.mock.calls[index][1]).toBe(options);
expect(Object.isFrozen(structuredMutation.mock.calls[index][0])).toBe(true);
expect(markProjectionDirty).toHaveBeenNthCalledWith(
index + 1,
undefined,
fixture.touched,
);
}
expect(invalidate).toHaveBeenCalledTimes(fixtures.length);
});

it('does not invalidate structured mutation failures or structural no-ops', async () => {
Expand All @@ -211,6 +300,8 @@ describe('#1863 replaceSubject through the agent store wrapper', () => {
kind: 'delete-subjects',
input: { graphUri: 'urn:test:target', subjects: [] },
});
expect(inner.structuredMutation).toHaveBeenCalledOnce();
expect(Object.isFrozen(inner.structuredMutation.mock.calls[0][0])).toBe(true);
expect(invalidate).not.toHaveBeenCalled();
expect(markProjectionDirty).not.toHaveBeenCalled();

Expand All @@ -222,4 +313,21 @@ describe('#1863 replaceSubject through the agent store wrapper', () => {
expect(invalidate).not.toHaveBeenCalled();
expect(markProjectionDirty).not.toHaveBeenCalled();
});

it('converts synchronous snapshot validation failures into rejected Promises', async () => {
const inner = {
structuredMutation: vi.fn(async () => undefined),
} as unknown as TripleStore;
const store = createListContextGraphsCacheInvalidatingStore(inner, vi.fn(), vi.fn());
let result!: Promise<void>;

expect(() => {
result = store.structuredMutation!({
kind: 'delete-subjects',
input: { graphUri: 'relative', subjects: [] },
});
}).not.toThrow();
await expect(result).rejects.toThrow(/absolute IRI/);
expect(inner.structuredMutation).not.toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions packages/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"./package.json": "./package.json"
},
"scripts": {
"benchmark:structured-mutation": "node scripts/benchmark-structured-mutation.mjs",
"build": "tsc",
"test": "vitest run",
"test:package-exports": "node scripts/verify-pack-exports.mjs",
Expand Down
Loading
Loading