agl 40.6.0 → 41.0.0
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 +33 -0
- package/dist_serve/bundle.js +3514 -3357
- package/dist_serve/bundle.js.map +2 -2
- package/dist_serve/bundle.js.third-party-notices.json +58 -32
- package/dist_serve/bundle.js.third-party-notices.txt +108 -33
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +64 -37
- package/dist_ts/classes.controller.d.ts +8 -0
- package/dist_ts/classes.controller.js +109 -21
- package/dist_ts/classes.controllereventdelivery.d.ts +72 -0
- package/dist_ts/classes.controllereventdelivery.js +233 -0
- package/dist_ts/classes.flexmodels.public.js +4 -1
- package/dist_ts/classes.flexprojectionreader.d.ts +6 -0
- package/dist_ts/classes.flexprojectionreader.js +29 -5
- package/dist_ts/classes.flexservice.d.ts +2 -1
- package/dist_ts/classes.flexservice.js +34 -6
- package/dist_ts/classes.flexstore.d.ts +41 -4
- package/dist_ts/classes.flexstore.js +266 -203
- package/dist_ts/classes.ticketnotifier.js +4 -2
- package/dist_ts/classes.ticketstore.d.ts +50 -2
- package/dist_ts/classes.ticketstore.js +195 -78
- package/dist_ts/driver/classes.drivereventrelay.d.ts +13 -1
- package/dist_ts/driver/classes.drivereventrelay.js +99 -6
- package/dist_ts/driver/functions.driverinstructions.d.ts +1 -1
- package/dist_ts/driver/functions.driverinstructions.js +1 -1
- package/dist_ts/driver/functions.drivertools.d.ts +11 -2
- package/dist_ts/driver/functions.drivertools.js +19 -9
- package/dist_ts/fleet/classes.boardpump.d.ts +8 -2
- package/dist_ts/fleet/classes.boardpump.js +38 -20
- package/dist_ts/fleet/classes.ticketeffects.d.ts +17 -2
- package/dist_ts/fleet/classes.ticketeffects.js +16 -10
- package/dist_ts/fleet/classes.workers.d.ts +4 -0
- package/dist_ts/fleet/classes.workers.js +37 -17
- package/dist_ts/fleet/functions.workervalidation.js +2 -2
- package/dist_ts/functions.flexprivatesnapshot.d.ts +102 -0
- package/dist_ts/functions.flexprivatesnapshot.js +461 -0
- package/dist_ts/functions.mcptickettools.d.ts +3 -0
- package/dist_ts/functions.mcptickettools.js +2 -2
- package/dist_ts/functions.ticketcli.d.ts +1 -1
- package/dist_ts/functions.ticketcli.js +8 -4
- package/dist_ts/functions.ticketvalidation.d.ts +3 -0
- package/dist_ts/functions.ticketvalidation.js +7 -4
- package/dist_ts/interfaces.flexipc.d.ts +21 -2
- package/dist_ts/interfaces.flexipc.js +33 -16
- package/dist_ts_interfaces/fleetworkers.d.ts +6 -0
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/requests.tickets.d.ts +8 -12
- package/dist_ts_interfaces/tickets.d.ts +37 -9
- package/dist_ts_interfaces/tickets.js +7 -1
- package/package.json +7 -7
- package/readme.md +54 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +63 -36
- package/ts/classes.controller.ts +131 -31
- package/ts/classes.controllereventdelivery.ts +289 -0
- package/ts/classes.flexmodels.public.ts +10 -0
- package/ts/classes.flexprojectionreader.ts +30 -3
- package/ts/classes.flexservice.ts +33 -5
- package/ts/classes.flexstore.ts +381 -246
- package/ts/classes.ticketnotifier.ts +3 -1
- package/ts/classes.ticketstore.ts +201 -75
- package/ts/driver/classes.drivereventrelay.ts +89 -4
- package/ts/driver/functions.driverinstructions.ts +1 -1
- package/ts/driver/functions.drivertools.ts +22 -8
- package/ts/fleet/classes.boardpump.ts +51 -19
- package/ts/fleet/classes.ticketeffects.ts +25 -11
- package/ts/fleet/classes.workers.ts +28 -16
- package/ts/fleet/functions.workervalidation.ts +1 -1
- package/ts/functions.flexprivatesnapshot.ts +601 -0
- package/ts/functions.mcptickettools.ts +1 -1
- package/ts/functions.ticketcli.ts +7 -3
- package/ts/functions.ticketvalidation.ts +7 -3
- package/ts/interfaces.flexipc.ts +50 -21
- package/ts_interfaces/fleetworkers.ts +6 -0
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/requests.tickets.ts +9 -6
- package/ts_interfaces/tickets.ts +32 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements.harnesscontrollerapp.ts +19 -8
- package/ts_web/elements.ticketboard.ts +16 -4
- package/ts_web/settings.general.ts +10 -4
package/ts/classes.flexstore.ts
CHANGED
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
assertFlexWriterLeaseDocument,
|
|
24
24
|
flexDomainHeadId,
|
|
25
25
|
flexMigrationId,
|
|
26
|
-
flexPrivateChunkBytes,
|
|
27
26
|
flexWriterLeaseId,
|
|
28
27
|
initFlexPrivateModels,
|
|
29
28
|
registerFlexPrivateModels,
|
|
@@ -67,6 +66,14 @@ import {
|
|
|
67
66
|
initFlexPublicModels,
|
|
68
67
|
registerFlexPublicModels,
|
|
69
68
|
} from './classes.flexmodels.public.js';
|
|
69
|
+
import {
|
|
70
|
+
buildFlexPrivateSnapshot,
|
|
71
|
+
discardFlexPrivateChunks,
|
|
72
|
+
holdFlexPrivateLayout,
|
|
73
|
+
privateCandidateDocumentId,
|
|
74
|
+
releaseFlexPrivateLayout,
|
|
75
|
+
type IFlexPrivateLayout,
|
|
76
|
+
} from './functions.flexprivatesnapshot.js';
|
|
70
77
|
import { isFlexWriterId } from './interfaces.flexipc.js';
|
|
71
78
|
|
|
72
79
|
type TFlexScopeSnapshot = plugins.flexharness.IFlexScopeSnapshot;
|
|
@@ -285,7 +292,25 @@ interface IFlexCachedSnapshot {
|
|
|
285
292
|
storageKey: string;
|
|
286
293
|
candidateId: string;
|
|
287
294
|
snapshot: unknown;
|
|
295
|
+
/** The candidate's serialized bytes. */
|
|
288
296
|
bytes: number;
|
|
297
|
+
/**
|
|
298
|
+
* How this writer stored the candidate, chunk by chunk with the chunks' bytes: the next save
|
|
299
|
+
* takes every chunk its change leaves in place. Absent for a snapshot read back.
|
|
300
|
+
*/
|
|
301
|
+
layout?: IFlexPrivateLayout;
|
|
302
|
+
/** What the entry counts against `flexSnapshotCacheBytes`. */
|
|
303
|
+
cost: number;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** A domain's committed snapshot a save took out of the cache or read, for the save alone. */
|
|
307
|
+
interface IFlexDomainCheckout<TSnapshot> {
|
|
308
|
+
head: TStoredDomainHead;
|
|
309
|
+
/** Undefined when the save did not need the snapshot and none was cached. */
|
|
310
|
+
snapshot?: TSnapshot;
|
|
311
|
+
layout?: IFlexPrivateLayout;
|
|
312
|
+
/** Ends the save's hold on the layout; the cache holds what the save committed. */
|
|
313
|
+
release(): void;
|
|
289
314
|
}
|
|
290
315
|
|
|
291
316
|
interface IFlexCachedTranscript {
|
|
@@ -293,8 +318,19 @@ interface IFlexCachedTranscript {
|
|
|
293
318
|
bytes: number;
|
|
294
319
|
}
|
|
295
320
|
|
|
296
|
-
/**
|
|
297
|
-
|
|
321
|
+
/**
|
|
322
|
+
* What the snapshot cache may hold in total. An entry counts its kept chunk bytes once and its
|
|
323
|
+
* parsed snapshot at `flexSnapshotHeapFactor` times the snapshot's serialized bytes, so the cache
|
|
324
|
+
* holds at most about 170 MiB of serialized snapshots stored by this writer (3 times their size
|
|
325
|
+
* counted). A snapshot that does not fit even alone is not cached: its saves take the full path.
|
|
326
|
+
*/
|
|
327
|
+
const flexSnapshotCacheBytes = 512 * 1024 * 1024;
|
|
328
|
+
/**
|
|
329
|
+
* The V8 heap a parsed snapshot takes per byte of its JSON, measured on realistic projections
|
|
330
|
+
* and Agent event logs (1.79 and 1.83 on Node 25), rounded up.
|
|
331
|
+
*/
|
|
332
|
+
const flexSnapshotHeapFactor = 2;
|
|
333
|
+
/** What the transcript cache may hold in total. */
|
|
298
334
|
const flexTranscriptCacheBytes = 16 * 1024 * 1024;
|
|
299
335
|
|
|
300
336
|
interface IFlexMaintenanceCandidate {
|
|
@@ -354,28 +390,6 @@ const randomCandidateId = (): string => plugins.crypto.randomBytes(16).toString(
|
|
|
354
390
|
|
|
355
391
|
const randomOwnerToken = (): string => plugins.crypto.randomBytes(32).toString('base64url');
|
|
356
392
|
|
|
357
|
-
const privateCandidateDocumentId = (candidateIdArg: string): string =>
|
|
358
|
-
`flex-private-candidate:${candidateIdArg}`;
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Private chunks are addressed by their content within one domain incarnation, so a save stores
|
|
362
|
-
* only the chunks it changed and shares the others with the snapshot it replaces.
|
|
363
|
-
*/
|
|
364
|
-
const privateChunkDocumentId = (
|
|
365
|
-
controllerIdArg: string,
|
|
366
|
-
storageKeyArg: string,
|
|
367
|
-
sourceArg: IFlexPrivateDomainSource,
|
|
368
|
-
chunkDigestArg: string,
|
|
369
|
-
): string => `flex-private-chunk:${sha256(JSON.stringify([
|
|
370
|
-
controllerIdArg,
|
|
371
|
-
storageKeyArg,
|
|
372
|
-
sourceArg.identity.domain,
|
|
373
|
-
sourceArg.identity.sessionId ?? null,
|
|
374
|
-
sourceArg.identity.archiveId ?? null,
|
|
375
|
-
sourceArg.incarnationId,
|
|
376
|
-
chunkDigestArg,
|
|
377
|
-
]))}`;
|
|
378
|
-
|
|
379
393
|
const publicCandidateDocumentId = (candidateIdArg: string): string =>
|
|
380
394
|
`flex-public-candidate:${candidateIdArg}`;
|
|
381
395
|
|
|
@@ -920,136 +934,6 @@ const inBatches = async <TValue>(
|
|
|
920
934
|
}
|
|
921
935
|
};
|
|
922
936
|
|
|
923
|
-
/** The lists of a domain snapshot that grow entry by entry, stored apart from the rest. */
|
|
924
|
-
const privateSnapshotLists: Partial<Record<TFlexDomainKind, readonly string[]>> = {
|
|
925
|
-
projection: ['messages', 'reversionSegments'],
|
|
926
|
-
};
|
|
927
|
-
|
|
928
|
-
/** A chunk of a growing list stays under this size, so a change rewrites little of the list. */
|
|
929
|
-
const privateListChunkBytes = 64 * 1024;
|
|
930
|
-
|
|
931
|
-
/**
|
|
932
|
-
* The JSON of a snapshot as runs of pieces. A chunk never spans two runs, and inside a run of
|
|
933
|
-
* list entries chunks are filled from the list's start, so entries before a change keep their
|
|
934
|
-
* chunks and a save that changes a list's newest entries rewrites only that list's last chunk
|
|
935
|
-
* besides the small runs between the lists.
|
|
936
|
-
*/
|
|
937
|
-
const privateSnapshotRuns = (snapshotArg: unknown, listKeysArg: readonly string[]): string[][] => {
|
|
938
|
-
if (
|
|
939
|
-
listKeysArg.length === 0
|
|
940
|
-
|| snapshotArg === null
|
|
941
|
-
|| typeof snapshotArg !== 'object'
|
|
942
|
-
|| Array.isArray(snapshotArg)
|
|
943
|
-
) return [[JSON.stringify(snapshotArg)]];
|
|
944
|
-
const runs: string[][] = [];
|
|
945
|
-
let frame = '{';
|
|
946
|
-
let first = true;
|
|
947
|
-
for (const [key, value] of Object.entries(snapshotArg)) {
|
|
948
|
-
if (value === undefined) continue;
|
|
949
|
-
frame += `${first ? '' : ','}${JSON.stringify(key)}:`;
|
|
950
|
-
first = false;
|
|
951
|
-
if (listKeysArg.includes(key) && Array.isArray(value) && value.length > 0) {
|
|
952
|
-
runs.push([`${frame}[`]);
|
|
953
|
-
runs.push(value.map((entry, index) => `${index === 0 ? '' : ','}${JSON.stringify(entry)}`));
|
|
954
|
-
frame = ']';
|
|
955
|
-
} else {
|
|
956
|
-
frame += JSON.stringify(value);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
runs.push([`${frame}}`]);
|
|
960
|
-
return runs;
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
/** Cuts the runs into chunks of at most `flexPrivateChunkBytes`, never across two runs. */
|
|
964
|
-
const privateSnapshotChunks = (runsArg: string[][]): Buffer[] => {
|
|
965
|
-
const chunks: Buffer[] = [];
|
|
966
|
-
for (const run of runsArg) {
|
|
967
|
-
let pending: Buffer[] = [];
|
|
968
|
-
let pendingBytes = 0;
|
|
969
|
-
const flush = (): void => {
|
|
970
|
-
if (pending.length === 0) return;
|
|
971
|
-
chunks.push(Buffer.concat(pending, pendingBytes));
|
|
972
|
-
pending = [];
|
|
973
|
-
pendingBytes = 0;
|
|
974
|
-
};
|
|
975
|
-
for (const piece of run) {
|
|
976
|
-
const bytes = Buffer.from(piece, 'utf8');
|
|
977
|
-
if (pendingBytes > 0 && pendingBytes + bytes.byteLength > privateListChunkBytes) flush();
|
|
978
|
-
if (bytes.byteLength <= flexPrivateChunkBytes - pendingBytes) {
|
|
979
|
-
pending.push(bytes);
|
|
980
|
-
pendingBytes += bytes.byteLength;
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
|
-
flush();
|
|
984
|
-
for (let offset = 0; offset < bytes.byteLength; offset += flexPrivateChunkBytes) {
|
|
985
|
-
chunks.push(bytes.subarray(offset, Math.min(offset + flexPrivateChunkBytes, bytes.byteLength)));
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
flush();
|
|
989
|
-
}
|
|
990
|
-
return chunks.length === 0 ? [Buffer.alloc(0)] : chunks;
|
|
991
|
-
};
|
|
992
|
-
|
|
993
|
-
const createPrivateProjection = (
|
|
994
|
-
controllerIdArg: string,
|
|
995
|
-
storageKeyArg: string,
|
|
996
|
-
candidateIdArg: string,
|
|
997
|
-
revisionArg: number,
|
|
998
|
-
snapshotArg: unknown,
|
|
999
|
-
createdAtArg: string,
|
|
1000
|
-
sourceArg: IFlexPrivateDomainSource,
|
|
1001
|
-
): { candidate: IFlexPrivateCandidateDocument; chunks: IFlexPrivateChunkDocument[] } => {
|
|
1002
|
-
const identity = {
|
|
1003
|
-
domain: sourceArg.identity.domain,
|
|
1004
|
-
incarnationId: sourceArg.incarnationId,
|
|
1005
|
-
...(sourceArg.identity.sessionId !== undefined
|
|
1006
|
-
? { sessionId: sourceArg.identity.sessionId }
|
|
1007
|
-
: {}),
|
|
1008
|
-
...(sourceArg.identity.archiveId !== undefined
|
|
1009
|
-
? { archiveId: sourceArg.identity.archiveId }
|
|
1010
|
-
: {}),
|
|
1011
|
-
};
|
|
1012
|
-
const buffers = privateSnapshotChunks(privateSnapshotRuns(
|
|
1013
|
-
snapshotArg,
|
|
1014
|
-
privateSnapshotLists[sourceArg.identity.domain] ?? [],
|
|
1015
|
-
));
|
|
1016
|
-
const chunks: IFlexPrivateChunkDocument[] = buffers.map((chunk, index) => {
|
|
1017
|
-
const digest = sha256(chunk);
|
|
1018
|
-
return {
|
|
1019
|
-
id: privateChunkDocumentId(controllerIdArg, storageKeyArg, sourceArg, digest),
|
|
1020
|
-
controllerId: controllerIdArg,
|
|
1021
|
-
scopeId: storageKeyArg,
|
|
1022
|
-
candidateId: candidateIdArg,
|
|
1023
|
-
revision: revisionArg,
|
|
1024
|
-
index,
|
|
1025
|
-
sha256: digest,
|
|
1026
|
-
bytes: chunk.byteLength,
|
|
1027
|
-
data: chunk.toString('base64'),
|
|
1028
|
-
createdAt: createdAtArg,
|
|
1029
|
-
...identity,
|
|
1030
|
-
};
|
|
1031
|
-
});
|
|
1032
|
-
const complete = Buffer.concat(buffers);
|
|
1033
|
-
const candidate: IFlexPrivateCandidateDocument = {
|
|
1034
|
-
id: privateCandidateDocumentId(candidateIdArg),
|
|
1035
|
-
controllerId: controllerIdArg,
|
|
1036
|
-
scopeId: storageKeyArg,
|
|
1037
|
-
candidateId: candidateIdArg,
|
|
1038
|
-
revision: revisionArg,
|
|
1039
|
-
sha256: sha256(complete),
|
|
1040
|
-
bytes: complete.byteLength,
|
|
1041
|
-
chunks: chunks.map(({ id, sha256: digest, bytes }, index) => ({
|
|
1042
|
-
id, index, sha256: digest, bytes,
|
|
1043
|
-
})),
|
|
1044
|
-
createdAt: createdAtArg,
|
|
1045
|
-
...identity,
|
|
1046
|
-
};
|
|
1047
|
-
complete.fill(0);
|
|
1048
|
-
assertFlexPrivateCandidateDocument(candidate);
|
|
1049
|
-
for (const chunk of chunks) assertFlexPrivateChunkDocument(chunk);
|
|
1050
|
-
return { candidate, chunks };
|
|
1051
|
-
};
|
|
1052
|
-
|
|
1053
937
|
export interface IFlexTranscriptLimits {
|
|
1054
938
|
messageLimit: number;
|
|
1055
939
|
messageBytesLimit: number;
|
|
@@ -1069,11 +953,39 @@ const defaultTranscriptLimits: IFlexTranscriptLimits = {
|
|
|
1069
953
|
singleMessageBytesLimit: flexPublicSingleMessageBytesLimit,
|
|
1070
954
|
};
|
|
1071
955
|
|
|
956
|
+
/**
|
|
957
|
+
* What a transcript build asks of each message: the id of its record, the size of its JSON and
|
|
958
|
+
* its stand-in when it is oversized. A store answers from what it cached per stored message.
|
|
959
|
+
*/
|
|
960
|
+
export interface IFlexTranscriptMessageRecords {
|
|
961
|
+
/** The id of the public record of `messageArg` at `messageIndexArg`. */
|
|
962
|
+
recordId(messageIndexArg: number, messageArg: TFlexMessage): string;
|
|
963
|
+
/** The UTF-8 bytes of `messageArg`'s JSON. */
|
|
964
|
+
messageBytes(messageArg: TFlexMessage): number;
|
|
965
|
+
/** The public stand-in of an oversized message, within `budgetArg` serialized bytes. */
|
|
966
|
+
placeholder(messageArg: TFlexMessage, budgetArg: number): TFlexMessage;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const computedMessageRecords = (
|
|
970
|
+
controllerIdArg: string,
|
|
971
|
+
storageKeyArg: string,
|
|
972
|
+
): IFlexTranscriptMessageRecords => ({
|
|
973
|
+
recordId: (messageIndexArg, messageArg) => publicMessageDocumentId(
|
|
974
|
+
controllerIdArg,
|
|
975
|
+
storageKeyArg,
|
|
976
|
+
messageIndexArg,
|
|
977
|
+
messageArg,
|
|
978
|
+
),
|
|
979
|
+
messageBytes: (messageArg) => serializedBytes(messageArg).byteLength,
|
|
980
|
+
placeholder: placeholderMessage,
|
|
981
|
+
});
|
|
982
|
+
|
|
1072
983
|
/**
|
|
1073
984
|
* The public transcript of one session: its newest messages within the public bounds, a message
|
|
1074
985
|
* over the single-message bound reduced to a placeholder that keeps its metadata. Records are
|
|
1075
986
|
* addressed by their content, and `storedRecordsArg` names records already stored, so the
|
|
1076
|
-
* transcript lists the stored copy of every record it shares
|
|
987
|
+
* transcript lists the stored copy of every record it shares; only the records it adds are
|
|
988
|
+
* serialized, hashed and checked.
|
|
1077
989
|
*/
|
|
1078
990
|
export const createProjectionTranscript = (
|
|
1079
991
|
controllerIdArg: string,
|
|
@@ -1083,12 +995,13 @@ export const createProjectionTranscript = (
|
|
|
1083
995
|
preparationArg: IFlexCandidatePreparation,
|
|
1084
996
|
storedRecordsArg: ReadonlyMap<string, IFlexRecordManifest> = new Map(),
|
|
1085
997
|
limitsArg: IFlexTranscriptLimits = defaultTranscriptLimits,
|
|
998
|
+
recordsArg: IFlexTranscriptMessageRecords = computedMessageRecords(controllerIdArg, storageKeyArg),
|
|
1086
999
|
): IFlexTranscriptBuild => {
|
|
1087
1000
|
const createRecord = (
|
|
1088
1001
|
messageIndexArg: number,
|
|
1089
1002
|
messageArg: TFlexMessage,
|
|
1090
1003
|
): IFlexPublicMessageRecordDocument => ({
|
|
1091
|
-
id:
|
|
1004
|
+
id: recordsArg.recordId(messageIndexArg, messageArg),
|
|
1092
1005
|
controllerId: controllerIdArg,
|
|
1093
1006
|
scopeId: storageKeyArg,
|
|
1094
1007
|
candidateId: preparationArg.candidateId,
|
|
@@ -1097,6 +1010,11 @@ export const createProjectionTranscript = (
|
|
|
1097
1010
|
message: messageArg,
|
|
1098
1011
|
createdAt: preparationArg.createdAt,
|
|
1099
1012
|
});
|
|
1013
|
+
// A record's JSON is its metadata's with the message's in place, so an oversized message is
|
|
1014
|
+
// never serialized whole to learn that it does not fit.
|
|
1015
|
+
const recordBytes = (recordArg: IFlexPublicMessageRecordDocument): number =>
|
|
1016
|
+
serializedBytes({ ...recordArg, message: 0 }).byteLength - 1
|
|
1017
|
+
+ recordsArg.messageBytes(recordArg.message);
|
|
1100
1018
|
const newest: Array<{ record: IFlexPublicMessageRecordDocument; manifest: IFlexRecordManifest }> = [];
|
|
1101
1019
|
let selectedBytes = 0;
|
|
1102
1020
|
let replaced = false;
|
|
@@ -1104,11 +1022,12 @@ export const createProjectionTranscript = (
|
|
|
1104
1022
|
for (let messageIndex = messagesArg.length - 1; messageIndex >= oldestRetained; messageIndex--) {
|
|
1105
1023
|
const message = messagesArg[messageIndex]!;
|
|
1106
1024
|
let record = createRecord(messageIndex, message);
|
|
1107
|
-
let manifest = storedRecordsArg.get(record.id)
|
|
1108
|
-
|
|
1025
|
+
let manifest = storedRecordsArg.get(record.id)
|
|
1026
|
+
?? (recordBytes(record) > limitsArg.singleMessageBytesLimit ? undefined : recordManifest(record));
|
|
1027
|
+
if (manifest === undefined || manifest.bytes > limitsArg.singleMessageBytesLimit) {
|
|
1109
1028
|
record = createRecord(
|
|
1110
1029
|
messageIndex,
|
|
1111
|
-
|
|
1030
|
+
recordsArg.placeholder(message, Math.floor(limitsArg.singleMessageBytesLimit * 0.8)),
|
|
1112
1031
|
);
|
|
1113
1032
|
manifest = storedRecordsArg.get(record.id) ?? recordManifest(record);
|
|
1114
1033
|
replaced = true;
|
|
@@ -1145,7 +1064,10 @@ export const createProjectionTranscript = (
|
|
|
1145
1064
|
...unaddressed,
|
|
1146
1065
|
};
|
|
1147
1066
|
assertFlexPublicTranscriptDocument(transcript);
|
|
1148
|
-
|
|
1067
|
+
// A stored record was checked when it was written; only the records this build adds are new.
|
|
1068
|
+
for (const { record } of selected) {
|
|
1069
|
+
if (!storedRecordsArg.has(record.id)) assertFlexPublicMessageRecordDocument(record);
|
|
1070
|
+
}
|
|
1149
1071
|
return {
|
|
1150
1072
|
transcript,
|
|
1151
1073
|
manifest: {
|
|
@@ -1318,10 +1240,27 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
1318
1240
|
/**
|
|
1319
1241
|
* The snapshot of each domain head this writer last committed or loaded, keyed by head id and
|
|
1320
1242
|
* valid while the head names the same private candidate: a save applies its change to it
|
|
1321
|
-
* instead of reading the snapshot back
|
|
1243
|
+
* instead of reading the snapshot back, and reuses the chunks of a layout it committed. Only
|
|
1244
|
+
* heads of storage keys this writer leases enter it; least recently used entries leave first.
|
|
1322
1245
|
*/
|
|
1323
1246
|
private readonly snapshotCache = new Map<string, IFlexCachedSnapshot>();
|
|
1324
1247
|
private snapshotCacheBytes = 0;
|
|
1248
|
+
/** Heads whose snapshot exceeds the whole snapshot cache, logged once each. */
|
|
1249
|
+
private readonly uncachedSnapshotHeads = new Set<string>();
|
|
1250
|
+
/*
|
|
1251
|
+
* What the public transcript needs of each stored message object, computed once: a stored
|
|
1252
|
+
* message is never edited (a change replaces it), so its record id at an index of a scope, its
|
|
1253
|
+
* JSON size and its oversized stand-in stay valid while the object lives.
|
|
1254
|
+
*/
|
|
1255
|
+
private readonly publicMessageRecordIds = new WeakMap<
|
|
1256
|
+
TFlexMessage,
|
|
1257
|
+
{ storageKey: string; messageIndex: number; recordId: string }
|
|
1258
|
+
>();
|
|
1259
|
+
private readonly publicMessageBytes = new WeakMap<TFlexMessage, number>();
|
|
1260
|
+
private readonly publicMessagePlaceholders = new WeakMap<
|
|
1261
|
+
TFlexMessage,
|
|
1262
|
+
{ budget: number; placeholder: TFlexMessage }
|
|
1263
|
+
>();
|
|
1325
1264
|
/** Transcripts by id; they are addressed by their content, so a cached one never goes stale. */
|
|
1326
1265
|
private readonly transcriptCache = new Map<string, IFlexCachedTranscript>();
|
|
1327
1266
|
private transcriptCacheBytes = 0;
|
|
@@ -1958,48 +1897,59 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
1958
1897
|
expectedRevisionArg,
|
|
1959
1898
|
actualRevisionArg,
|
|
1960
1899
|
);
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1900
|
+
// A change that keeps stored entries applies to the committed snapshot; one that sends
|
|
1901
|
+
// whole lists needs none read.
|
|
1902
|
+
const keepsEntries = change.messages.end > 0 || change.reversionSegments.end > 0;
|
|
1903
|
+
const checkout = await this.checkoutDomain<TFlexProjectionSnapshot>(
|
|
1904
|
+
identity,
|
|
1905
|
+
(value) => this.validateProjectionSnapshot(value, sessionIdArg),
|
|
1906
|
+
keepsEntries,
|
|
1907
|
+
);
|
|
1908
|
+
let transcriptChanged = true;
|
|
1909
|
+
try {
|
|
1910
|
+
if (keepsEntries) {
|
|
1911
|
+
const actualRevision = checkout?.head.revision ?? 0;
|
|
1912
|
+
if (actualRevision !== expectedRevisionArg) throw conflict(actualRevision);
|
|
1913
|
+
}
|
|
1914
|
+
// The committed snapshot was validated when it was loaded or saved and is never edited, and
|
|
1915
|
+
// the change is this store's own copy: FlexHarness validates what the change carries and
|
|
1916
|
+
// reads the kept entries for their identities only, never copying the snapshot again.
|
|
1917
|
+
const snapshot = plugins.flexharness.createChangedProjectionSnapshot(
|
|
1918
|
+
keepsEntries ? checkout?.snapshot : undefined,
|
|
1919
|
+
change,
|
|
1920
|
+
sessionIdArg,
|
|
1921
|
+
);
|
|
1922
|
+
await this.saveDomain(
|
|
1923
|
+
lease,
|
|
1964
1924
|
identity,
|
|
1965
|
-
|
|
1925
|
+
snapshot,
|
|
1926
|
+
expectedRevisionArg,
|
|
1927
|
+
conflict,
|
|
1928
|
+
undefined,
|
|
1929
|
+
undefined,
|
|
1930
|
+
async (preparation, current) => {
|
|
1931
|
+
const previous = current?.transcript
|
|
1932
|
+
? await this.loadTranscript(storageKeyArg, current.transcript.id)
|
|
1933
|
+
: undefined;
|
|
1934
|
+
const build = await this.buildTranscript(
|
|
1935
|
+
storageKeyArg,
|
|
1936
|
+
sessionIdArg,
|
|
1937
|
+
snapshot.messages,
|
|
1938
|
+
preparation,
|
|
1939
|
+
previous,
|
|
1940
|
+
);
|
|
1941
|
+
if (current?.transcript?.id === build.transcript.id) {
|
|
1942
|
+
transcriptChanged = false;
|
|
1943
|
+
return current.transcript;
|
|
1944
|
+
}
|
|
1945
|
+
await this.writeTranscript(build.transcript, build.added);
|
|
1946
|
+
return build.manifest;
|
|
1947
|
+
},
|
|
1948
|
+
checkout,
|
|
1966
1949
|
);
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
stored = current?.snapshot;
|
|
1950
|
+
} finally {
|
|
1951
|
+
checkout?.release();
|
|
1970
1952
|
}
|
|
1971
|
-
const snapshot = this.validateProjectionSnapshot(
|
|
1972
|
-
plugins.flexharness.applyFlexProjectionChange(stored, change),
|
|
1973
|
-
sessionIdArg,
|
|
1974
|
-
);
|
|
1975
|
-
let transcriptChanged = true;
|
|
1976
|
-
await this.saveDomain(
|
|
1977
|
-
lease,
|
|
1978
|
-
identity,
|
|
1979
|
-
snapshot,
|
|
1980
|
-
expectedRevisionArg,
|
|
1981
|
-
conflict,
|
|
1982
|
-
undefined,
|
|
1983
|
-
undefined,
|
|
1984
|
-
async (preparation, current) => {
|
|
1985
|
-
const previous = current?.transcript
|
|
1986
|
-
? await this.loadTranscript(storageKeyArg, current.transcript.id)
|
|
1987
|
-
: undefined;
|
|
1988
|
-
const build = await this.buildTranscript(
|
|
1989
|
-
storageKeyArg,
|
|
1990
|
-
sessionIdArg,
|
|
1991
|
-
snapshot.messages,
|
|
1992
|
-
preparation,
|
|
1993
|
-
previous,
|
|
1994
|
-
);
|
|
1995
|
-
if (current?.transcript?.id === build.transcript.id) {
|
|
1996
|
-
transcriptChanged = false;
|
|
1997
|
-
return current.transcript;
|
|
1998
|
-
}
|
|
1999
|
-
await this.writeTranscript(build.transcript, build.added);
|
|
2000
|
-
return build.manifest;
|
|
2001
|
-
},
|
|
2002
|
-
);
|
|
2003
1953
|
if (!transcriptChanged) return;
|
|
2004
1954
|
if (!publishPublicProjectionArg) {
|
|
2005
1955
|
// The publication no longer names this session's transcript; the next one reads every head.
|
|
@@ -2135,12 +2085,14 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2135
2085
|
sessionIdArg: string,
|
|
2136
2086
|
): Promise<TAgentEventSnapshot | undefined> {
|
|
2137
2087
|
this.assertBoundSession(boundSessionIdArg, sessionIdArg);
|
|
2138
|
-
return this.runStorage(storageKeyArg, async () =>
|
|
2139
|
-
await this.loadDomainDirect<TAgentEventSnapshot>(
|
|
2088
|
+
return this.runStorage(storageKeyArg, async () => {
|
|
2089
|
+
const loaded = await this.loadDomainDirect<TAgentEventSnapshot>(
|
|
2140
2090
|
{ storageKey: storageKeyArg, domain: 'agentEvents', sessionId: sessionIdArg },
|
|
2141
2091
|
(value) => this.validateAgentEventSnapshot(value, sessionIdArg),
|
|
2142
|
-
)
|
|
2143
|
-
|
|
2092
|
+
);
|
|
2093
|
+
// The store keeps its own copy for the next save.
|
|
2094
|
+
return loaded === undefined ? undefined : structuredClone(loaded.snapshot);
|
|
2095
|
+
});
|
|
2144
2096
|
}
|
|
2145
2097
|
|
|
2146
2098
|
private async saveAgentEventSnapshot(
|
|
@@ -2169,25 +2121,45 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2169
2121
|
expectedRevisionArg,
|
|
2170
2122
|
actualRevisionArg,
|
|
2171
2123
|
);
|
|
2172
|
-
const
|
|
2124
|
+
const checkout = await this.checkoutDomain<TAgentEventSnapshot>(
|
|
2173
2125
|
identity,
|
|
2174
2126
|
(value) => this.validateAgentEventSnapshot(value, sessionIdArg),
|
|
2127
|
+
true,
|
|
2175
2128
|
);
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2129
|
+
try {
|
|
2130
|
+
const actualRevision = checkout?.head.revision ?? 0;
|
|
2131
|
+
if (actualRevision !== expectedRevisionArg) throw conflict(actualRevision);
|
|
2132
|
+
const events = asStoreFormat(
|
|
2133
|
+
'Agent event change does not apply to the stored log',
|
|
2134
|
+
() => plugins.flexAgent.applyAgentEventChange(checkout?.snapshot?.events ?? [], change),
|
|
2135
|
+
);
|
|
2136
|
+
// The stored events were validated when they were stored and are never edited, and the
|
|
2137
|
+
// change is this store's own copy: the log is checked, never copied again.
|
|
2138
|
+
const snapshot = asStoreFormat(
|
|
2139
|
+
'Invalid schema-2 Agent event snapshot',
|
|
2140
|
+
() => plugins.flexAgent.validateAgentEventSnapshot({
|
|
2141
|
+
schemaVersion: 2,
|
|
2142
|
+
sessionId: sessionIdArg,
|
|
2143
|
+
revision: expectedRevisionArg + 1,
|
|
2144
|
+
updatedAt: events.reduce((latest, event) => Math.max(latest, event.timestamp), 0),
|
|
2145
|
+
events,
|
|
2146
|
+
}, sessionIdArg),
|
|
2147
|
+
);
|
|
2148
|
+
await this.saveDomain(
|
|
2149
|
+
lease,
|
|
2150
|
+
identity,
|
|
2151
|
+
snapshot,
|
|
2152
|
+
expectedRevisionArg,
|
|
2153
|
+
conflict,
|
|
2154
|
+
undefined,
|
|
2155
|
+
undefined,
|
|
2156
|
+
undefined,
|
|
2157
|
+
checkout,
|
|
2158
|
+
);
|
|
2159
|
+
return snapshot.revision;
|
|
2160
|
+
} finally {
|
|
2161
|
+
checkout?.release();
|
|
2162
|
+
}
|
|
2191
2163
|
});
|
|
2192
2164
|
}
|
|
2193
2165
|
|
|
@@ -2396,11 +2368,19 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2396
2368
|
sessionIdArg: string,
|
|
2397
2369
|
): TFlexProjectionSnapshot {
|
|
2398
2370
|
const value = cloneJson(valueArg as TFlexProjectionSnapshot);
|
|
2399
|
-
|
|
2371
|
+
this.assertProjectionSnapshot(value, sessionIdArg);
|
|
2372
|
+
return value;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
/** Checks a projection snapshot in place: FlexHarness owns what a valid projection is. */
|
|
2376
|
+
private assertProjectionSnapshot(
|
|
2377
|
+
valueArg: TFlexProjectionSnapshot,
|
|
2378
|
+
sessionIdArg: string,
|
|
2379
|
+
): void {
|
|
2380
|
+
if (!Number.isSafeInteger(valueArg?.revision) || valueArg.revision < 0) {
|
|
2400
2381
|
throw new plugins.flexharness.FlexHarnessStoreFormatError('Invalid projection snapshot revision.');
|
|
2401
2382
|
}
|
|
2402
|
-
plugins.flexharness.assertFlexProjectionSnapshot(
|
|
2403
|
-
return value;
|
|
2383
|
+
plugins.flexharness.assertFlexProjectionSnapshot(valueArg, sessionIdArg);
|
|
2404
2384
|
}
|
|
2405
2385
|
|
|
2406
2386
|
private async validatePermissionSnapshot(valueArg: unknown): Promise<TFlexPermissionSnapshot> {
|
|
@@ -2530,6 +2510,7 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2530
2510
|
preparationArg: IFlexCandidatePreparation,
|
|
2531
2511
|
currentArg: TStoredDomainHead | null,
|
|
2532
2512
|
) => Promise<IFlexPublicTranscriptManifest>,
|
|
2513
|
+
checkoutArg?: IFlexDomainCheckout<TSnapshot>,
|
|
2533
2514
|
): Promise<TStoredDomainHead> {
|
|
2534
2515
|
if (!Number.isSafeInteger(revisionArg) || Number(revisionArg) < 1) {
|
|
2535
2516
|
throw new plugins.flexharness.FlexHarnessStoreFormatError('Invalid domain revision.');
|
|
@@ -2542,15 +2523,30 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2542
2523
|
const candidateId = randomCandidateId();
|
|
2543
2524
|
const createdAt = new Date().toISOString();
|
|
2544
2525
|
const incarnationId = current?.incarnationId ?? randomCandidateId();
|
|
2545
|
-
const
|
|
2546
|
-
|
|
2547
|
-
|
|
2526
|
+
const sharedChunkIds = new Set(current?.privateCandidate.chunks.map((chunk) => chunk.id) ?? []);
|
|
2527
|
+
// The checked-out layout describes the candidate the head names now, or it is not used.
|
|
2528
|
+
const previous = current && checkoutArg?.layout
|
|
2529
|
+
&& checkoutArg.head.currentPrivateCandidateId === current.currentPrivateCandidateId
|
|
2530
|
+
&& checkoutArg.layout.sha256 === current.privateCandidate.sha256
|
|
2531
|
+
&& checkoutArg.layout.chunks.length === current.privateCandidate.chunks.length
|
|
2532
|
+
? checkoutArg.layout
|
|
2533
|
+
: undefined;
|
|
2534
|
+
const projection = buildFlexPrivateSnapshot({
|
|
2535
|
+
owner: {
|
|
2536
|
+
controllerId: this.controllerId,
|
|
2537
|
+
storageKey: identityArg.storageKey,
|
|
2538
|
+
domain: identityArg.domain,
|
|
2539
|
+
...(identityArg.sessionId !== undefined ? { sessionId: identityArg.sessionId } : {}),
|
|
2540
|
+
...(identityArg.archiveId !== undefined ? { archiveId: identityArg.archiveId } : {}),
|
|
2541
|
+
incarnationId,
|
|
2542
|
+
},
|
|
2548
2543
|
candidateId,
|
|
2549
2544
|
revision,
|
|
2550
|
-
snapshotArg,
|
|
2551
2545
|
createdAt,
|
|
2552
|
-
|
|
2553
|
-
|
|
2546
|
+
snapshot: snapshotArg,
|
|
2547
|
+
...(previous === undefined ? {} : { previous }),
|
|
2548
|
+
sharedChunkIds,
|
|
2549
|
+
});
|
|
2554
2550
|
const manifest: IFlexPrivateCandidateManifest = {
|
|
2555
2551
|
candidateId,
|
|
2556
2552
|
revision,
|
|
@@ -2564,7 +2560,7 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2564
2560
|
await this.writeAndVerifyPrivate(
|
|
2565
2561
|
projection.candidate,
|
|
2566
2562
|
projection.chunks,
|
|
2567
|
-
|
|
2563
|
+
sharedChunkIds,
|
|
2568
2564
|
abortSignalArg,
|
|
2569
2565
|
);
|
|
2570
2566
|
abortSignalArg?.throwIfAborted();
|
|
@@ -2603,7 +2599,14 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2603
2599
|
throw errorArg;
|
|
2604
2600
|
}
|
|
2605
2601
|
committed = true;
|
|
2606
|
-
|
|
2602
|
+
// Only the domains whose saves take changes keep a layout to build the next save from.
|
|
2603
|
+
this.cacheSnapshot(
|
|
2604
|
+
next,
|
|
2605
|
+
snapshotArg,
|
|
2606
|
+
identityArg.domain === 'projection' || identityArg.domain === 'agentEvents'
|
|
2607
|
+
? projection.layout
|
|
2608
|
+
: undefined,
|
|
2609
|
+
);
|
|
2607
2610
|
if (current) {
|
|
2608
2611
|
await this.cleanupPrivateCandidate(
|
|
2609
2612
|
current.currentPrivateCandidateId,
|
|
@@ -2618,6 +2621,7 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2618
2621
|
);
|
|
2619
2622
|
})();
|
|
2620
2623
|
} finally {
|
|
2624
|
+
discardFlexPrivateChunks(projection.minted);
|
|
2621
2625
|
if (!committed) this.queueOrphanMaintenance();
|
|
2622
2626
|
this.activeCandidateIds.delete(candidateId);
|
|
2623
2627
|
}
|
|
@@ -2654,6 +2658,48 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2654
2658
|
return { head, snapshot };
|
|
2655
2659
|
}
|
|
2656
2660
|
|
|
2661
|
+
/**
|
|
2662
|
+
* The committed snapshot a save changes: taken out of the cache, so the save holds its layout
|
|
2663
|
+
* alone, or read when `loadArg` asks for it. Undefined when the domain has no head. The caller
|
|
2664
|
+
* releases it once the save ended, whatever its outcome.
|
|
2665
|
+
*/
|
|
2666
|
+
private async checkoutDomain<TSnapshot>(
|
|
2667
|
+
identityArg: IFlexDomainIdentity,
|
|
2668
|
+
validateArg: (valueArg: unknown) => TSnapshot | Promise<TSnapshot>,
|
|
2669
|
+
loadArg: boolean,
|
|
2670
|
+
): Promise<IFlexDomainCheckout<TSnapshot> | undefined> {
|
|
2671
|
+
const head = await this.readDomainHead(identityArg);
|
|
2672
|
+
if (!head) return undefined;
|
|
2673
|
+
const entry = this.takeCachedSnapshot(head);
|
|
2674
|
+
if (entry) {
|
|
2675
|
+
let released = false;
|
|
2676
|
+
return {
|
|
2677
|
+
head,
|
|
2678
|
+
snapshot: entry.snapshot as TSnapshot,
|
|
2679
|
+
...(entry.layout === undefined ? {} : { layout: entry.layout }),
|
|
2680
|
+
release: () => {
|
|
2681
|
+
if (released) return;
|
|
2682
|
+
released = true;
|
|
2683
|
+
if (entry.layout) releaseFlexPrivateLayout(entry.layout);
|
|
2684
|
+
},
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
if (!loadArg) return { head, release: () => undefined };
|
|
2688
|
+
const snapshot = await this.loadPrivateCandidate(
|
|
2689
|
+
identityArg.storageKey,
|
|
2690
|
+
head.privateCandidate,
|
|
2691
|
+
validateArg,
|
|
2692
|
+
undefined,
|
|
2693
|
+
{ identity: identityArg, incarnationId: head.incarnationId },
|
|
2694
|
+
);
|
|
2695
|
+
if ((snapshot as { revision?: unknown }).revision !== head.revision) {
|
|
2696
|
+
throw new plugins.flexharness.FlexHarnessStoreFormatError(
|
|
2697
|
+
'Flex domain snapshot revision does not match its head.',
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
return { head, snapshot, release: () => undefined };
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2657
2703
|
private async readMigration(
|
|
2658
2704
|
storageKeyArg: string,
|
|
2659
2705
|
optionsArg?: IFlexReadOptions,
|
|
@@ -2986,24 +3032,65 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
2986
3032
|
return entry.snapshot as TSnapshot;
|
|
2987
3033
|
}
|
|
2988
3034
|
|
|
2989
|
-
/**
|
|
2990
|
-
|
|
2991
|
-
|
|
3035
|
+
/**
|
|
3036
|
+
* Takes the cached snapshot of `headArg` out of the cache for a save, which then holds its
|
|
3037
|
+
* layout until it releases it; a stale entry is dropped.
|
|
3038
|
+
*/
|
|
3039
|
+
private takeCachedSnapshot(headArg: IFlexDomainHeadDocument): IFlexCachedSnapshot | undefined {
|
|
3040
|
+
const entry = this.snapshotCache.get(headArg.id);
|
|
3041
|
+
if (!entry) return undefined;
|
|
3042
|
+
this.snapshotCache.delete(headArg.id);
|
|
3043
|
+
this.snapshotCacheBytes -= entry.cost;
|
|
3044
|
+
if (
|
|
3045
|
+
entry.candidateId === headArg.currentPrivateCandidateId
|
|
3046
|
+
&& entry.bytes === headArg.privateCandidate.bytes
|
|
3047
|
+
&& (entry.layout === undefined || entry.layout.sha256 === headArg.privateCandidate.sha256)
|
|
3048
|
+
) return entry;
|
|
3049
|
+
if (entry.layout) releaseFlexPrivateLayout(entry.layout);
|
|
3050
|
+
return undefined;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
/**
|
|
3054
|
+
* Keeps a projection, scope or Agent event snapshot this writer committed or loaded, never
|
|
3055
|
+
* edited after, and the layout this writer stored it in.
|
|
3056
|
+
*/
|
|
3057
|
+
private cacheSnapshot(
|
|
3058
|
+
headArg: IFlexDomainHeadDocument,
|
|
3059
|
+
snapshotArg: unknown,
|
|
3060
|
+
layoutArg?: IFlexPrivateLayout,
|
|
3061
|
+
): void {
|
|
3062
|
+
if (
|
|
3063
|
+
headArg.domain !== 'projection'
|
|
3064
|
+
&& headArg.domain !== 'scope'
|
|
3065
|
+
&& headArg.domain !== 'agentEvents'
|
|
3066
|
+
) return;
|
|
2992
3067
|
if (!this.heldLeases.has(headArg.storageKey)) return;
|
|
2993
3068
|
this.forgetCachedSnapshot(headArg.id);
|
|
2994
3069
|
const bytes = headArg.privateCandidate.bytes;
|
|
2995
|
-
|
|
3070
|
+
const cost = (layoutArg?.bytes ?? 0) + Math.ceil(bytes * flexSnapshotHeapFactor);
|
|
3071
|
+
if (cost > flexSnapshotCacheBytes) {
|
|
3072
|
+
if (!this.uncachedSnapshotHeads.has(headArg.id)) {
|
|
3073
|
+
this.uncachedSnapshotHeads.add(headArg.id);
|
|
3074
|
+
console.warn(
|
|
3075
|
+
`Flex snapshot ${headArg.id} of ${bytes} bytes exceeds the ${flexSnapshotCacheBytes}-byte snapshot cache; its saves read and rewrite it whole.`,
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
this.uncachedSnapshotHeads.delete(headArg.id);
|
|
3081
|
+
if (layoutArg) holdFlexPrivateLayout(layoutArg);
|
|
2996
3082
|
this.snapshotCache.set(headArg.id, {
|
|
2997
3083
|
storageKey: headArg.storageKey,
|
|
2998
3084
|
candidateId: headArg.currentPrivateCandidateId,
|
|
2999
3085
|
snapshot: snapshotArg,
|
|
3000
3086
|
bytes,
|
|
3087
|
+
...(layoutArg === undefined ? {} : { layout: layoutArg }),
|
|
3088
|
+
cost,
|
|
3001
3089
|
});
|
|
3002
|
-
this.snapshotCacheBytes +=
|
|
3003
|
-
for (const
|
|
3090
|
+
this.snapshotCacheBytes += cost;
|
|
3091
|
+
for (const id of this.snapshotCache.keys()) {
|
|
3004
3092
|
if (this.snapshotCacheBytes <= flexSnapshotCacheBytes) break;
|
|
3005
|
-
this.
|
|
3006
|
-
this.snapshotCacheBytes -= entry.bytes;
|
|
3093
|
+
this.forgetCachedSnapshot(id);
|
|
3007
3094
|
}
|
|
3008
3095
|
}
|
|
3009
3096
|
|
|
@@ -3011,7 +3098,8 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
3011
3098
|
const entry = this.snapshotCache.get(headIdArg);
|
|
3012
3099
|
if (!entry) return;
|
|
3013
3100
|
this.snapshotCache.delete(headIdArg);
|
|
3014
|
-
this.snapshotCacheBytes -= entry.
|
|
3101
|
+
this.snapshotCacheBytes -= entry.cost;
|
|
3102
|
+
if (entry.layout) releaseFlexPrivateLayout(entry.layout);
|
|
3015
3103
|
}
|
|
3016
3104
|
|
|
3017
3105
|
private forgetCachedSnapshots(storageKeyArg: string): void {
|
|
@@ -3037,6 +3125,7 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
3037
3125
|
{ id, sha256: digest, bytes },
|
|
3038
3126
|
]),
|
|
3039
3127
|
);
|
|
3128
|
+
const records = this.transcriptMessageRecords(storageKeyArg);
|
|
3040
3129
|
let build = createProjectionTranscript(
|
|
3041
3130
|
this.controllerId,
|
|
3042
3131
|
storageKeyArg,
|
|
@@ -3044,6 +3133,8 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
3044
3133
|
messagesArg,
|
|
3045
3134
|
preparationArg,
|
|
3046
3135
|
known,
|
|
3136
|
+
undefined,
|
|
3137
|
+
records,
|
|
3047
3138
|
);
|
|
3048
3139
|
const unknownIds = build.records.map((record) => record.id).filter((id) => !known.has(id));
|
|
3049
3140
|
if (unknownIds.length > 0) {
|
|
@@ -3057,12 +3148,52 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
3057
3148
|
messagesArg,
|
|
3058
3149
|
preparationArg,
|
|
3059
3150
|
known,
|
|
3151
|
+
undefined,
|
|
3152
|
+
records,
|
|
3060
3153
|
);
|
|
3061
3154
|
}
|
|
3062
3155
|
}
|
|
3063
3156
|
return { ...build, added: build.records.filter((record) => !known.has(record.id)) };
|
|
3064
3157
|
}
|
|
3065
3158
|
|
|
3159
|
+
/** The record facts of a scope's messages, from this store's per-message caches. */
|
|
3160
|
+
private transcriptMessageRecords(storageKeyArg: string): IFlexTranscriptMessageRecords {
|
|
3161
|
+
return {
|
|
3162
|
+
recordId: (messageIndexArg, messageArg) => {
|
|
3163
|
+
const cached = this.publicMessageRecordIds.get(messageArg);
|
|
3164
|
+
if (cached?.storageKey === storageKeyArg && cached.messageIndex === messageIndexArg) {
|
|
3165
|
+
return cached.recordId;
|
|
3166
|
+
}
|
|
3167
|
+
const recordId = publicMessageDocumentId(
|
|
3168
|
+
this.controllerId,
|
|
3169
|
+
storageKeyArg,
|
|
3170
|
+
messageIndexArg,
|
|
3171
|
+
messageArg,
|
|
3172
|
+
);
|
|
3173
|
+
this.publicMessageRecordIds.set(messageArg, {
|
|
3174
|
+
storageKey: storageKeyArg,
|
|
3175
|
+
messageIndex: messageIndexArg,
|
|
3176
|
+
recordId,
|
|
3177
|
+
});
|
|
3178
|
+
return recordId;
|
|
3179
|
+
},
|
|
3180
|
+
messageBytes: (messageArg) => {
|
|
3181
|
+
const cached = this.publicMessageBytes.get(messageArg);
|
|
3182
|
+
if (cached !== undefined) return cached;
|
|
3183
|
+
const bytes = serializedBytes(messageArg).byteLength;
|
|
3184
|
+
this.publicMessageBytes.set(messageArg, bytes);
|
|
3185
|
+
return bytes;
|
|
3186
|
+
},
|
|
3187
|
+
placeholder: (messageArg, budgetArg) => {
|
|
3188
|
+
const cached = this.publicMessagePlaceholders.get(messageArg);
|
|
3189
|
+
if (cached?.budget === budgetArg) return cached.placeholder;
|
|
3190
|
+
const placeholder = placeholderMessage(messageArg, budgetArg);
|
|
3191
|
+
this.publicMessagePlaceholders.set(messageArg, { budget: budgetArg, placeholder });
|
|
3192
|
+
return placeholder;
|
|
3193
|
+
},
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3066
3197
|
/**
|
|
3067
3198
|
* Stores a transcript before the records it adds, so an interrupted write leaves a transcript no
|
|
3068
3199
|
* head names that still names everything the write stored, and proves both.
|
|
@@ -4374,11 +4505,15 @@ export class FlexStore implements plugins.flexharness.IFlexHarnessStores {
|
|
|
4374
4505
|
.flatMap((chunk: unknown) => (
|
|
4375
4506
|
typeof (chunk as { id?: unknown })?.id === 'string' ? [(chunk as { id: string }).id] : []
|
|
4376
4507
|
));
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
}, options);
|
|
4508
|
+
// The chunks the candidate wrote or lists, but those `keepArg` names: two deletes the indexes
|
|
4509
|
+
// answer (by candidate, by id), so only the chunks that go are examined, never every chunk of
|
|
4510
|
+
// the storage key.
|
|
4511
|
+
const keep = keepArg.size > 0 ? { id: { $nin: [...keepArg] } } : {};
|
|
4512
|
+
await FlexPrivateChunkModel.atomicDeleteMany({ ...owner, candidateId: candidateIdArg, ...keep }, options);
|
|
4513
|
+
const unkept = listed.filter((id) => !keepArg.has(id));
|
|
4514
|
+
if (unkept.length > 0) {
|
|
4515
|
+
await FlexPrivateChunkModel.atomicDeleteMany({ ...owner, id: { $in: unkept } }, options);
|
|
4516
|
+
}
|
|
4382
4517
|
await FlexPrivateCandidateModel.atomicDeleteMany({ ...owner, candidateId: candidateIdArg }, options);
|
|
4383
4518
|
}
|
|
4384
4519
|
|