@vercel/factory 0.0.17 → 0.0.19
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/CHANGELOG.md +52 -0
- package/dist/api-contracts.d.mts +665 -715
- package/dist/api-contracts.mjs +91 -4
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api-task-graph.mjs +14 -5
- package/dist/api-task-graph.mjs.map +1 -1
- package/dist/api.d.mts +58 -11
- package/dist/api.mjs +715 -399
- package/dist/api.mjs.map +1 -1
- package/dist/approval-access.d.mts +1 -1
- package/dist/approval-contracts.d.mts +3 -4
- package/dist/approval-contracts.mjs +1 -2
- package/dist/approval-contracts.mjs.map +1 -1
- package/dist/blob/index.d.mts +1 -1
- package/dist/budget.mjs +20 -3
- package/dist/budget.mjs.map +1 -1
- package/dist/change-verification/brief.d.mts +1 -1
- package/dist/change-verification/dispatch.mjs +8 -1
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/changes/eve-record-change.mjs +4 -1
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/client-stream.d.mts +19 -2
- package/dist/client-stream.mjs +115 -53
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.d.mts +25 -2
- package/dist/client-transcript.mjs +95 -13
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +145 -19
- package/dist/client.mjs +89 -16
- package/dist/client.mjs.map +1 -1
- package/dist/code-review/contracts.d.mts +1 -0
- package/dist/code-review/eve-post-review.d.mts +4 -2
- package/dist/code-review/eve-post-review.mjs +8 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +6 -2
- package/dist/code-review/github-reporter.mjs +86 -35
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review/preparation.d.mts +2 -1
- package/dist/code-review/preparation.mjs +2 -1
- package/dist/code-review/preparation.mjs.map +1 -1
- package/dist/code-review/stall.mjs +11 -2
- package/dist/code-review/stall.mjs.map +1 -1
- package/dist/connectors/github.d.mts +1 -1
- package/dist/connectors/index.d.mts +1 -1
- package/dist/connectors/slack.d.mts +1 -1
- package/dist/delivery-metrics/metrics.d.mts +1 -1
- package/dist/dispatch.mjs +3 -3
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +1 -1
- package/dist/eve/index.mjs +8 -1
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +4 -4
- package/dist/eve/transcript.d.mts +3 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +1 -1
- package/dist/finding-remediation/brief.d.mts +1 -1
- package/dist/github-app.d.mts +1 -1
- package/dist/github-publication.d.mts +1 -1
- package/dist/github.d.mts +9 -1
- package/dist/github.mjs +29 -11
- package/dist/github.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/intake-contracts.d.mts +1 -0
- package/dist/intake-contracts.mjs +2 -1
- package/dist/intake-contracts.mjs.map +1 -1
- package/dist/integrations/github-issues.d.mts +64 -0
- package/dist/integrations/github-issues.mjs +175 -0
- package/dist/integrations/github-issues.mjs.map +1 -0
- package/dist/integrations/github.d.mts +2 -1
- package/dist/integrations/github.mjs +2 -1
- package/dist/multi-repository-changes/coordination.mjs +1 -1
- package/dist/planning/reconcile.mjs +15 -2
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +2 -1
- package/dist/postgres/index.mjs +11 -5
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/postgres/indexed-codec.mjs +137 -0
- package/dist/postgres/indexed-codec.mjs.map +1 -0
- package/dist/postgres/indexed-schema.mjs +99 -0
- package/dist/postgres/indexed-schema.mjs.map +1 -0
- package/dist/postgres/indexed.d.mts +28 -0
- package/dist/postgres/indexed.mjs +277 -0
- package/dist/postgres/indexed.mjs.map +1 -0
- package/dist/postgres/record-predicates.mjs +23 -0
- package/dist/postgres/record-predicates.mjs.map +1 -0
- package/dist/presets/software-development/deepsec-brief.d.mts +1 -1
- package/dist/presets/software-development/dispatch.mjs +1 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/recovery.mjs +9 -2
- package/dist/presets/software-development/recovery.mjs.map +1 -1
- package/dist/pull-requests/eve-fetch.d.mts +4 -1
- package/dist/pull-requests/eve-fetch.mjs +9 -0
- package/dist/pull-requests/eve-fetch.mjs.map +1 -1
- package/dist/pull-requests/github-publisher.d.mts +1 -1
- package/dist/pull-requests/github-publisher.mjs +19 -7
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/repositories.mjs +20 -13
- package/dist/repositories.mjs.map +1 -1
- package/dist/sandbox/execution-run-ledger.d.mts +2 -2
- package/dist/sandbox/execution-run-ledger.mjs.map +1 -1
- package/dist/sandbox/execution.d.mts +2 -2
- package/dist/sandbox/execution.mjs.map +1 -1
- package/dist/schema/catalog.mjs +1 -1
- package/dist/schema/change.d.mts +1 -1
- package/dist/schema/communication.d.mts +2 -2
- package/dist/schema/factory-config.d.mts +1 -1
- package/dist/schema/outbox.d.mts +6 -6
- package/dist/schema/session-summary.d.mts +218 -0
- package/dist/schema/session-summary.mjs +68 -0
- package/dist/schema/session-summary.mjs.map +1 -0
- package/dist/schema/session.d.mts +39 -59
- package/dist/schema/session.mjs +6 -6
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/task-graph.d.mts +24 -24
- package/dist/schema/transcript.d.mts +141 -3
- package/dist/schema/transcript.mjs +19 -3
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/transitions.d.mts +1 -1
- package/dist/schema/transitions.mjs +1 -1
- package/dist/schema/transitions.mjs.map +1 -1
- package/dist/signals.d.mts +1 -1
- package/dist/stall.mjs +10 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/storage.d.mts +10 -1
- package/dist/storage.mjs +7 -1
- package/dist/store/approvals.d.mts +14 -0
- package/dist/store/approvals.mjs +78 -0
- package/dist/store/approvals.mjs.map +1 -0
- package/dist/store/conversations.d.mts +59 -0
- package/dist/store/conversations.mjs +549 -0
- package/dist/store/conversations.mjs.map +1 -0
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +38 -16
- package/dist/store/engine.mjs +229 -106
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/indexed-engine.d.mts +57 -0
- package/dist/store/indexed-engine.mjs +155 -0
- package/dist/store/indexed-engine.mjs.map +1 -0
- package/dist/store/indexed-memory.d.mts +7 -0
- package/dist/store/indexed-memory.mjs +200 -0
- package/dist/store/indexed-memory.mjs.map +1 -0
- package/dist/store/indexed-projections.d.mts +280 -0
- package/dist/store/indexed-projections.mjs +481 -0
- package/dist/store/indexed-projections.mjs.map +1 -0
- package/dist/store/indexed.d.mts +101 -0
- package/dist/store/indexed.mjs +19 -0
- package/dist/store/indexed.mjs.map +1 -0
- package/dist/store/iterate.d.mts +9 -0
- package/dist/store/iterate.mjs +23 -0
- package/dist/store/iterate.mjs.map +1 -0
- package/dist/store/query-scope.mjs +62 -0
- package/dist/store/query-scope.mjs.map +1 -0
- package/dist/store/query.d.mts +32 -0
- package/dist/store/query.mjs +103 -0
- package/dist/store/query.mjs.map +1 -0
- package/dist/store/session-history.mjs +24 -0
- package/dist/store/session-history.mjs.map +1 -0
- package/dist/store/slack-pr-notifications.d.mts +2 -2
- package/dist/store/slack-pr-notifications.mjs.map +1 -1
- package/dist/store/state.d.mts +26 -0
- package/dist/store/state.mjs +95 -0
- package/dist/store/state.mjs.map +1 -0
- package/dist/store/task-effects.d.mts +1 -1
- package/dist/store/task-graphs.d.mts +1 -1
- package/dist/store/task-messages.d.mts +1 -1
- package/dist/store/task-work.d.mts +1 -1
- package/dist/store/task-work.mjs +1 -1
- package/dist/store/transcripts.mjs +308 -0
- package/dist/store/transcripts.mjs.map +1 -0
- package/dist/sweep.d.mts +1 -1
- package/dist/sweep.mjs +12 -7
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-communication.mjs +15 -1
- package/dist/task-communication.mjs.map +1 -1
- package/dist/task-protocols.d.mts +3 -3
- package/dist/tasks.d.mts +1 -1
- package/dist/vercel.d.mts +1 -1
- package/dist/work-triage/find-existing-work.d.mts +1 -1
- package/dist/work-triage/find-existing-work.mjs +12 -2
- package/dist/work-triage/find-existing-work.mjs.map +1 -1
- package/docs/index.md +4 -0
- package/docs/recipes/persistence-recovery.md +15 -9
- package/docs/recipes/postgres-indexed-engine.md +108 -0
- package/docs/recipes/postgres-indexed-storage.md +145 -0
- package/package.json +3 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { StoreDriver } from "./driver.mjs";
|
|
2
|
+
import { IndexedStore } from "./indexed.mjs";
|
|
3
|
+
import { CreateStoresOptions, FactoryStores } from "./engine.mjs";
|
|
4
|
+
import { ApprovalStore } from "./approvals.mjs";
|
|
5
|
+
import { ConversationProjectionOptions, ConversationStore } from "./conversations.mjs";
|
|
6
|
+
import { FactoryIndexScope, FactoryRecordSummary } from "./indexed-projections.mjs";
|
|
7
|
+
//#region src/store/indexed-engine.d.ts
|
|
8
|
+
/** Storage and workflow configuration for fresh indexed state; images use a separate object store. */
|
|
9
|
+
interface CreateIndexedStoresOptions extends Omit<CreateStoresOptions, "driver"> {
|
|
10
|
+
/** Authoritative structured persistence, with no historical backend or fallback. */
|
|
11
|
+
readonly store: IndexedStore;
|
|
12
|
+
/** Explicit operator read-model routing and automatic-work owner. */
|
|
13
|
+
readonly conversations?: ConversationProjectionOptions;
|
|
14
|
+
/** Object storage used exclusively for uploaded image bytes; never receives structured state. */
|
|
15
|
+
readonly images: Pick<StoreDriver, "get" | "insert">;
|
|
16
|
+
}
|
|
17
|
+
/** One validated compact summary and the canonical record version that produced it. */
|
|
18
|
+
interface FactoryIndexedEntry {
|
|
19
|
+
readonly summary: FactoryRecordSummary;
|
|
20
|
+
readonly version: number;
|
|
21
|
+
}
|
|
22
|
+
/** Bounded source-summary page; cursors continue only the same scope and ordering. */
|
|
23
|
+
interface FactoryIndexedPage {
|
|
24
|
+
readonly entries: readonly FactoryIndexedEntry[];
|
|
25
|
+
readonly nextCursor: string | null;
|
|
26
|
+
}
|
|
27
|
+
/** Engine write operations and point reads, with bounded queries replacing unbounded catalog-list methods. */
|
|
28
|
+
interface IndexedFactoryStores extends FactoryStores {
|
|
29
|
+
readonly conversations: ConversationStore;
|
|
30
|
+
readonly approvals: ApprovalStore;
|
|
31
|
+
/** Trusted source queries. Source attention counts are not deduplicated conversation counts. */
|
|
32
|
+
readonly indexes: {
|
|
33
|
+
query(input: {
|
|
34
|
+
scope: FactoryIndexScope;
|
|
35
|
+
limit: number;
|
|
36
|
+
cursor?: string;
|
|
37
|
+
order?: "asc" | "desc";
|
|
38
|
+
}): Promise<FactoryIndexedPage>;
|
|
39
|
+
count(scope: FactoryIndexScope): Promise<number>;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates the validating Factory write engine over atomic indexed persistence.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* New canonical state and its compact memberships/counts commit together. No catalog shards,
|
|
47
|
+
* Blob fallback, backfill, or unbounded list methods exist on the returned API. Images alone use the
|
|
48
|
+
* supplied object store. Authentication and query-scope authorization remain the application's
|
|
49
|
+
* responsibility. Repository membership does not grant access to a multi-repository record.
|
|
50
|
+
* The Postgres adapter's record/summary/membership bounds apply; source attention counts must not
|
|
51
|
+
* be presented as deduplicated conversation counts. Configure application conversation projections
|
|
52
|
+
* for the operator API; growing histories use bounded pages.
|
|
53
|
+
*/
|
|
54
|
+
declare function createIndexedStores(options: CreateIndexedStoresOptions): IndexedFactoryStores;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { CreateIndexedStoresOptions, FactoryIndexedEntry, FactoryIndexedPage, IndexedFactoryStores, createIndexedStores };
|
|
57
|
+
//# sourceMappingURL=indexed-engine.d.mts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { taskSchema } from "../schema/task.mjs";
|
|
2
|
+
import { agentSessionSchema } from "../schema/session.mjs";
|
|
3
|
+
import { factoryIndexKey, factoryIndexScopeSchema, factoryRecordSummarySchema, projectIndexedRecord } from "./indexed-projections.mjs";
|
|
4
|
+
import { createIndexedTranscriptStore } from "./transcripts.mjs";
|
|
5
|
+
import { createStoreEngine } from "./engine.mjs";
|
|
6
|
+
import { createApprovalStore, projectApproval } from "./approvals.mjs";
|
|
7
|
+
import { conversationTaskMemberships, createConversationStore, refreshConversations } from "./conversations.mjs";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
//#region src/store/indexed-engine.ts
|
|
11
|
+
const catalogs = /* @__PURE__ */ new Set([
|
|
12
|
+
"taskCatalogs",
|
|
13
|
+
"changeCatalogs",
|
|
14
|
+
"sessionCatalogs",
|
|
15
|
+
"externalSessionCatalogs",
|
|
16
|
+
"taskRoutes"
|
|
17
|
+
]);
|
|
18
|
+
function storedJson(value) {
|
|
19
|
+
const json = JSON.stringify(value, function(_key, entry) {
|
|
20
|
+
if (entry === void 0 && Array.isArray(this)) throw new Error("Undefined array element");
|
|
21
|
+
if (typeof entry === "number" && !Number.isFinite(entry)) throw new Error("Non-finite storage number");
|
|
22
|
+
if (typeof entry === "bigint" || typeof entry === "function" || typeof entry === "symbol") throw new Error("Storage requires JSON values");
|
|
23
|
+
return entry;
|
|
24
|
+
});
|
|
25
|
+
if (json === void 0) throw new Error("Storage requires a JSON value");
|
|
26
|
+
return JSON.parse(json);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Creates the validating Factory write engine over atomic indexed persistence.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* New canonical state and its compact memberships/counts commit together. No catalog shards,
|
|
33
|
+
* Blob fallback, backfill, or unbounded list methods exist on the returned API. Images alone use the
|
|
34
|
+
* supplied object store. Authentication and query-scope authorization remain the application's
|
|
35
|
+
* responsibility. Repository membership does not grant access to a multi-repository record.
|
|
36
|
+
* The Postgres adapter's record/summary/membership bounds apply; source attention counts must not
|
|
37
|
+
* be presented as deduplicated conversation counts. Configure application conversation projections
|
|
38
|
+
* for the operator API; growing histories use bounded pages.
|
|
39
|
+
*/
|
|
40
|
+
function createIndexedStores(options) {
|
|
41
|
+
return indexedStores(options, true);
|
|
42
|
+
}
|
|
43
|
+
function indexedStores(options, observe) {
|
|
44
|
+
const { images } = options;
|
|
45
|
+
function observingStore(raw) {
|
|
46
|
+
const projectionOptions = options.conversations;
|
|
47
|
+
return {
|
|
48
|
+
...raw,
|
|
49
|
+
transaction: (run) => raw.transaction((tx) => run(observingStore(tx))),
|
|
50
|
+
write: (input) => raw.transaction(async (tx) => {
|
|
51
|
+
const canonical = indexedStores({
|
|
52
|
+
...options,
|
|
53
|
+
store: tx,
|
|
54
|
+
conversations: void 0
|
|
55
|
+
}, false);
|
|
56
|
+
const memberships = input.collection === "tasks" ? await conversationTaskMemberships(canonical, taskSchema.parse(input.value)) : [];
|
|
57
|
+
const result = await tx.write({
|
|
58
|
+
...input,
|
|
59
|
+
indexes: [...input.indexes, ...memberships]
|
|
60
|
+
});
|
|
61
|
+
const execution = input.collection === "tasks" ? taskSchema.parse(input.value).execution : input.collection === "sessions" || input.collection === "externalSessions" ? agentSessionSchema.parse(input.value).execution : void 0;
|
|
62
|
+
if (execution && await canonical.transcripts.head(execution) === null) await canonical.transcripts.put(execution, {
|
|
63
|
+
blocks: [],
|
|
64
|
+
inputs: [],
|
|
65
|
+
cursor: 0,
|
|
66
|
+
state: "connecting",
|
|
67
|
+
tokens: 0
|
|
68
|
+
});
|
|
69
|
+
if (input.collection === "tasks") await projectApproval(tx, canonical, input.value);
|
|
70
|
+
if (projectionOptions) await refreshConversations(tx, canonical, input.collection, input.id, input.value, projectionOptions);
|
|
71
|
+
return result;
|
|
72
|
+
})
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const store = observe ? observingStore(options.store) : options.store;
|
|
76
|
+
function assertCanonical(collection) {
|
|
77
|
+
if (catalogs.has(collection)) throw new Error("Catalog persistence is unavailable in indexed stores");
|
|
78
|
+
}
|
|
79
|
+
async function write(collection, id, value, expectedVersion) {
|
|
80
|
+
assertCanonical(collection);
|
|
81
|
+
if (collection === "images") throw new Error("Images must use object storage");
|
|
82
|
+
const canonical = storedJson(value);
|
|
83
|
+
const intent = {
|
|
84
|
+
collection,
|
|
85
|
+
id,
|
|
86
|
+
value: canonical,
|
|
87
|
+
expectedVersion,
|
|
88
|
+
indexes: projectIndexedRecord(collection, id, canonical).map((entry) => ({
|
|
89
|
+
...entry,
|
|
90
|
+
summary: storedJson(entry.summary)
|
|
91
|
+
}))
|
|
92
|
+
};
|
|
93
|
+
const operationId = randomUUID();
|
|
94
|
+
await store.write({
|
|
95
|
+
...intent,
|
|
96
|
+
operationId
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const driver = {
|
|
100
|
+
name: store.name,
|
|
101
|
+
async get(collection, id) {
|
|
102
|
+
assertCanonical(collection);
|
|
103
|
+
return collection === "images" ? images.get(collection, id) : store.get(collection, id);
|
|
104
|
+
},
|
|
105
|
+
async insert(input) {
|
|
106
|
+
if (input.collection === "images") return images.insert(input);
|
|
107
|
+
await write(input.collection, input.id, input.value, 0);
|
|
108
|
+
},
|
|
109
|
+
async update(input) {
|
|
110
|
+
await write(input.collection, input.id, input.value, input.expectedVersion);
|
|
111
|
+
},
|
|
112
|
+
async list() {
|
|
113
|
+
throw new Error("Indexed stores require bounded queries instead of collection lists");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
...createStoreEngine({
|
|
118
|
+
...options,
|
|
119
|
+
driver
|
|
120
|
+
}, store),
|
|
121
|
+
conversations: createConversationStore(store),
|
|
122
|
+
approvals: createApprovalStore(store),
|
|
123
|
+
transcripts: createIndexedTranscriptStore(store, options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString())),
|
|
124
|
+
indexes: {
|
|
125
|
+
async query(input) {
|
|
126
|
+
const scope = factoryIndexScopeSchema.parse(input.scope);
|
|
127
|
+
const index = factoryIndexKey(scope);
|
|
128
|
+
const page = await store.query({
|
|
129
|
+
index,
|
|
130
|
+
limit: input.limit,
|
|
131
|
+
cursor: input.cursor,
|
|
132
|
+
order: input.order
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
entries: page.entries.map((entry) => {
|
|
136
|
+
const summary = factoryRecordSummarySchema.parse(entry.summary);
|
|
137
|
+
if (entry.index !== index || entry.collection !== scope.collection || summary.collection !== entry.collection || summary.id !== entry.id) throw new Error("Indexed summary identity mismatch");
|
|
138
|
+
return {
|
|
139
|
+
summary,
|
|
140
|
+
version: z.number().int().positive().parse(entry.version)
|
|
141
|
+
};
|
|
142
|
+
}),
|
|
143
|
+
nextCursor: page.nextCursor
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
count(scope) {
|
|
147
|
+
return store.count(factoryIndexKey(scope));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { createIndexedStores };
|
|
154
|
+
|
|
155
|
+
//# sourceMappingURL=indexed-engine.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexed-engine.mjs","names":[],"sources":["../../src/store/indexed-engine.ts"],"sourcesContent":["import { createApprovalStore, projectApproval, type ApprovalStore } from \"./approvals\";\nimport {\n createConversationStore,\n refreshConversations,\n conversationTaskMemberships,\n type ConversationProjectionOptions,\n type ConversationStore,\n} from \"./conversations\";\nimport { taskSchema } from \"../schema/task\";\nimport { agentSessionSchema } from \"../schema/session\";\nimport { randomUUID } from \"node:crypto\";\nimport { z } from \"zod\";\nimport { createStoreEngine, type CreateStoresOptions, type FactoryStores } from \"./engine\";\nimport type { CollectionName, StoreDriver, StoredRecord } from \"./driver\";\nimport type { IndexedStore } from \"./indexed\";\nimport { createIndexedTranscriptStore } from \"./transcripts\";\nimport {\n factoryIndexKey,\n factoryIndexScopeSchema,\n factoryRecordSummarySchema,\n projectIndexedRecord,\n type FactoryIndexScope,\n type FactoryRecordSummary,\n} from \"./indexed-projections\";\n\n/** Storage and workflow configuration for fresh indexed state; images use a separate object store. */\nexport interface CreateIndexedStoresOptions extends Omit<CreateStoresOptions, \"driver\"> {\n /** Authoritative structured persistence, with no historical backend or fallback. */\n readonly store: IndexedStore;\n /** Explicit operator read-model routing and automatic-work owner. */\n readonly conversations?: ConversationProjectionOptions;\n /** Object storage used exclusively for uploaded image bytes; never receives structured state. */\n readonly images: Pick<StoreDriver, \"get\" | \"insert\">;\n}\n\n/** One validated compact summary and the canonical record version that produced it. */\nexport interface FactoryIndexedEntry {\n readonly summary: FactoryRecordSummary;\n readonly version: number;\n}\n\n/** Bounded source-summary page; cursors continue only the same scope and ordering. */\nexport interface FactoryIndexedPage {\n readonly entries: readonly FactoryIndexedEntry[];\n readonly nextCursor: string | null;\n}\n\n/** Engine write operations and point reads, with bounded queries replacing unbounded catalog-list methods. */\nexport interface IndexedFactoryStores extends FactoryStores {\n readonly conversations: ConversationStore;\n readonly approvals: ApprovalStore;\n /** Trusted source queries. Source attention counts are not deduplicated conversation counts. */\n readonly indexes: {\n query(input: {\n scope: FactoryIndexScope;\n limit: number;\n cursor?: string;\n order?: \"asc\" | \"desc\";\n }): Promise<FactoryIndexedPage>;\n count(scope: FactoryIndexScope): Promise<number>;\n };\n}\n\nconst catalogs = new Set<CollectionName>([\n \"taskCatalogs\",\n \"changeCatalogs\",\n \"sessionCatalogs\",\n \"externalSessionCatalogs\",\n \"taskRoutes\",\n]);\n\n// Engine schemas may retain optional undefined properties. JSON persistence omits those properties,\n// but must not silently turn non-finite numbers or missing array elements into null.\nfunction storedJson(value: unknown): unknown {\n const json = JSON.stringify(value, function (_key, entry: unknown) {\n if (entry === undefined && Array.isArray(this)) throw new Error(\"Undefined array element\");\n if (typeof entry === \"number\" && !Number.isFinite(entry))\n throw new Error(\"Non-finite storage number\");\n if (typeof entry === \"bigint\" || typeof entry === \"function\" || typeof entry === \"symbol\")\n throw new Error(\"Storage requires JSON values\");\n return entry;\n });\n if (json === undefined) throw new Error(\"Storage requires a JSON value\");\n return JSON.parse(json) as unknown;\n}\n\n/**\n * Creates the validating Factory write engine over atomic indexed persistence.\n *\n * @remarks\n * New canonical state and its compact memberships/counts commit together. No catalog shards,\n * Blob fallback, backfill, or unbounded list methods exist on the returned API. Images alone use the\n * supplied object store. Authentication and query-scope authorization remain the application's\n * responsibility. Repository membership does not grant access to a multi-repository record.\n * The Postgres adapter's record/summary/membership bounds apply; source attention counts must not\n * be presented as deduplicated conversation counts. Configure application conversation projections\n * for the operator API; growing histories use bounded pages.\n */\nexport function createIndexedStores(options: CreateIndexedStoresOptions): IndexedFactoryStores {\n return indexedStores(options, true);\n}\n\nfunction indexedStores(\n options: CreateIndexedStoresOptions,\n observe: boolean,\n): IndexedFactoryStores {\n const { images } = options;\n function observingStore(raw: IndexedStore): IndexedStore {\n const projectionOptions = options.conversations;\n return {\n ...raw,\n transaction: (run) => raw.transaction((tx) => run(observingStore(tx))),\n write: (input) =>\n raw.transaction(async (tx) => {\n const canonical = indexedStores(\n { ...options, store: tx, conversations: undefined },\n false,\n );\n const memberships =\n input.collection === \"tasks\"\n ? await conversationTaskMemberships(canonical, taskSchema.parse(input.value))\n : [];\n const result = await tx.write({ ...input, indexes: [...input.indexes, ...memberships] });\n const execution =\n input.collection === \"tasks\"\n ? taskSchema.parse(input.value).execution\n : input.collection === \"sessions\" || input.collection === \"externalSessions\"\n ? agentSessionSchema.parse(input.value).execution\n : undefined;\n if (execution && (await canonical.transcripts.head(execution)) === null)\n await canonical.transcripts.put(execution, {\n blocks: [],\n inputs: [],\n cursor: 0,\n state: \"connecting\",\n tokens: 0,\n });\n if (input.collection === \"tasks\") await projectApproval(tx, canonical, input.value);\n if (projectionOptions)\n await refreshConversations(\n tx,\n canonical,\n input.collection,\n input.id,\n input.value,\n projectionOptions,\n );\n return result;\n }),\n };\n }\n const store = observe ? observingStore(options.store) : options.store;\n function assertCanonical(collection: CollectionName) {\n if (catalogs.has(collection))\n throw new Error(\"Catalog persistence is unavailable in indexed stores\");\n }\n async function write(\n collection: CollectionName,\n id: string,\n value: unknown,\n expectedVersion: number,\n ) {\n assertCanonical(collection);\n if (collection === \"images\") throw new Error(\"Images must use object storage\");\n const canonical = storedJson(value);\n const indexes = projectIndexedRecord(collection, id, canonical).map((entry) => ({\n ...entry,\n summary: storedJson(entry.summary),\n }));\n const intent = { collection, id, value: canonical, expectedVersion, indexes };\n // Driver calls retain insert/CAS conflict semantics. The indexed adapter uses this identity\n // to reconcile an ambiguous response within this call, not to replay a later engine call.\n const operationId = randomUUID();\n await store.write({ ...intent, operationId });\n }\n const driver: StoreDriver = {\n name: store.name,\n async get(collection, id): Promise<StoredRecord | null> {\n assertCanonical(collection);\n return collection === \"images\" ? images.get(collection, id) : store.get(collection, id);\n },\n async insert(input) {\n if (input.collection === \"images\") return images.insert(input);\n await write(input.collection, input.id, input.value, 0);\n },\n async update(input) {\n await write(input.collection, input.id, input.value, input.expectedVersion);\n },\n async list() {\n throw new Error(\"Indexed stores require bounded queries instead of collection lists\");\n },\n };\n const stores = createStoreEngine({ ...options, driver }, store);\n return {\n ...stores,\n conversations: createConversationStore(store),\n approvals: createApprovalStore(store),\n transcripts: createIndexedTranscriptStore(\n store,\n options.now ?? (() => new Date().toISOString()),\n ),\n indexes: {\n async query(input) {\n const scope = factoryIndexScopeSchema.parse(input.scope);\n const index = factoryIndexKey(scope);\n const page = await store.query({\n index,\n limit: input.limit,\n cursor: input.cursor,\n order: input.order,\n });\n return {\n entries: page.entries.map((entry) => {\n const summary = factoryRecordSummarySchema.parse(entry.summary);\n if (\n entry.index !== index ||\n entry.collection !== scope.collection ||\n summary.collection !== entry.collection ||\n summary.id !== entry.id\n )\n throw new Error(\"Indexed summary identity mismatch\");\n return { summary, version: z.number().int().positive().parse(entry.version) };\n }),\n nextCursor: page.nextCursor,\n };\n },\n count(scope) {\n return store.count(factoryIndexKey(scope));\n },\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { IndexedStore } from "./indexed.mjs";
|
|
2
|
+
//#region src/store/indexed-memory.d.ts
|
|
3
|
+
/** Creates an isolated transactional indexed store for local fixtures and tests. */
|
|
4
|
+
declare function createInMemoryIndexedStore(): IndexedStore;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { createInMemoryIndexedStore };
|
|
7
|
+
//# sourceMappingURL=indexed-memory.d.mts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { RecordExistsError, RecordNotFoundError, VersionConflictError } from "./driver.mjs";
|
|
2
|
+
import { compareStorageText, matchesRecord, queryIdentity, recordQuerySchema } from "./query.mjs";
|
|
3
|
+
import { StoreOperationConflictError } from "./indexed.mjs";
|
|
4
|
+
import { canonicalJson, collectionNameSchema, decodeCursor, digest, encodeCursor, indexKeySchema, recordIdSchema, sortKeySchema } from "../postgres/indexed-codec.mjs";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
//#region src/store/indexed-memory.ts
|
|
8
|
+
/** Creates an isolated transactional indexed store for local fixtures and tests. */
|
|
9
|
+
function createInMemoryIndexedStore() {
|
|
10
|
+
const namespace = randomUUID();
|
|
11
|
+
let state = {
|
|
12
|
+
countAttributes: /* @__PURE__ */ new Map(),
|
|
13
|
+
records: /* @__PURE__ */ new Map(),
|
|
14
|
+
entries: /* @__PURE__ */ new Map(),
|
|
15
|
+
operations: /* @__PURE__ */ new Map(),
|
|
16
|
+
byIndex: /* @__PURE__ */ new Map()
|
|
17
|
+
};
|
|
18
|
+
let available = Promise.resolve();
|
|
19
|
+
function key(collection, id) {
|
|
20
|
+
return JSON.stringify([collectionNameSchema.parse(collection), recordIdSchema.parse(id)]);
|
|
21
|
+
}
|
|
22
|
+
function ordered(left, right) {
|
|
23
|
+
return compareStorageText(left.sortKey, right.sortKey) || compareStorageText(left.collection, right.collection) || compareStorageText(left.id, right.id);
|
|
24
|
+
}
|
|
25
|
+
function pageRows(rows, index, order, limit, encoded) {
|
|
26
|
+
const cursor = encoded === void 0 ? void 0 : decodeCursor(encoded, namespace, index, order);
|
|
27
|
+
const sign = order === "asc" ? 1 : -1;
|
|
28
|
+
const filtered = rows.filter((row) => !cursor || sign * ordered(row, {
|
|
29
|
+
...cursor,
|
|
30
|
+
summary: null,
|
|
31
|
+
version: 0
|
|
32
|
+
}) > 0).sort((left, right) => sign * ordered(left, right));
|
|
33
|
+
const entries = filtered.slice(0, limit);
|
|
34
|
+
const last = entries.at(-1);
|
|
35
|
+
return {
|
|
36
|
+
entries: structuredClone(entries),
|
|
37
|
+
nextCursor: filtered.length > entries.length && last ? encodeCursor({
|
|
38
|
+
version: 1,
|
|
39
|
+
namespace,
|
|
40
|
+
index,
|
|
41
|
+
order,
|
|
42
|
+
sortKey: last.sortKey,
|
|
43
|
+
collection: last.collection,
|
|
44
|
+
id: last.id
|
|
45
|
+
}) : null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function create(snapshot) {
|
|
49
|
+
const current = () => snapshot ?? state;
|
|
50
|
+
const changedIndexes = /* @__PURE__ */ new Set();
|
|
51
|
+
function writableIndex(index) {
|
|
52
|
+
if (!snapshot) throw new Error("Index writes require a transaction");
|
|
53
|
+
if (!changedIndexes.has(index)) {
|
|
54
|
+
snapshot.byIndex.set(index, new Map(snapshot.byIndex.get(index)));
|
|
55
|
+
changedIndexes.add(index);
|
|
56
|
+
}
|
|
57
|
+
return snapshot.byIndex.get(index);
|
|
58
|
+
}
|
|
59
|
+
const store = {
|
|
60
|
+
name: "memory",
|
|
61
|
+
async transaction(run) {
|
|
62
|
+
if (snapshot) return run(store);
|
|
63
|
+
const preceding = available;
|
|
64
|
+
let release;
|
|
65
|
+
available = new Promise((resolve) => {
|
|
66
|
+
release = resolve;
|
|
67
|
+
});
|
|
68
|
+
await preceding;
|
|
69
|
+
try {
|
|
70
|
+
const next = {
|
|
71
|
+
records: new Map(state.records),
|
|
72
|
+
entries: new Map(state.entries),
|
|
73
|
+
operations: new Map(state.operations),
|
|
74
|
+
countAttributes: new Map(state.countAttributes),
|
|
75
|
+
byIndex: new Map(state.byIndex)
|
|
76
|
+
};
|
|
77
|
+
const value = await run(create(next));
|
|
78
|
+
state = next;
|
|
79
|
+
return value;
|
|
80
|
+
} finally {
|
|
81
|
+
release();
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
async get(collection, id) {
|
|
85
|
+
return structuredClone(current().records.get(key(collection, id)) ?? null);
|
|
86
|
+
},
|
|
87
|
+
async write(input) {
|
|
88
|
+
if (!snapshot) return store.transaction((transaction) => transaction.write(input));
|
|
89
|
+
const recordKey = key(input.collection, input.id);
|
|
90
|
+
const operationId = recordIdSchema.parse(input.operationId);
|
|
91
|
+
const expectedVersion = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER - 1).parse(input.expectedVersion);
|
|
92
|
+
if (input.indexes.length > 32) throw new Error("A record can belong to at most 32 indexes");
|
|
93
|
+
const indexes = input.indexes.map((entry) => ({
|
|
94
|
+
index: indexKeySchema.parse(entry.index),
|
|
95
|
+
sortKey: sortKeySchema.parse(entry.sortKey),
|
|
96
|
+
summary: JSON.parse(canonicalJson(entry.summary, 8192)),
|
|
97
|
+
...entry.countGroup ? { countGroup: {
|
|
98
|
+
group: indexKeySchema.parse(entry.countGroup.group),
|
|
99
|
+
attributes: JSON.parse(canonicalJson(entry.countGroup.attributes, 8192))
|
|
100
|
+
} } : {}
|
|
101
|
+
})).sort((a, b) => compareStorageText(a.index, b.index));
|
|
102
|
+
if (new Set(indexes.map((entry) => entry.index)).size !== indexes.length) throw new Error("Duplicate index membership");
|
|
103
|
+
const value = JSON.parse(canonicalJson(input.value, 262144));
|
|
104
|
+
const fingerprint = digest(canonicalJson({
|
|
105
|
+
collection: input.collection,
|
|
106
|
+
id: input.id,
|
|
107
|
+
expectedVersion,
|
|
108
|
+
value,
|
|
109
|
+
indexes
|
|
110
|
+
}, 614400));
|
|
111
|
+
const previous = snapshot.operations.get(operationId);
|
|
112
|
+
if (previous) {
|
|
113
|
+
if (previous.fingerprint !== fingerprint) throw new StoreOperationConflictError(operationId);
|
|
114
|
+
return { version: previous.version };
|
|
115
|
+
}
|
|
116
|
+
const existing = snapshot.records.get(recordKey);
|
|
117
|
+
if (expectedVersion === 0 && existing) throw new RecordExistsError(input.collection, input.id);
|
|
118
|
+
if (expectedVersion > 0 && !existing) throw new RecordNotFoundError(input.collection, input.id);
|
|
119
|
+
if (existing && existing.version !== expectedVersion) throw new VersionConflictError(input.collection, input.id, expectedVersion);
|
|
120
|
+
for (const entry of indexes) {
|
|
121
|
+
const attributes = canonicalJson(entry.countGroup ?? null, 16384);
|
|
122
|
+
const previous = snapshot.countAttributes.get(entry.index);
|
|
123
|
+
if (previous !== void 0 && previous !== attributes) throw new Error("Index count attributes are immutable");
|
|
124
|
+
snapshot.countAttributes.set(entry.index, attributes);
|
|
125
|
+
}
|
|
126
|
+
const version = expectedVersion + 1;
|
|
127
|
+
snapshot.records.set(recordKey, {
|
|
128
|
+
value,
|
|
129
|
+
version
|
|
130
|
+
});
|
|
131
|
+
for (const entry of snapshot.entries.get(recordKey) ?? []) writableIndex(entry.index).delete(recordKey);
|
|
132
|
+
const entries = indexes.map((entry) => ({
|
|
133
|
+
...entry,
|
|
134
|
+
collection: input.collection,
|
|
135
|
+
id: input.id,
|
|
136
|
+
version
|
|
137
|
+
}));
|
|
138
|
+
for (const entry of entries) writableIndex(entry.index).set(recordKey, entry);
|
|
139
|
+
snapshot.entries.set(recordKey, entries);
|
|
140
|
+
snapshot.operations.set(operationId, {
|
|
141
|
+
fingerprint,
|
|
142
|
+
version
|
|
143
|
+
});
|
|
144
|
+
return { version };
|
|
145
|
+
},
|
|
146
|
+
async query(input) {
|
|
147
|
+
const index = indexKeySchema.parse(input.index);
|
|
148
|
+
const limit = z.number().int().min(1).max(100).parse(input.limit);
|
|
149
|
+
const order = z.enum(["asc", "desc"]).parse(input.order ?? "desc");
|
|
150
|
+
return pageRows([...current().byIndex.get(index)?.values() ?? []], index, order, limit, input.cursor);
|
|
151
|
+
},
|
|
152
|
+
async queryRecords({ sources: inputSources, ...input }) {
|
|
153
|
+
const sources = z.array(z.strictObject({
|
|
154
|
+
collection: collectionNameSchema,
|
|
155
|
+
index: indexKeySchema
|
|
156
|
+
})).min(1).max(8).parse(inputSources).sort((a, b) => compareStorageText(a.collection, b.collection));
|
|
157
|
+
if (new Set(sources.map((source) => source.collection)).size !== sources.length) throw new Error("Record queries require exactly one source index per collection");
|
|
158
|
+
const query = recordQuerySchema.parse(input);
|
|
159
|
+
const index = `records:${queryIdentity(JSON.stringify(sources), query)}`;
|
|
160
|
+
const records = current();
|
|
161
|
+
const rows = sources.flatMap((source) => [...records.byIndex.get(source.index)?.values() ?? []].filter((entry) => entry.collection === source.collection)).filter((entry) => matchesRecord(records.records.get(key(entry.collection, entry.id)).value, {
|
|
162
|
+
...query,
|
|
163
|
+
since: void 0,
|
|
164
|
+
until: void 0
|
|
165
|
+
}) && (!query.since || entry.sortKey >= query.since) && (!query.until || entry.sortKey < query.until));
|
|
166
|
+
const page = pageRows(rows, index, query.order, query.limit, query.cursor);
|
|
167
|
+
return {
|
|
168
|
+
...query.includeTotal ? { totalCount: rows.length } : {},
|
|
169
|
+
entries: page.entries.map((entry) => ({
|
|
170
|
+
collection: entry.collection,
|
|
171
|
+
id: entry.id,
|
|
172
|
+
record: structuredClone(records.records.get(key(entry.collection, entry.id)))
|
|
173
|
+
})),
|
|
174
|
+
nextCursor: page.nextCursor
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
async sumCounts(group, query) {
|
|
178
|
+
let total = 0;
|
|
179
|
+
for (const [index, attributes] of current().countAttributes) {
|
|
180
|
+
const metadata = JSON.parse(attributes);
|
|
181
|
+
if (metadata?.group === group && matchesRecord(metadata.attributes, {
|
|
182
|
+
...query,
|
|
183
|
+
limit: 1
|
|
184
|
+
})) total += current().byIndex.get(index)?.size ?? 0;
|
|
185
|
+
}
|
|
186
|
+
return total;
|
|
187
|
+
},
|
|
188
|
+
async count(index) {
|
|
189
|
+
indexKeySchema.parse(index);
|
|
190
|
+
return current().byIndex.get(index)?.size ?? 0;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
return store;
|
|
194
|
+
}
|
|
195
|
+
return create();
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
export { createInMemoryIndexedStore };
|
|
199
|
+
|
|
200
|
+
//# sourceMappingURL=indexed-memory.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexed-memory.mjs","names":[],"sources":["../../src/store/indexed-memory.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { z } from \"zod\";\nimport {\n canonicalJson,\n collectionNameSchema,\n decodeCursor,\n digest,\n encodeCursor,\n indexKeySchema,\n recordIdSchema,\n sortKeySchema,\n} from \"../postgres/indexed-codec\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type StoredRecord,\n} from \"./driver\";\nimport { StoreOperationConflictError, type IndexedStore, type StoreIndexRecord } from \"./indexed\";\nimport { compareStorageText, matchesRecord, queryIdentity, recordQuerySchema } from \"./query\";\n\ninterface MemoryState {\n countAttributes: Map<string, string>;\n records: Map<string, StoredRecord>;\n entries: Map<string, StoreIndexRecord[]>;\n operations: Map<string, { fingerprint: string; version: number }>;\n byIndex: Map<string, Map<string, StoreIndexRecord>>;\n}\n\n/** Creates an isolated transactional indexed store for local fixtures and tests. */\nexport function createInMemoryIndexedStore(): IndexedStore {\n const namespace = randomUUID();\n let state: MemoryState = {\n countAttributes: new Map(),\n records: new Map(),\n entries: new Map(),\n operations: new Map(),\n byIndex: new Map(),\n };\n let available = Promise.resolve();\n function key(collection: string, id: string) {\n return JSON.stringify([collectionNameSchema.parse(collection), recordIdSchema.parse(id)]);\n }\n function ordered(left: StoreIndexRecord, right: StoreIndexRecord) {\n return (\n compareStorageText(left.sortKey, right.sortKey) ||\n compareStorageText(left.collection, right.collection) ||\n compareStorageText(left.id, right.id)\n );\n }\n function pageRows(\n rows: StoreIndexRecord[],\n index: string,\n order: \"asc\" | \"desc\",\n limit: number,\n encoded?: string,\n ) {\n const cursor =\n encoded === undefined ? undefined : decodeCursor(encoded, namespace, index, order);\n const sign = order === \"asc\" ? 1 : -1;\n const filtered = rows\n .filter((row) => !cursor || sign * ordered(row, { ...cursor, summary: null, version: 0 }) > 0)\n .sort((left, right) => sign * ordered(left, right));\n const entries = filtered.slice(0, limit);\n const last = entries.at(-1);\n return {\n entries: structuredClone(entries),\n nextCursor:\n filtered.length > entries.length && last\n ? encodeCursor({\n version: 1,\n namespace,\n index,\n order,\n sortKey: last.sortKey,\n collection: last.collection,\n id: last.id,\n })\n : null,\n };\n }\n function create(snapshot?: MemoryState): IndexedStore {\n const current = () => snapshot ?? state;\n const changedIndexes = new Set<string>();\n function writableIndex(index: string) {\n if (!snapshot) throw new Error(\"Index writes require a transaction\");\n if (!changedIndexes.has(index)) {\n snapshot.byIndex.set(index, new Map(snapshot.byIndex.get(index)));\n changedIndexes.add(index);\n }\n return snapshot.byIndex.get(index)!;\n }\n const store: IndexedStore = {\n name: \"memory\",\n async transaction(run) {\n if (snapshot) return run(store);\n const preceding = available;\n let release!: () => void;\n available = new Promise<void>((resolve) => {\n release = resolve;\n });\n await preceding;\n try {\n // Values are replaced on write and cloned at every public read boundary.\n const next: MemoryState = {\n records: new Map(state.records),\n entries: new Map(state.entries),\n operations: new Map(state.operations),\n countAttributes: new Map(state.countAttributes),\n byIndex: new Map(state.byIndex),\n };\n const value = await run(create(next));\n state = next;\n return value;\n } finally {\n release();\n }\n },\n async get(collection, id) {\n return structuredClone(current().records.get(key(collection, id)) ?? null);\n },\n async write(input) {\n if (!snapshot) return store.transaction((transaction) => transaction.write(input));\n const recordKey = key(input.collection, input.id);\n const operationId = recordIdSchema.parse(input.operationId);\n const expectedVersion = z\n .number()\n .int()\n .min(0)\n .max(Number.MAX_SAFE_INTEGER - 1)\n .parse(input.expectedVersion);\n if (input.indexes.length > 32) throw new Error(\"A record can belong to at most 32 indexes\");\n const indexes = input.indexes\n .map((entry) => ({\n index: indexKeySchema.parse(entry.index),\n sortKey: sortKeySchema.parse(entry.sortKey),\n summary: JSON.parse(canonicalJson(entry.summary, 8 * 1024)) as unknown,\n ...(entry.countGroup\n ? {\n countGroup: {\n group: indexKeySchema.parse(entry.countGroup.group),\n attributes: JSON.parse(\n canonicalJson(entry.countGroup.attributes, 8 * 1024),\n ) as unknown,\n },\n }\n : {}),\n }))\n .sort((a, b) => compareStorageText(a.index, b.index));\n if (new Set(indexes.map((entry) => entry.index)).size !== indexes.length)\n throw new Error(\"Duplicate index membership\");\n const value = JSON.parse(canonicalJson(input.value, 256 * 1024)) as unknown;\n const fingerprint = digest(\n canonicalJson(\n { collection: input.collection, id: input.id, expectedVersion, value, indexes },\n 600 * 1024,\n ),\n );\n const previous = snapshot.operations.get(operationId);\n if (previous) {\n if (previous.fingerprint !== fingerprint)\n throw new StoreOperationConflictError(operationId);\n return { version: previous.version };\n }\n const existing = snapshot.records.get(recordKey);\n if (expectedVersion === 0 && existing)\n throw new RecordExistsError(input.collection, input.id);\n if (expectedVersion > 0 && !existing)\n throw new RecordNotFoundError(input.collection, input.id);\n if (existing && existing.version !== expectedVersion)\n throw new VersionConflictError(input.collection, input.id, expectedVersion);\n for (const entry of indexes) {\n const attributes = canonicalJson(entry.countGroup ?? null, 16 * 1024);\n const previous = snapshot.countAttributes.get(entry.index);\n if (previous !== undefined && previous !== attributes)\n throw new Error(\"Index count attributes are immutable\");\n snapshot.countAttributes.set(entry.index, attributes);\n }\n const version = expectedVersion + 1;\n snapshot.records.set(recordKey, { value, version });\n for (const entry of snapshot.entries.get(recordKey) ?? [])\n writableIndex(entry.index).delete(recordKey);\n const entries = indexes.map((entry) => ({\n ...entry,\n collection: input.collection,\n id: input.id,\n version,\n }));\n for (const entry of entries) writableIndex(entry.index).set(recordKey, entry);\n snapshot.entries.set(recordKey, entries);\n snapshot.operations.set(operationId, { fingerprint, version });\n return { version };\n },\n async query(input) {\n const index = indexKeySchema.parse(input.index);\n const limit = z.number().int().min(1).max(100).parse(input.limit);\n const order = z.enum([\"asc\", \"desc\"]).parse(input.order ?? \"desc\");\n return pageRows(\n [...(current().byIndex.get(index)?.values() ?? [])],\n index,\n order,\n limit,\n input.cursor,\n );\n },\n async queryRecords({ sources: inputSources, ...input }) {\n const sources = z\n .array(z.strictObject({ collection: collectionNameSchema, index: indexKeySchema }))\n .min(1)\n .max(8)\n .parse(inputSources)\n .sort((a, b) => compareStorageText(a.collection, b.collection));\n if (new Set(sources.map((source) => source.collection)).size !== sources.length)\n throw new Error(\"Record queries require exactly one source index per collection\");\n const query = recordQuerySchema.parse(input);\n const index = `records:${queryIdentity(JSON.stringify(sources), query)}`;\n const records = current();\n const rows = sources\n .flatMap((source) =>\n [...(records.byIndex.get(source.index)?.values() ?? [])].filter(\n (entry) => entry.collection === source.collection,\n ),\n )\n .filter(\n (entry) =>\n matchesRecord(records.records.get(key(entry.collection, entry.id))!.value, {\n ...query,\n since: undefined,\n until: undefined,\n }) &&\n (!query.since || entry.sortKey >= query.since) &&\n (!query.until || entry.sortKey < query.until),\n );\n const page = pageRows(rows, index, query.order, query.limit, query.cursor);\n return {\n ...(query.includeTotal ? { totalCount: rows.length } : {}),\n entries: page.entries.map((entry) => ({\n collection: entry.collection,\n id: entry.id,\n record: structuredClone(records.records.get(key(entry.collection, entry.id))!),\n })),\n nextCursor: page.nextCursor,\n };\n },\n async sumCounts(group, query) {\n let total = 0;\n for (const [index, attributes] of current().countAttributes) {\n const metadata = JSON.parse(attributes) as StoreIndexRecord[\"countGroup\"] | null;\n if (\n metadata?.group === group &&\n matchesRecord(metadata.attributes, { ...query, limit: 1 })\n )\n total += current().byIndex.get(index)?.size ?? 0;\n }\n return total;\n },\n async count(index) {\n indexKeySchema.parse(index);\n return current().byIndex.get(index)?.size ?? 0;\n },\n };\n return store;\n }\n return create();\n}\n"],"mappings":"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"}
|