@evomap/evolver-core 2.0.0-beta.2 → 2.0.0-beta.22
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/assets/gep/genes.jsonl +5 -5
- package/dist/algo/bans.d.ts +10 -1
- package/dist/algo/bans.js +56 -6
- package/dist/algo/candidateAssembly.d.ts +25 -2
- package/dist/algo/candidateAssembly.js +171 -26
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/conversationSniffer.js +25 -8
- package/dist/algo/cycleEngine.d.ts +63 -5
- package/dist/algo/cycleEngine.js +320 -42
- package/dist/algo/cycleFailureClassifier.d.ts +1 -1
- package/dist/algo/evolutionEvent.d.ts +26 -0
- package/dist/algo/evolutionEvent.js +32 -0
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +36 -3
- package/dist/algo/geneHealth.js +47 -4
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +126 -6
- package/dist/algo/geneSelection.js +479 -36
- package/dist/algo/index.d.ts +6 -1
- package/dist/algo/index.js +6 -1
- package/dist/algo/kautoProjection.d.ts +41 -0
- package/dist/algo/kautoProjection.js +95 -0
- package/dist/algo/kautoValidator.d.ts +68 -0
- package/dist/algo/kautoValidator.js +256 -0
- package/dist/algo/memoryGraph.d.ts +62 -0
- package/dist/algo/memoryGraph.js +86 -0
- package/dist/algo/orchestrator.d.ts +17 -1
- package/dist/algo/orchestrator.js +30 -4
- package/dist/algo/publishEligibility.d.ts +34 -0
- package/dist/algo/publishEligibility.js +52 -0
- package/dist/algo/solidify.d.ts +11 -2
- package/dist/algo/solidify.js +37 -7
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetrepair/hubRejection.d.ts +12 -0
- package/dist/assetrepair/hubRejection.js +109 -0
- package/dist/assetrepair/index.d.ts +2 -0
- package/dist/assetrepair/index.js +2 -0
- package/dist/assetrepair/repair.d.ts +33 -0
- package/dist/assetrepair/repair.js +155 -0
- package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
- package/dist/assetstore/assetSidecarRecords.js +384 -0
- package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
- package/dist/assetstore/assetSidecarRecovery.js +288 -0
- package/dist/assetstore/assetStoreHealth.d.ts +81 -0
- package/dist/assetstore/assetStoreHealth.js +319 -0
- package/dist/assetstore/assetStoreLayout.d.ts +2 -0
- package/dist/assetstore/assetStoreLayout.js +6 -0
- package/dist/assetstore/assetStoreStorage.d.ts +42 -0
- package/dist/assetstore/assetStoreStorage.js +336 -0
- package/dist/assetstore/assetSyncLedger.d.ts +91 -1
- package/dist/assetstore/assetSyncLedger.js +718 -59
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +5 -0
- package/dist/assetstore/index.js +5 -0
- package/dist/assetstore/learningHistory.js +3 -3
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +12 -1
- package/dist/assetstore/localJsonl.js +251 -42
- package/dist/assetstore/provenance.d.ts +93 -4
- package/dist/assetstore/provenance.js +368 -84
- package/dist/assetstore/provider.d.ts +63 -0
- package/dist/assetstore/provider.js +97 -6
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +39 -1
- package/dist/assetstore/reviewLedger.d.ts +8 -2
- package/dist/assetstore/reviewLedger.js +71 -45
- package/dist/assetstore/unionReadStore.d.ts +25 -0
- package/dist/assetstore/unionReadStore.js +119 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +3 -1
- package/dist/benchmark/index.js +3 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/benchmark/triggerShift.d.ts +62 -0
- package/dist/benchmark/triggerShift.js +106 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +3 -1
- package/dist/bootstrap/index.js +3 -1
- package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
- package/dist/bootstrap/v1EnvCompat.js +303 -0
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +2 -1
- package/dist/events/ingest.js +15 -0
- package/dist/events/paths.d.ts +12 -10
- package/dist/events/paths.js +24 -20
- package/dist/events/public.d.ts +2 -2
- package/dist/events/public.js +2 -2
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +68 -4
- package/dist/exec/autoExec.js +392 -33
- package/dist/exec/autonomousCycle.d.ts +31 -4
- package/dist/exec/autonomousCycle.js +71 -13
- package/dist/exec/claudeBridge.d.ts +80 -15
- package/dist/exec/claudeBridge.js +845 -76
- package/dist/exec/executionBinding.d.ts +414 -0
- package/dist/exec/executionBinding.js +588 -0
- package/dist/exec/index.d.ts +1 -0
- package/dist/exec/index.js +1 -0
- package/dist/exec/openPrRegistry.d.ts +8 -2
- package/dist/exec/openPrRegistry.js +32 -22
- package/dist/exec/prompt.js +14 -1
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +153 -36
- package/dist/exec/runnerRegistry.js +848 -65
- package/dist/exec/selfPr.js +1 -7
- package/dist/feedback/envelope.d.ts +61 -0
- package/dist/feedback/envelope.js +168 -0
- package/dist/feedback/index.d.ts +1 -0
- package/dist/feedback/index.js +1 -0
- package/dist/hooks/hooks.js +1 -0
- package/dist/hub/assetCallLog.d.ts +35 -1
- package/dist/hub/assetCallLog.js +124 -1
- package/dist/hub/bindings.d.ts +8 -1
- package/dist/hub/bindings.js +29 -8
- package/dist/hub/capability.d.ts +130 -4
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +3 -2
- package/dist/hub/fake.js +2 -1
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/questionGenerator.d.ts +5 -1
- package/dist/hub/questionGenerator.js +8 -6
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +122 -7
- package/dist/index.d.ts +6 -1
- package/dist/index.js +7 -1
- package/dist/issueReporter/index.d.ts +156 -0
- package/dist/issueReporter/index.js +1688 -0
- package/dist/mailbox/dispatch.d.ts +1 -1
- package/dist/mailbox/dispatch.js +22 -6
- package/dist/mailbox/envelope.d.ts +7 -1
- package/dist/mailbox/envelope.js +9 -2
- package/dist/mailbox/ipcServer.d.ts +12 -2
- package/dist/mailbox/ipcServer.js +183 -13
- package/dist/mailbox/store.d.ts +89 -3
- package/dist/mailbox/store.js +895 -41
- package/dist/modelCompatibility.d.ts +164 -0
- package/dist/modelCompatibility.js +309 -0
- package/dist/observers/valueDigestObserver.d.ts +9 -0
- package/dist/observers/valueDigestObserver.js +35 -2
- package/dist/ops/cleanup.js +1 -1
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/savingsCore.js +1 -2
- package/dist/ops/selfUpdate.d.ts +18 -1
- package/dist/ops/selfUpdate.js +88 -23
- package/dist/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +24 -24
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/proofOfWork.d.ts +125 -6
- package/dist/schema/proofOfWork.js +102 -4
- package/dist/schema/signal.d.ts +3 -3
- package/dist/schema/signal.js +1 -1
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +202 -0
- package/dist/signals/cycleHistoryFromEvents.js +17 -8
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +169 -1
- package/dist/signals/extractor.d.ts +2 -2
- package/dist/signals/extractor.js +31 -6
- package/dist/signals/index.d.ts +4 -1
- package/dist/signals/index.js +4 -1
- package/dist/signals/metaSignals.d.ts +4 -0
- package/dist/signals/metaSignals.js +42 -0
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/signalGate.js +1 -1
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/strategy/constraintAblation.d.ts +64 -0
- package/dist/strategy/constraintAblation.js +2820 -0
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +339 -0
- package/dist/strategy/index.d.ts +2 -1
- package/dist/strategy/index.js +2 -1
- package/dist/trace/index.d.ts +3 -1
- package/dist/trace/index.js +3 -1
- package/dist/trace/learningTrace.d.ts +216 -0
- package/dist/trace/learningTrace.js +298 -0
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fetchPort.d.ts +1 -0
- package/dist/util/fetchPort.js +11 -0
- package/dist/util/fileLock.d.ts +113 -7
- package/dist/util/fileLock.js +1035 -80
- package/dist/util/index.d.ts +3 -1
- package/dist/util/index.js +2 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +30 -0
- package/dist/verify/sandboxRunner.js +370 -27
- package/dist/verify/sandboxedValidation.d.ts +16 -2
- package/dist/verify/sandboxedValidation.js +200 -22
- package/dist/verify/validation.d.ts +35 -4
- package/dist/verify/validation.js +164 -16
- package/dist/wire/geneHints.d.ts +88 -16
- package/dist/wire/geneHints.js +124 -15
- package/dist/wire/index.d.ts +8 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/schemaGate.d.ts +21 -0
- package/dist/wire/schemaGate.js +119 -10
- package/dist/workflow/dsl.d.ts +24 -3
- package/dist/workflow/dsl.js +4 -0
- package/dist/workflow/engine.d.ts +5 -1
- package/dist/workflow/engine.js +3 -0
- package/dist/workflow/index.d.ts +3 -1
- package/dist/workflow/index.js +3 -1
- package/dist/workflow/runtime.d.ts +110 -0
- package/dist/workflow/runtime.js +1298 -0
- package/dist/workflow/stateStore.d.ts +172 -0
- package/dist/workflow/stateStore.js +1044 -0
- package/package.json +12 -4
|
@@ -1,86 +1,745 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import { appendUtf8Durable, assertAssetStoreDirectory, ensureAssetStoreDirectory, readUtf8Regular, regularFileFingerprint, replaceUtf8Durable, withAssetStoreLock, } from './assetStoreStorage.js';
|
|
4
|
+
import { ASSET_SYNC_INVENTORY_MAX_SEGMENT_BYTES, parseAssetSyncInventorySegmentRecord, parseAssetSyncRecord, parseAssetSyncRunRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
|
|
5
|
+
export const ASSET_SYNC_INVENTORY_MAX_SEGMENTS = 24;
|
|
6
|
+
export const ASSET_SYNC_INVENTORY_MAX_ITEMS_PER_SEGMENT = 10_000;
|
|
7
|
+
export const ASSET_SYNC_INVENTORY_MAX_UNIQUE_ITEMS = 240_000;
|
|
8
|
+
export const ASSET_SYNC_INVENTORY_MAX_TOTAL_BYTES = 48 * 1024 * 1024;
|
|
9
|
+
const ASSET_SYNC_SIDECAR_MAX_BYTES = 64 * 1024 * 1024;
|
|
3
10
|
export class AssetSyncLedger {
|
|
4
11
|
now;
|
|
5
12
|
path;
|
|
13
|
+
lockPath;
|
|
6
14
|
index = new Map();
|
|
15
|
+
runKeyIndex = new Map();
|
|
16
|
+
inventoryKeyIndex = new Map();
|
|
17
|
+
fileState = null;
|
|
7
18
|
loaded = false;
|
|
8
19
|
constructor(baseDir, now = Date.now) {
|
|
9
20
|
this.now = now;
|
|
21
|
+
ensureAssetStoreDirectory(baseDir);
|
|
10
22
|
this.path = join(baseDir, 'asset-sync.jsonl');
|
|
23
|
+
this.lockPath = join(baseDir, '.assetstore.lock');
|
|
11
24
|
}
|
|
12
25
|
append(rec) {
|
|
13
|
-
this.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
27
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
28
|
+
this.refreshUnderLock();
|
|
29
|
+
const full = immutableRecord({
|
|
30
|
+
...rec,
|
|
31
|
+
syncedAt: rec.syncedAt ?? new Date(this.now()).toISOString(),
|
|
32
|
+
});
|
|
33
|
+
appendUtf8Durable(this.path, `${JSON.stringify(full)}\n`);
|
|
34
|
+
this.index.set(full.assetId, full);
|
|
35
|
+
if (full.runKey) {
|
|
36
|
+
const scoped = this.runKeyIndex.get(full.runKey) ?? new Map();
|
|
37
|
+
scoped.set(full.assetId, full);
|
|
38
|
+
this.runKeyIndex.set(full.runKey, scoped);
|
|
39
|
+
}
|
|
40
|
+
if (full.inventoryKey) {
|
|
41
|
+
const scoped = this.inventoryKeyIndex.get(full.inventoryKey) ?? new Map();
|
|
42
|
+
scoped.set(full.assetId, full);
|
|
43
|
+
this.inventoryKeyIndex.set(full.inventoryKey, scoped);
|
|
44
|
+
}
|
|
45
|
+
this.fileState = regularFileFingerprint(this.path);
|
|
46
|
+
return full;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
appendRun(rec) {
|
|
50
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
51
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
52
|
+
this.refreshUnderLock();
|
|
53
|
+
const full = parseAssetSyncRunRecord({
|
|
54
|
+
recordType: 'run',
|
|
55
|
+
...rec,
|
|
56
|
+
syncedAt: rec.syncedAt ?? new Date(this.now()).toISOString(),
|
|
57
|
+
});
|
|
58
|
+
if (!full)
|
|
59
|
+
throw new Error('invalid asset sync run record');
|
|
60
|
+
appendUtf8Durable(this.path, `${JSON.stringify(full)}\n`);
|
|
61
|
+
this.fileState = regularFileFingerprint(this.path);
|
|
62
|
+
return full;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
appendInventorySegment(rec) {
|
|
66
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
67
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
68
|
+
this.refreshUnderLock();
|
|
69
|
+
const full = parseAssetSyncInventorySegmentRecord({
|
|
70
|
+
recordType: 'inventory_scan',
|
|
71
|
+
...rec,
|
|
72
|
+
syncedAt: rec.syncedAt ?? new Date(this.now()).toISOString(),
|
|
73
|
+
});
|
|
74
|
+
if (!full)
|
|
75
|
+
return null;
|
|
76
|
+
const state = readUtf8Regular(this.path) ?? '';
|
|
77
|
+
const line = `${JSON.stringify(full)}\n`;
|
|
78
|
+
if (full.index === 0) {
|
|
79
|
+
const compacted = removeInventoryScan(state, full.inventoryKey);
|
|
80
|
+
const replacement = `${compacted}${line}`;
|
|
81
|
+
if (!canAppendInventorySegment(undefined, full)
|
|
82
|
+
|| inventoryScanBytes(replacement) > ASSET_SYNC_INVENTORY_MAX_TOTAL_BYTES
|
|
83
|
+
|| Buffer.byteLength(replacement, 'utf8') > ASSET_SYNC_SIDECAR_MAX_BYTES) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
replaceUtf8Durable(this.path, replacement);
|
|
87
|
+
this.rebuildIndex(regularFileFingerprint(this.path));
|
|
88
|
+
return full;
|
|
89
|
+
}
|
|
90
|
+
const current = this.latestInventorySnapshotUnderLock(full.inventoryKey);
|
|
91
|
+
if (!canAppendInventorySegment(current, full))
|
|
92
|
+
return null;
|
|
93
|
+
if (inventoryScanBytes(state) + Buffer.byteLength(line, 'utf8') > ASSET_SYNC_INVENTORY_MAX_TOTAL_BYTES
|
|
94
|
+
|| Buffer.byteLength(state, 'utf8') + Buffer.byteLength(line, 'utf8') > ASSET_SYNC_SIDECAR_MAX_BYTES) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
appendUtf8Durable(this.path, line);
|
|
98
|
+
this.fileState = regularFileFingerprint(this.path);
|
|
99
|
+
return full;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
appendInventoryBatch(rec) {
|
|
103
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
104
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
105
|
+
this.refreshUnderLock();
|
|
106
|
+
const records = createInventoryBatchRecords({
|
|
107
|
+
...rec,
|
|
108
|
+
syncedAt: rec.syncedAt ?? new Date(this.now()).toISOString(),
|
|
109
|
+
});
|
|
110
|
+
if (!records)
|
|
111
|
+
return null;
|
|
112
|
+
const state = readUtf8Regular(this.path) ?? '';
|
|
113
|
+
const lines = records.map((record) => `${JSON.stringify(record)}\n`).join('');
|
|
114
|
+
const current = rec.index === 0 ? undefined : this.latestInventorySnapshotUnderLock(rec.inventoryKey);
|
|
115
|
+
if (!canAppendInventoryBatch(current, records))
|
|
116
|
+
return null;
|
|
117
|
+
if (rec.index === 0) {
|
|
118
|
+
const compacted = removeInventoryScan(state, rec.inventoryKey);
|
|
119
|
+
const replacement = `${compacted}${lines}`;
|
|
120
|
+
if (inventoryScanBytes(replacement) > ASSET_SYNC_INVENTORY_MAX_TOTAL_BYTES
|
|
121
|
+
|| Buffer.byteLength(replacement, 'utf8') > ASSET_SYNC_SIDECAR_MAX_BYTES) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
replaceUtf8Durable(this.path, replacement);
|
|
125
|
+
this.rebuildIndex(regularFileFingerprint(this.path));
|
|
126
|
+
return records;
|
|
127
|
+
}
|
|
128
|
+
if (inventoryScanBytes(state) + Buffer.byteLength(lines, 'utf8') > ASSET_SYNC_INVENTORY_MAX_TOTAL_BYTES
|
|
129
|
+
|| Buffer.byteLength(state, 'utf8') + Buffer.byteLength(lines, 'utf8') > ASSET_SYNC_SIDECAR_MAX_BYTES) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
appendUtf8Durable(this.path, lines);
|
|
133
|
+
this.fileState = regularFileFingerprint(this.path);
|
|
134
|
+
return records;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
replaceInventoryRetryBatch(rec) {
|
|
138
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
139
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
140
|
+
this.refreshUnderLock();
|
|
141
|
+
const state = readUtf8Regular(this.path) ?? '';
|
|
142
|
+
const current = this.latestInventorySnapshotUnderLock(rec.inventoryKey);
|
|
143
|
+
if (!current
|
|
144
|
+
|| current.retryIndex === undefined
|
|
145
|
+
|| rec.index !== current.retryIndex
|
|
146
|
+
|| rec.scanId !== current.scanId
|
|
147
|
+
|| rec.scope !== current.scope
|
|
148
|
+
|| !cursorFingerprintsEqual(rec.inputCursorFingerprints, current.nextCursorFingerprints))
|
|
149
|
+
return null;
|
|
150
|
+
const retryTail = findInventoryRetryTail(state, current);
|
|
151
|
+
if (!retryTail)
|
|
152
|
+
return null;
|
|
153
|
+
const mergedItems = mergeInventoryRetryItems(retryTail.records, rec.items);
|
|
154
|
+
if (!mergedItems)
|
|
155
|
+
return null;
|
|
156
|
+
const previousAnonymousBlocked = retryTail.records.reduce((total, record) => total + record.anonymousBlocked, 0);
|
|
157
|
+
const records = createInventoryBatchRecords({
|
|
158
|
+
...rec,
|
|
159
|
+
items: mergedItems,
|
|
160
|
+
anonymousBlocked: Math.max(previousAnonymousBlocked, rec.anonymousBlocked),
|
|
161
|
+
syncedAt: rec.syncedAt ?? new Date(this.now()).toISOString(),
|
|
162
|
+
});
|
|
163
|
+
if (!records)
|
|
164
|
+
return null;
|
|
165
|
+
const preserved = removeInventoryRetryTail(state, retryTail.lineIndexes);
|
|
166
|
+
const prefix = inventorySnapshotFromRaw(preserved, rec.inventoryKey);
|
|
167
|
+
if (!canAppendInventoryBatch(prefix, records))
|
|
168
|
+
return null;
|
|
169
|
+
const replacement = `${preserved}${records.map((record) => `${JSON.stringify(record)}\n`).join('')}`;
|
|
170
|
+
if (inventoryScanBytes(replacement) > ASSET_SYNC_INVENTORY_MAX_TOTAL_BYTES
|
|
171
|
+
|| Buffer.byteLength(replacement, 'utf8') > ASSET_SYNC_SIDECAR_MAX_BYTES)
|
|
172
|
+
return null;
|
|
173
|
+
replaceUtf8Durable(this.path, replacement);
|
|
174
|
+
this.rebuildIndex(regularFileFingerprint(this.path));
|
|
175
|
+
return records;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
clearInventoryScan(inventoryKey) {
|
|
179
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
180
|
+
withAssetStoreLock(this.lockPath, () => {
|
|
181
|
+
this.refreshUnderLock();
|
|
182
|
+
const state = readUtf8Regular(this.path) ?? '';
|
|
183
|
+
const compacted = removeInventoryScan(state, inventoryKey);
|
|
184
|
+
if (compacted === state)
|
|
185
|
+
return;
|
|
186
|
+
replaceUtf8Durable(this.path, compacted);
|
|
187
|
+
this.rebuildIndex(regularFileFingerprint(this.path));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
runRecords(runId) {
|
|
191
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
192
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
193
|
+
this.refreshUnderLock();
|
|
194
|
+
return this.readRunRecordsUnderLock().filter((record) => record.runId === runId);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
latestIncompleteRun(runKey) {
|
|
198
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
199
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
200
|
+
this.refreshUnderLock();
|
|
201
|
+
let current;
|
|
202
|
+
for (const record of this.readRunRecordsUnderLock()) {
|
|
203
|
+
if (record.runKey !== runKey)
|
|
204
|
+
continue;
|
|
205
|
+
if (record.state === 'started') {
|
|
206
|
+
current = Object.freeze({
|
|
207
|
+
...record,
|
|
208
|
+
processed: current?.runId === record.runId ? current.processed : new Map(),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (record.state === 'progress' && record.remoteAssetId && record.outcome) {
|
|
212
|
+
if (current?.runId === record.runId) {
|
|
213
|
+
const processed = new Map(current.processed);
|
|
214
|
+
processed.set(record.remoteAssetId, record);
|
|
215
|
+
current = Object.freeze({ ...current, processed });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (record.state === 'completed' && current?.runId === record.runId)
|
|
219
|
+
current = undefined;
|
|
220
|
+
}
|
|
221
|
+
return current;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
latestInventoryScan(inventoryKey) {
|
|
225
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
226
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
227
|
+
this.refreshUnderLock();
|
|
228
|
+
return this.latestInventorySnapshotUnderLock(inventoryKey);
|
|
229
|
+
});
|
|
22
230
|
}
|
|
23
231
|
get(assetId) {
|
|
24
|
-
this.
|
|
25
|
-
|
|
232
|
+
return this.withFreshRead((index) => index.get(assetId) ?? null);
|
|
233
|
+
}
|
|
234
|
+
getForRunKey(runKey, assetId) {
|
|
235
|
+
return this.withFreshRead(() => this.runKeyIndex.get(runKey)?.get(assetId) ?? null);
|
|
26
236
|
}
|
|
27
237
|
list() {
|
|
28
|
-
this.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
238
|
+
return this.withFreshRead((index) => [...index.values()]);
|
|
239
|
+
}
|
|
240
|
+
listForRunKey(runKey) {
|
|
241
|
+
return this.withFreshRead(() => [...(this.runKeyIndex.get(runKey)?.values() ?? [])]);
|
|
242
|
+
}
|
|
243
|
+
listForInventoryKey(inventoryKey) {
|
|
244
|
+
return this.withFreshRead(() => [...(this.inventoryKeyIndex.get(inventoryKey)?.values() ?? [])]);
|
|
245
|
+
}
|
|
246
|
+
rebuildIndex(state) {
|
|
247
|
+
const next = new Map();
|
|
248
|
+
const nextRunKeyIndex = new Map();
|
|
249
|
+
const nextInventoryKeyIndex = new Map();
|
|
250
|
+
const raw = state === 'missing' ? null : readUtf8Regular(this.path);
|
|
251
|
+
if (raw !== null) {
|
|
252
|
+
const parsed = parseSidecarJsonl(raw, parseAssetSyncRecord);
|
|
253
|
+
for (const parsedRecord of parsed.records) {
|
|
254
|
+
const record = immutableRecord(parsedRecord);
|
|
255
|
+
next.set(record.assetId, record);
|
|
256
|
+
if (record.runKey) {
|
|
257
|
+
const scoped = nextRunKeyIndex.get(record.runKey) ?? new Map();
|
|
258
|
+
scoped.set(record.assetId, record);
|
|
259
|
+
nextRunKeyIndex.set(record.runKey, scoped);
|
|
42
260
|
}
|
|
43
|
-
|
|
44
|
-
|
|
261
|
+
if (record.inventoryKey) {
|
|
262
|
+
const scoped = nextInventoryKeyIndex.get(record.inventoryKey) ?? new Map();
|
|
263
|
+
scoped.set(record.assetId, record);
|
|
264
|
+
nextInventoryKeyIndex.set(record.inventoryKey, scoped);
|
|
45
265
|
}
|
|
46
266
|
}
|
|
47
267
|
}
|
|
268
|
+
this.index.clear();
|
|
269
|
+
for (const [assetId, record] of next)
|
|
270
|
+
this.index.set(assetId, record);
|
|
271
|
+
this.runKeyIndex.clear();
|
|
272
|
+
for (const [runKey, records] of nextRunKeyIndex)
|
|
273
|
+
this.runKeyIndex.set(runKey, records);
|
|
274
|
+
this.inventoryKeyIndex.clear();
|
|
275
|
+
for (const [inventoryKey, records] of nextInventoryKeyIndex)
|
|
276
|
+
this.inventoryKeyIndex.set(inventoryKey, records);
|
|
277
|
+
this.fileState = state;
|
|
48
278
|
this.loaded = true;
|
|
49
279
|
}
|
|
280
|
+
refreshUnderLock() {
|
|
281
|
+
const state = regularFileFingerprint(this.path);
|
|
282
|
+
if (!this.loaded || state !== this.fileState)
|
|
283
|
+
this.rebuildIndex(state);
|
|
284
|
+
}
|
|
285
|
+
withFreshRead(read) {
|
|
286
|
+
assertAssetStoreDirectory(dirname(this.path));
|
|
287
|
+
return withAssetStoreLock(this.lockPath, () => {
|
|
288
|
+
this.refreshUnderLock();
|
|
289
|
+
return read(this.index);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
readRunRecordsUnderLock() {
|
|
293
|
+
const raw = readUtf8Regular(this.path);
|
|
294
|
+
if (raw === null)
|
|
295
|
+
return [];
|
|
296
|
+
const records = [];
|
|
297
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
298
|
+
if (!line.trim())
|
|
299
|
+
continue;
|
|
300
|
+
try {
|
|
301
|
+
const record = parseAssetSyncRunRecord(JSON.parse(line));
|
|
302
|
+
if (record)
|
|
303
|
+
records.push(record);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Corrupt sidecar lines cannot make all resumable work unreadable.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return records;
|
|
310
|
+
}
|
|
311
|
+
latestInventorySnapshotUnderLock(inventoryKey) {
|
|
312
|
+
const raw = readUtf8Regular(this.path);
|
|
313
|
+
return raw === null ? undefined : inventorySnapshotFromRaw(raw, inventoryKey);
|
|
314
|
+
}
|
|
50
315
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
316
|
+
function inventorySnapshotFromRaw(raw, inventoryKey) {
|
|
317
|
+
let current;
|
|
318
|
+
let pendingBatch = [];
|
|
319
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
320
|
+
if (!line.trim())
|
|
321
|
+
continue;
|
|
322
|
+
const record = parseInventoryLine(line);
|
|
323
|
+
if (!record || record.inventoryKey !== inventoryKey)
|
|
324
|
+
continue;
|
|
325
|
+
if (record.batchId) {
|
|
326
|
+
if (record.batchIndex === 0) {
|
|
327
|
+
pendingBatch = [record];
|
|
328
|
+
}
|
|
329
|
+
else if (continuesPendingBatch(pendingBatch, record)) {
|
|
330
|
+
pendingBatch.push(record);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
pendingBatch = [];
|
|
334
|
+
}
|
|
335
|
+
if (pendingBatch.length === record.batchSize) {
|
|
336
|
+
const batch = pendingBatch;
|
|
337
|
+
pendingBatch = [];
|
|
338
|
+
if (!canAppendInventoryBatch(current, batch)) {
|
|
339
|
+
if (current?.scanId === record.scanId)
|
|
340
|
+
current = undefined;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
current = snapshotFromInventoryBatch(current, batch);
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
pendingBatch = [];
|
|
348
|
+
if (record.index === 0) {
|
|
349
|
+
current = snapshotFromFirstSegment(record);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (!current || !canAppendInventorySegment(current, record)) {
|
|
353
|
+
if (current?.scanId === record.scanId)
|
|
354
|
+
current = undefined;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
current = appendSegmentToSnapshot(current, record);
|
|
65
358
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
359
|
+
return current;
|
|
360
|
+
}
|
|
361
|
+
function immutableRecord(record) {
|
|
362
|
+
return Object.freeze({ ...record });
|
|
363
|
+
}
|
|
364
|
+
function createInventoryBatchRecords(rec) {
|
|
365
|
+
if (!Array.isArray(rec.items) || !Number.isInteger(rec.anonymousBlocked) || rec.anonymousBlocked < 0)
|
|
366
|
+
return null;
|
|
367
|
+
const syncedAt = rec.syncedAt ?? new Date().toISOString();
|
|
368
|
+
const common = parseAssetSyncInventorySegmentRecord({
|
|
369
|
+
recordType: 'inventory_scan',
|
|
370
|
+
...rec,
|
|
371
|
+
items: [],
|
|
372
|
+
anonymousBlocked: 0,
|
|
75
373
|
syncedAt,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
374
|
+
});
|
|
375
|
+
const totalItems = rec.items.length + rec.anonymousBlocked;
|
|
376
|
+
const remoteAssetIds = new Set();
|
|
377
|
+
for (const item of rec.items) {
|
|
378
|
+
if (!item || typeof item.remoteAssetId !== 'string' || remoteAssetIds.has(item.remoteAssetId))
|
|
379
|
+
return null;
|
|
380
|
+
remoteAssetIds.add(item.remoteAssetId);
|
|
381
|
+
}
|
|
382
|
+
if (!common
|
|
383
|
+
|| totalItems > ASSET_SYNC_INVENTORY_MAX_UNIQUE_ITEMS)
|
|
384
|
+
return null;
|
|
385
|
+
if (splitInventoryUnits(rec.items, rec.anonymousBlocked).length === 1) {
|
|
386
|
+
const singleton = parseAssetSyncInventorySegmentRecord({
|
|
387
|
+
...common,
|
|
388
|
+
items: rec.items,
|
|
389
|
+
anonymousBlocked: rec.anonymousBlocked,
|
|
390
|
+
});
|
|
391
|
+
if (singleton)
|
|
392
|
+
return Object.freeze([singleton]);
|
|
393
|
+
}
|
|
394
|
+
const batchId = randomUUID();
|
|
395
|
+
const initialChunks = splitInventoryUnits(rec.items, rec.anonymousBlocked);
|
|
396
|
+
const chunks = [];
|
|
397
|
+
for (const chunk of initialChunks) {
|
|
398
|
+
const remainingSegments = ASSET_SYNC_INVENTORY_MAX_SEGMENTS - rec.index - chunks.length;
|
|
399
|
+
const fitted = splitInventoryChunkToFit(common, chunk, batchId, chunks.length, remainingSegments);
|
|
400
|
+
if (!fitted)
|
|
401
|
+
return null;
|
|
402
|
+
chunks.push(...fitted);
|
|
403
|
+
}
|
|
404
|
+
if (chunks.length === 0
|
|
405
|
+
|| chunks.length > ASSET_SYNC_INVENTORY_MAX_SEGMENTS
|
|
406
|
+
|| rec.index + chunks.length > ASSET_SYNC_INVENTORY_MAX_SEGMENTS)
|
|
407
|
+
return null;
|
|
408
|
+
const records = [];
|
|
409
|
+
for (const [batchIndex, chunk] of chunks.entries()) {
|
|
410
|
+
const parsed = parseAssetSyncInventorySegmentRecord({
|
|
411
|
+
...common,
|
|
412
|
+
index: rec.index + batchIndex,
|
|
413
|
+
items: chunk.items,
|
|
414
|
+
anonymousBlocked: chunk.anonymousBlocked,
|
|
415
|
+
...(chunks.length > 1 ? { batchId, batchIndex, batchSize: chunks.length } : {}),
|
|
416
|
+
});
|
|
417
|
+
if (!parsed)
|
|
418
|
+
return null;
|
|
419
|
+
records.push(parsed);
|
|
420
|
+
}
|
|
421
|
+
return Object.freeze(records);
|
|
422
|
+
}
|
|
423
|
+
function splitInventoryUnits(items, anonymousBlocked) {
|
|
424
|
+
if (items.length === 0 && anonymousBlocked === 0)
|
|
425
|
+
return [{ items: [], anonymousBlocked: 0 }];
|
|
426
|
+
const chunks = [];
|
|
427
|
+
let itemOffset = 0;
|
|
428
|
+
let anonymousRemaining = anonymousBlocked;
|
|
429
|
+
while (itemOffset < items.length || anonymousRemaining > 0) {
|
|
430
|
+
const itemCount = Math.min(ASSET_SYNC_INVENTORY_MAX_ITEMS_PER_SEGMENT, items.length - itemOffset);
|
|
431
|
+
const anonymousCount = Math.min(ASSET_SYNC_INVENTORY_MAX_ITEMS_PER_SEGMENT - itemCount, anonymousRemaining);
|
|
432
|
+
chunks.push({
|
|
433
|
+
items: items.slice(itemOffset, itemOffset + itemCount),
|
|
434
|
+
anonymousBlocked: anonymousCount,
|
|
435
|
+
});
|
|
436
|
+
itemOffset += itemCount;
|
|
437
|
+
anonymousRemaining -= anonymousCount;
|
|
438
|
+
}
|
|
439
|
+
return chunks;
|
|
440
|
+
}
|
|
441
|
+
function splitInventoryChunkToFit(common, chunk, batchId, batchIndex, remainingSegments) {
|
|
442
|
+
if (remainingSegments < 1)
|
|
443
|
+
return null;
|
|
444
|
+
const parsed = parseAssetSyncInventorySegmentRecord({
|
|
445
|
+
...common,
|
|
446
|
+
index: common.index + batchIndex,
|
|
447
|
+
items: chunk.items,
|
|
448
|
+
anonymousBlocked: chunk.anonymousBlocked,
|
|
449
|
+
});
|
|
450
|
+
const batchProbe = parsed ? {
|
|
451
|
+
...parsed,
|
|
452
|
+
batchId,
|
|
453
|
+
batchIndex,
|
|
454
|
+
batchSize: ASSET_SYNC_INVENTORY_MAX_SEGMENTS,
|
|
455
|
+
} : null;
|
|
456
|
+
if (batchProbe
|
|
457
|
+
&& Buffer.byteLength(JSON.stringify(batchProbe), 'utf8') <= ASSET_SYNC_INVENTORY_MAX_SEGMENT_BYTES)
|
|
458
|
+
return [chunk];
|
|
459
|
+
const units = chunk.items.length + chunk.anonymousBlocked;
|
|
460
|
+
if (units <= 1)
|
|
461
|
+
return null;
|
|
462
|
+
const leftUnits = Math.floor(units / 2);
|
|
463
|
+
const leftItemCount = Math.min(leftUnits, chunk.items.length);
|
|
464
|
+
const left = {
|
|
465
|
+
items: chunk.items.slice(0, leftItemCount),
|
|
466
|
+
anonymousBlocked: leftUnits - leftItemCount,
|
|
467
|
+
};
|
|
468
|
+
const right = {
|
|
469
|
+
items: chunk.items.slice(leftItemCount),
|
|
470
|
+
anonymousBlocked: chunk.anonymousBlocked - left.anonymousBlocked,
|
|
81
471
|
};
|
|
472
|
+
const fittedLeft = splitInventoryChunkToFit(common, left, batchId, batchIndex, remainingSegments);
|
|
473
|
+
if (!fittedLeft)
|
|
474
|
+
return null;
|
|
475
|
+
const fittedRight = splitInventoryChunkToFit(common, right, batchId, batchIndex + fittedLeft.length, remainingSegments - fittedLeft.length);
|
|
476
|
+
return fittedLeft && fittedRight ? [...fittedLeft, ...fittedRight] : null;
|
|
477
|
+
}
|
|
478
|
+
function continuesPendingBatch(pending, record) {
|
|
479
|
+
const first = pending[0];
|
|
480
|
+
return Boolean(first?.batchId
|
|
481
|
+
&& record.batchId === first.batchId
|
|
482
|
+
&& record.batchSize === first.batchSize
|
|
483
|
+
&& record.batchIndex === pending.length
|
|
484
|
+
&& record.index === first.index + pending.length
|
|
485
|
+
&& record.scanId === first.scanId
|
|
486
|
+
&& record.inventoryKey === first.inventoryKey
|
|
487
|
+
&& record.scope === first.scope
|
|
488
|
+
&& record.syncedAt === first.syncedAt
|
|
489
|
+
&& record.cursorHeld === first.cursorHeld
|
|
490
|
+
&& cursorFingerprintsEqual(record.inputCursorFingerprints, first.inputCursorFingerprints)
|
|
491
|
+
&& cursorFingerprintsEqual(record.nextCursorFingerprints, first.nextCursorFingerprints));
|
|
492
|
+
}
|
|
493
|
+
function canAppendInventoryBatch(current, records) {
|
|
494
|
+
const first = records[0];
|
|
495
|
+
if (!first)
|
|
496
|
+
return false;
|
|
497
|
+
if (records.length === 1)
|
|
498
|
+
return !first.batchId && canAppendInventorySegment(current, first);
|
|
499
|
+
if (first.batchIndex !== 0
|
|
500
|
+
|| first.batchSize !== records.length
|
|
501
|
+
|| !records.every((record, index) => index === 0 || continuesPendingBatch(records.slice(0, index), record))
|
|
502
|
+
|| (first.cursorHeld === true && !inventoryBatchHasRetryableOutcome(records)))
|
|
503
|
+
return false;
|
|
504
|
+
if (first.index === 0) {
|
|
505
|
+
if (!cursorFingerprintsEqual(first.inputCursorFingerprints, { purchased: null, published: null }))
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
else if (!current
|
|
509
|
+
|| current.complete
|
|
510
|
+
|| current.retryIndex !== undefined
|
|
511
|
+
|| current.scanId !== first.scanId
|
|
512
|
+
|| current.inventoryKey !== first.inventoryKey
|
|
513
|
+
|| current.scope !== first.scope
|
|
514
|
+
|| current.segmentCount !== first.index
|
|
515
|
+
|| !cursorFingerprintsEqual(current.nextCursorFingerprints, first.inputCursorFingerprints)) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
if ((current?.segmentCount ?? 0) + records.length > ASSET_SYNC_INVENTORY_MAX_SEGMENTS)
|
|
519
|
+
return false;
|
|
520
|
+
const outcomes = new Set(current?.outcomes.keys() ?? []);
|
|
521
|
+
let anonymousBlocked = current?.anonymousBlocked ?? 0;
|
|
522
|
+
for (const record of records) {
|
|
523
|
+
for (const item of record.items)
|
|
524
|
+
outcomes.add(item.remoteAssetId);
|
|
525
|
+
anonymousBlocked += record.anonymousBlocked;
|
|
526
|
+
}
|
|
527
|
+
return outcomes.size + anonymousBlocked <= ASSET_SYNC_INVENTORY_MAX_UNIQUE_ITEMS;
|
|
82
528
|
}
|
|
83
|
-
function
|
|
84
|
-
const
|
|
85
|
-
|
|
529
|
+
function snapshotFromInventoryBatch(current, records) {
|
|
530
|
+
const first = records[0];
|
|
531
|
+
if (!first)
|
|
532
|
+
throw new Error('inventory batch is empty');
|
|
533
|
+
const outcomes = new Map(current?.outcomes ?? []);
|
|
534
|
+
let anonymousBlocked = current?.anonymousBlocked ?? 0;
|
|
535
|
+
for (const record of records) {
|
|
536
|
+
for (const item of record.items) {
|
|
537
|
+
if (!outcomes.has(item.remoteAssetId))
|
|
538
|
+
outcomes.set(item.remoteAssetId, item.outcome);
|
|
539
|
+
}
|
|
540
|
+
anonymousBlocked += record.anonymousBlocked;
|
|
541
|
+
}
|
|
542
|
+
const retryIndex = inventoryBatchRetryIndex(first);
|
|
543
|
+
return immutableInventorySnapshot({
|
|
544
|
+
scanId: first.scanId,
|
|
545
|
+
inventoryKey: first.inventoryKey,
|
|
546
|
+
scope: first.scope,
|
|
547
|
+
segmentCount: (current?.segmentCount ?? 0) + records.length,
|
|
548
|
+
nextCursorFingerprints: first.nextCursorFingerprints,
|
|
549
|
+
outcomes,
|
|
550
|
+
anonymousBlocked,
|
|
551
|
+
complete: retryIndex === undefined && inventoryScanComplete(first.scope, first.nextCursorFingerprints),
|
|
552
|
+
...(retryIndex === undefined ? {} : { retryIndex }),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
function canAppendInventorySegment(current, record) {
|
|
556
|
+
if (record.cursorHeld === true && !inventoryBatchHasRetryableOutcome([record]))
|
|
557
|
+
return false;
|
|
558
|
+
if (record.index === 0) {
|
|
559
|
+
return cursorFingerprintsEqual(record.inputCursorFingerprints, { purchased: null, published: null });
|
|
560
|
+
}
|
|
561
|
+
if (!current
|
|
562
|
+
|| current.complete
|
|
563
|
+
|| current.retryIndex !== undefined
|
|
564
|
+
|| current.scanId !== record.scanId
|
|
565
|
+
|| current.inventoryKey !== record.inventoryKey
|
|
566
|
+
|| current.scope !== record.scope
|
|
567
|
+
|| current.segmentCount !== record.index
|
|
568
|
+
|| !cursorFingerprintsEqual(current.nextCursorFingerprints, record.inputCursorFingerprints)
|
|
569
|
+
|| current.segmentCount >= ASSET_SYNC_INVENTORY_MAX_SEGMENTS) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
const newUniqueItems = record.items.reduce((count, item) => count + (current.outcomes.has(item.remoteAssetId) ? 0 : 1), 0);
|
|
573
|
+
return current.outcomes.size + current.anonymousBlocked + newUniqueItems + record.anonymousBlocked
|
|
574
|
+
<= ASSET_SYNC_INVENTORY_MAX_UNIQUE_ITEMS;
|
|
575
|
+
}
|
|
576
|
+
function snapshotFromFirstSegment(record) {
|
|
577
|
+
if (record.items.length + record.anonymousBlocked > ASSET_SYNC_INVENTORY_MAX_UNIQUE_ITEMS)
|
|
578
|
+
return undefined;
|
|
579
|
+
if (record.cursorHeld === true && !inventoryBatchHasRetryableOutcome([record]))
|
|
580
|
+
return undefined;
|
|
581
|
+
const retryIndex = inventoryBatchRetryIndex(record);
|
|
582
|
+
return immutableInventorySnapshot({
|
|
583
|
+
scanId: record.scanId,
|
|
584
|
+
inventoryKey: record.inventoryKey,
|
|
585
|
+
scope: record.scope,
|
|
586
|
+
segmentCount: 1,
|
|
587
|
+
nextCursorFingerprints: record.nextCursorFingerprints,
|
|
588
|
+
outcomes: new Map(record.items.map((item) => [item.remoteAssetId, item.outcome])),
|
|
589
|
+
anonymousBlocked: record.anonymousBlocked,
|
|
590
|
+
complete: retryIndex === undefined && inventoryScanComplete(record.scope, record.nextCursorFingerprints),
|
|
591
|
+
...(retryIndex === undefined ? {} : { retryIndex }),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function appendSegmentToSnapshot(current, record) {
|
|
595
|
+
const outcomes = new Map(current.outcomes);
|
|
596
|
+
for (const item of record.items) {
|
|
597
|
+
if (!outcomes.has(item.remoteAssetId))
|
|
598
|
+
outcomes.set(item.remoteAssetId, item.outcome);
|
|
599
|
+
}
|
|
600
|
+
const retryIndex = inventoryBatchRetryIndex(record);
|
|
601
|
+
return immutableInventorySnapshot({
|
|
602
|
+
...current,
|
|
603
|
+
segmentCount: current.segmentCount + 1,
|
|
604
|
+
nextCursorFingerprints: record.nextCursorFingerprints,
|
|
605
|
+
outcomes,
|
|
606
|
+
anonymousBlocked: current.anonymousBlocked + record.anonymousBlocked,
|
|
607
|
+
complete: retryIndex === undefined && inventoryScanComplete(record.scope, record.nextCursorFingerprints),
|
|
608
|
+
retryIndex,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
function inventoryBatchRetryIndex(first) {
|
|
612
|
+
return first.cursorHeld === true ? first.index : undefined;
|
|
613
|
+
}
|
|
614
|
+
function inventoryBatchHasRetryableOutcome(records) {
|
|
615
|
+
return records.some((record) => record.items.some((item) => (item.outcome === 'failed' || item.outcome === 'pending')));
|
|
616
|
+
}
|
|
617
|
+
function findInventoryRetryTail(raw, snapshot) {
|
|
618
|
+
if (snapshot.retryIndex === undefined)
|
|
619
|
+
return undefined;
|
|
620
|
+
const lines = raw.split('\n');
|
|
621
|
+
let found;
|
|
622
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
623
|
+
const first = parseInventoryLine(lines[lineIndex]);
|
|
624
|
+
if (!first
|
|
625
|
+
|| first.inventoryKey !== snapshot.inventoryKey
|
|
626
|
+
|| first.scanId !== snapshot.scanId
|
|
627
|
+
|| first.index !== snapshot.retryIndex
|
|
628
|
+
|| first.batchIndex !== undefined && first.batchIndex !== 0)
|
|
629
|
+
continue;
|
|
630
|
+
const records = [first];
|
|
631
|
+
const lineIndexes = new Set([lineIndex]);
|
|
632
|
+
if (first.batchSize !== undefined) {
|
|
633
|
+
for (let offset = 1; offset < first.batchSize; offset += 1) {
|
|
634
|
+
const next = parseInventoryLine(lines[lineIndex + offset]);
|
|
635
|
+
if (!next || !continuesPendingBatch(records, next))
|
|
636
|
+
break;
|
|
637
|
+
records.push(next);
|
|
638
|
+
lineIndexes.add(lineIndex + offset);
|
|
639
|
+
}
|
|
640
|
+
if (records.length !== first.batchSize)
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (records.length !== snapshot.segmentCount - snapshot.retryIndex
|
|
644
|
+
|| inventoryBatchRetryIndex(first) !== snapshot.retryIndex)
|
|
645
|
+
continue;
|
|
646
|
+
found = { records: Object.freeze(records), lineIndexes };
|
|
647
|
+
}
|
|
648
|
+
return found;
|
|
649
|
+
}
|
|
650
|
+
function parseInventoryLine(line) {
|
|
651
|
+
if (!line?.trim())
|
|
652
|
+
return null;
|
|
653
|
+
try {
|
|
654
|
+
return parseAssetSyncInventorySegmentRecord(JSON.parse(line));
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function mergeInventoryRetryItems(previousRecords, retriedItems) {
|
|
661
|
+
const retriedById = new Map();
|
|
662
|
+
for (const item of retriedItems) {
|
|
663
|
+
if (retriedById.has(item.remoteAssetId))
|
|
664
|
+
return null;
|
|
665
|
+
retriedById.set(item.remoteAssetId, item);
|
|
666
|
+
}
|
|
667
|
+
const previousIds = new Set();
|
|
668
|
+
const merged = [];
|
|
669
|
+
for (const record of previousRecords) {
|
|
670
|
+
for (const item of record.items) {
|
|
671
|
+
previousIds.add(item.remoteAssetId);
|
|
672
|
+
const retried = retriedById.get(item.remoteAssetId);
|
|
673
|
+
merged.push(retried ? {
|
|
674
|
+
remoteAssetId: item.remoteAssetId,
|
|
675
|
+
outcome: mergeInventoryRetryOutcome(item.outcome, retried.outcome),
|
|
676
|
+
} : item);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
for (const item of retriedItems) {
|
|
680
|
+
if (!previousIds.has(item.remoteAssetId))
|
|
681
|
+
merged.push(item);
|
|
682
|
+
}
|
|
683
|
+
return merged;
|
|
684
|
+
}
|
|
685
|
+
function mergeInventoryRetryOutcome(previous, retried) {
|
|
686
|
+
if (previous !== 'failed' && previous !== 'pending')
|
|
687
|
+
return previous;
|
|
688
|
+
if (retried === 'imported' || retried === 'already_local' || retried === 'blocked')
|
|
689
|
+
return retried;
|
|
690
|
+
if (previous === 'failed' && retried === 'pending')
|
|
691
|
+
return previous;
|
|
692
|
+
return retried;
|
|
693
|
+
}
|
|
694
|
+
function removeInventoryRetryTail(raw, removedLineIndexes) {
|
|
695
|
+
return raw.split('\n')
|
|
696
|
+
.filter((line, index) => line.trim() && !removedLineIndexes.has(index))
|
|
697
|
+
.map((line) => `${line}\n`)
|
|
698
|
+
.join('');
|
|
699
|
+
}
|
|
700
|
+
function immutableInventorySnapshot(snapshot) {
|
|
701
|
+
return Object.freeze({
|
|
702
|
+
...snapshot,
|
|
703
|
+
nextCursorFingerprints: Object.freeze({ ...snapshot.nextCursorFingerprints }),
|
|
704
|
+
outcomes: new Map(snapshot.outcomes),
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
function inventoryScanComplete(scope, cursors) {
|
|
708
|
+
if (scope === 'purchased')
|
|
709
|
+
return cursors.purchased === null;
|
|
710
|
+
if (scope === 'published')
|
|
711
|
+
return cursors.published === null;
|
|
712
|
+
return cursors.purchased === null && cursors.published === null;
|
|
713
|
+
}
|
|
714
|
+
function cursorFingerprintsEqual(left, right) {
|
|
715
|
+
return left.purchased === right.purchased && left.published === right.published;
|
|
716
|
+
}
|
|
717
|
+
function removeInventoryScan(raw, inventoryKey) {
|
|
718
|
+
return raw.split('\n').filter((line) => {
|
|
719
|
+
if (!line.trim())
|
|
720
|
+
return false;
|
|
721
|
+
try {
|
|
722
|
+
const record = JSON.parse(line);
|
|
723
|
+
return record['recordType'] !== 'inventory_scan' || record['inventoryKey'] !== inventoryKey;
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}).map((line) => `${line}\n`).join('');
|
|
729
|
+
}
|
|
730
|
+
function inventoryScanBytes(raw) {
|
|
731
|
+
let bytes = 0;
|
|
732
|
+
for (const line of raw.split('\n')) {
|
|
733
|
+
if (!line.trim())
|
|
734
|
+
continue;
|
|
735
|
+
try {
|
|
736
|
+
const record = JSON.parse(line);
|
|
737
|
+
if (record['recordType'] === 'inventory_scan')
|
|
738
|
+
bytes += Buffer.byteLength(`${line}\n`, 'utf8');
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
// Unknown corrupt rows are preserved but cannot grow through this API.
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return bytes;
|
|
86
745
|
}
|