--- a/packages/runner/src/cfc/prepare.ts +++ b/packages/runner/src/cfc/prepare.ts @@ -1186,11 +1186,18 @@ */ class VerifierMetadataResolver { #tx: IExtendedStorageTransaction; - #envelopes = new Map>(); + #envelopes = new Map< + MemorySpace, + Map< + ReturnType, + Map> + > + >(); #seenWrites = 0; #viewIndexes = new WeakMap(); #views = new WeakMap>(); #labelIndexes = new WeakMap(); + #labels = new WeakMap>(); #coverIndexes = new WeakMap(); #covers = new WeakMap< CfcLabelView, @@ -1209,11 +1216,20 @@ scope: ReturnType, type: MediaType, ): CfcMetadata | undefined { - const key = stringTupleKey([space, scope, id]); - let types = this.#envelopes.get(key); + let scopes = this.#envelopes.get(space); + if (scopes === undefined) { + scopes = new Map(); + this.#envelopes.set(space, scopes); + } + let documents = scopes.get(scope); + if (documents === undefined) { + documents = new Map(); + scopes.set(scope, documents); + } + let types = documents.get(id); if (types === undefined) { types = new Map(); - this.#envelopes.set(key, types); + documents.set(id, types); } if (!types.has(type)) { types.set(type, storedMetadataFor(this.#tx, space, id, scope, type)); @@ -1223,6 +1239,13 @@ /** Resolves a source label using the validated envelope's path index. */ label(metadata: CfcMetadata, path: readonly string[]): IFCLabel | undefined { + let labels = this.#labels.get(metadata); + if (labels === undefined) { + labels = new Map(); + this.#labels.set(metadata, labels); + } + const key = encodePointer(path); + if (labels.has(key)) return labels.get(key); let index = this.#labelIndexes.get(metadata); if (index === undefined) { index = new ConsumedLabelIndex(metadata.labelMap.entries, { @@ -1234,10 +1257,12 @@ }); this.#labelIndexes.set(metadata, index); } - return labelForEntriesAtPath( + const label = labelForEntriesAtPath( index.overlapping(path, false).map(({ entry }) => entry), path, ); + labels.set(key, label); + return label; } /** Reuses rebased views while their validated source envelope is unchanged. */ @@ -1341,18 +1366,34 @@ const writes = getTransactionWriteAttempts(this.#tx.tx); if (writes === undefined) { this.#envelopes.clear(); + this.#viewIndexes = new WeakMap(); + this.#views = new WeakMap(); + this.#labelIndexes = new WeakMap(); + this.#labels = new WeakMap(); + this.#coverIndexes = new WeakMap(); + this.#covers = new WeakMap(); this.#seenWrites = 0; return; } for (let index = this.#seenWrites; index < writes.length; index++) { const write = writes[index]; - this.#envelopes.delete( - stringTupleKey([ - write.space, - normalizeCellScope(write.scope), - write.id, - ]), + const documents = this.#envelopes.get(write.space)?.get( + normalizeCellScope(write.scope), ); + const types = documents?.get(write.id); + for (const metadata of types?.values() ?? []) { + if (metadata === undefined) continue; + for (const view of this.#views.get(metadata)?.values() ?? []) { + if (view === undefined) continue; + this.#coverIndexes.delete(view); + this.#covers.delete(view); + } + this.#views.delete(metadata); + this.#viewIndexes.delete(metadata); + this.#labelIndexes.delete(metadata); + this.#labels.delete(metadata); + } + documents?.delete(write.id); } this.#seenWrites = writes.length; } @@ -4290,16 +4331,24 @@ // while producing the current view. Refresh after that inspection so every // envelope resolution observes those writes. metadataResolver.refresh(); - const readSources = currentReads.map((read) => ({ - ...read, - path: canonicalizeLogicalPath(read.path), - metadata: metadataResolver.read( + const schemaEntries = cfcSchemaEntries(schema); + const needsReadLabels = provenance !== undefined || + schemaEntries.some(({ schema: entrySchema }) => { + const ifc = isObjectOrArray(entrySchema) ? entrySchema.ifc : undefined; + return (ifc?.requiredIntegrity?.length ?? 0) > 0 || + ifc?.maxConfidentiality !== undefined; + }); + const sourceMetadata = currentReads.map((read) => { + // Gate paths are captured before resolving an envelope: backend reads may + // mutate a caller-owned path array. Ungated targets only need the address. + if (needsReadLabels) read.path = canonicalizeLogicalPath(read.path); + return metadataResolver.read( read.space, read.id, normalizeCellScope(read.scope), read.type ?? "application/json", - ), - })); + ); + }); if (provenance !== undefined) { provenance.clockLessReads = clockLessReads; } @@ -4310,10 +4359,10 @@ // declaring `requiredIntegrity` or `maxConfidentiality` reads the result, // so the set is assembled on first ask and kept for the rest of the call. const buildGatedReads = () => { - const gatedReads = readSources.map(({ metadata, ...read }) => ({ + const gatedReads = currentReads.map((read, index) => ({ ...read, label: effectiveReadLabel( - metadata, + sourceMetadata[index], read.path, { nonRecursive: read.nonRecursive, consumes: "all" }, ), @@ -4359,7 +4408,7 @@ .filter((read) => !isProvenanceOnlyConsumedLabel(read.label!)) .length; - for (const entry of cfcSchemaEntries(schema)) { + for (const entry of schemaEntries) { if ( !ifcEntryAppliesToAttemptedWrite( tx,