@milaboratories/pl-tree 1.15.0 → 1.15.2

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/dist/state.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"state.js","names":[],"sources":["../src/state.ts"],"sourcesContent":["import type {\n BasicResourceData,\n FieldData,\n FieldStatus,\n FieldType,\n KeyValue,\n OptionalSignedResourceId,\n ResourceData,\n SignedResourceId,\n ResourceKind,\n ResourceType,\n} from \"@milaboratories/pl-client\";\nimport {\n isNotNullSignedResourceId,\n isNullSignedResourceId,\n NullSignedResourceId,\n resourceIdToString,\n stringifyWithResourceId,\n} from \"@milaboratories/pl-client\";\nimport type { Watcher } from \"@milaboratories/computable\";\nimport { ChangeSource, KeyedChangeSource } from \"@milaboratories/computable\";\nimport { PlTreeEntry } from \"./accessors\";\nimport type { ValueAndError } from \"./value_and_error\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { cachedDecode, cachedDeserialize, notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { FieldTraversalStep, GetFieldStep } from \"./traversal_ops\";\nimport type { FinalResourceDataPredicate } from \"@milaboratories/pl-client\";\n\nexport type ExtendedResourceData = ResourceData & {\n kv: KeyValue[];\n};\n\nexport class TreeStateUpdateError extends Error {\n constructor(message: string) {\n super(message);\n }\n}\n\nclass PlTreeField implements FieldData {\n readonly change = new ChangeSource();\n\n constructor(\n public readonly name: string,\n public type: FieldType,\n public value: OptionalSignedResourceId,\n public error: OptionalSignedResourceId,\n public status: FieldStatus,\n public valueIsFinal: boolean,\n /** Last version of resource this field was observed, used to garbage collect fields in tree patching procedure */\n public resourceVersion: number,\n ) {}\n\n get state(): FieldData {\n return {\n name: this.name,\n type: this.type,\n status: this.status,\n value: this.value,\n error: this.error,\n valueIsFinal: this.valueIsFinal,\n };\n }\n}\n\nconst InitialResourceVersion = 0;\n\nexport type ResourceDataWithFinalState = ResourceData & {\n finalState: boolean;\n};\n\n/** Cross-cycle re-fetch duplication counters, populated by {@link PlTreeState.updateFromResourceData}.\n * Separates genuinely new/changed resources from redundant re-fetches of unchanged ones\n * (the delta-skip opportunity: intra-cycle dedup already happens in the loader, so all waste here\n * is cross-cycle). */\nexport interface ResourceUpdateStat {\n /** Resources seen for the first time in this update. */\n resourcesNew: number;\n /** Resources already held whose state actually changed. */\n resourcesChanged: number;\n /** Resources already held that came back unchanged: a pure duplicate re-fetch. */\n resourcesUnchanged: number;\n /** data + KV bytes of the unchanged bucket: wasted downlink. */\n bytesUnchanged: number;\n /** Changed resources whose immutable metadata (type, kind, field set) did not change,\n * i.e. metadata re-streamed even though only a value or flag flipped. */\n metadataStableChanged: number;\n /** Per-resource fetches spent on unchanged resources (BFS only; streaming re-sends them\n * inside one stream, so this stays 0). */\n bfsRequestsWasted: number;\n /** Whether the current load used backend streaming; set by the loader, read here to\n * attribute {@link bfsRequestsWasted}. */\n usedStreaming: boolean;\n // Breakdown of what changed among resourcesChanged (a resource can bump several).\n /** New fields appeared on a held resource. */\n fieldsAdded: number;\n /** Dynamic fields disappeared from a held resource. */\n fieldsRemoved: number;\n /** Existing field value pointer changed (a new value/result was attached). */\n fieldsChanged: number;\n /** KV entries added, changed, or deleted. */\n kvChanged: number;\n /** resourceReady flipped. */\n readyFlips: number;\n /** inputsLocked or outputsLocked transitioned. */\n locksChanged: number;\n /** An error resource was attached. */\n errorsAttached: number;\n /** originalResourceId resolved (duplicate -> original). */\n duplicatesResolved: number;\n /** Held resources that transitioned to final this update. */\n resourcesMarkedFinal: number;\n}\n\n/** Never store instances of this class, always get fresh instance from {@link PlTreeState} */\nexport class PlTreeResource implements ResourceDataWithFinalState {\n /** Tracks number of other resources referencing this resource. Used to perform garbage collection in tree patching procedure */\n refCount: number = 0;\n\n /** Increments each time resource is checked for difference with new state */\n version: number = InitialResourceVersion;\n /** Set to resource version when resource state, or it's fields have changed */\n dataVersion: number = InitialResourceVersion;\n\n readonly fieldsMap: Map<string, PlTreeField> = new Map();\n\n readonly kv = new Map<string, Uint8Array>();\n\n readonly resourceRemoved = new ChangeSource();\n\n // following change source are removed when resource is marked as final\n\n finalChanged? = new ChangeSource();\n\n resourceStateChange? = new ChangeSource();\n\n lockedChange? = new ChangeSource();\n inputAndServiceFieldListChanged? = new ChangeSource();\n outputFieldListChanged? = new ChangeSource();\n dynamicFieldListChanged? = new ChangeSource();\n\n // kvChangedGlobal? = new ChangeSource();\n kvChangedPerKey? = new KeyedChangeSource();\n\n readonly id: SignedResourceId;\n originalResourceId: OptionalSignedResourceId;\n\n readonly kind: ResourceKind;\n readonly type: ResourceType;\n\n readonly data?: Uint8Array;\n private dataAsString?: string;\n private dataAsJson?: unknown;\n\n error: OptionalSignedResourceId;\n\n inputsLocked: boolean;\n outputsLocked: boolean;\n resourceReady: boolean;\n finalFlag: boolean;\n\n /** Set externally by the tree, using {@link FinalResourceDataPredicate} */\n _finalState: boolean = false;\n\n private readonly logger?: MiLogger;\n\n constructor(initialState: BasicResourceData, logger?: MiLogger) {\n this.id = initialState.id;\n this.originalResourceId = initialState.originalResourceId;\n this.kind = initialState.kind;\n this.type = initialState.type;\n this.data = initialState.data;\n this.error = initialState.error;\n this.inputsLocked = initialState.inputsLocked;\n this.outputsLocked = initialState.outputsLocked;\n this.resourceReady = initialState.resourceReady;\n this.finalFlag = initialState.final;\n this.logger = logger;\n }\n\n // TODO add logging\n\n private info(msg: string) {\n if (this.logger !== undefined) this.logger.info(msg);\n }\n\n private warn(msg: string) {\n if (this.logger !== undefined) this.logger.warn(msg);\n }\n\n get final(): boolean {\n return this.finalFlag;\n }\n\n get finalState(): boolean {\n return this._finalState;\n }\n\n get fields(): FieldData[] {\n return [...this.fieldsMap.values()];\n }\n\n public getField(\n watcher: Watcher,\n _step:\n | (Omit<GetFieldStep, \"errorIfFieldNotFound\"> & { errorIfFieldNotFound: true })\n | (Omit<GetFieldStep, \"errorIfFieldNotSet\"> & { errorIfFieldNotSet: true }),\n onUnstable: (marker: string) => void,\n ): ValueAndError<SignedResourceId>;\n public getField(\n watcher: Watcher,\n _step: string | GetFieldStep,\n onUnstable: (marker: string) => void,\n ): ValueAndError<SignedResourceId> | undefined;\n public getField(\n watcher: Watcher,\n _step: string | GetFieldStep,\n onUnstable: (marker: string) => void = () => {},\n ): ValueAndError<SignedResourceId> | undefined {\n const step: FieldTraversalStep = typeof _step === \"string\" ? { field: _step } : _step;\n\n const field = this.fieldsMap.get(step.field);\n if (field === undefined) {\n if (step.errorIfFieldNotFound || step.errorIfFieldNotSet)\n throw new Error(\n `Field \"${step.field}\" not found in resource ${resourceIdToString(this.id)}`,\n );\n\n if (!this.inputsLocked) this.inputAndServiceFieldListChanged?.attachWatcher(watcher);\n else if (step.assertFieldType === \"Service\" || step.assertFieldType === \"Input\") {\n if (step.allowPermanentAbsence)\n // stable absence of field\n return undefined;\n else throw new Error(`Service or input field not found ${step.field}.`);\n }\n\n if (!this.outputsLocked) this.outputFieldListChanged?.attachWatcher(watcher);\n else if (step.assertFieldType === \"Output\") {\n if (step.allowPermanentAbsence)\n // stable absence of field\n return undefined;\n else throw new Error(`Output field not found ${step.field}.`);\n }\n\n this.dynamicFieldListChanged?.attachWatcher(watcher);\n if (!this._finalState && !step.stableIfNotFound) onUnstable(\"field_not_found:\" + step.field);\n\n return undefined;\n } else {\n if (step.assertFieldType !== undefined && field.type !== step.assertFieldType)\n throw new Error(\n `Unexpected field type: expected ${step.assertFieldType} but got ${field.type} for the field name ${step.field}`,\n );\n\n const ret = {} as ValueAndError<SignedResourceId>;\n if (isNotNullSignedResourceId(field.value)) ret.value = field.value;\n if (isNotNullSignedResourceId(field.error)) ret.error = field.error;\n if (ret.value === undefined && ret.error === undefined)\n // this method returns value and error of the field, thus those values are considered to be accessed;\n // any existing but not resolved field here is considered to be unstable, in the sense it is\n // considered to acquire some resolved value eventually\n onUnstable(\"field_not_resolved:\" + step.field);\n field.change.attachWatcher(watcher);\n return ret;\n }\n }\n\n public getInputsLocked(watcher: Watcher): boolean {\n if (!this.inputsLocked)\n // reverse transition can't happen, so there is no reason to wait for value to change\n this.resourceStateChange?.attachWatcher(watcher);\n return this.inputsLocked;\n }\n\n public getOutputsLocked(watcher: Watcher): boolean {\n if (!this.outputsLocked)\n // reverse transition can't happen, so there is no reason to wait for value to change\n this.resourceStateChange?.attachWatcher(watcher);\n return this.outputsLocked;\n }\n\n public get isReadyOrError(): boolean {\n return (\n this.error !== NullSignedResourceId ||\n this.resourceReady ||\n this.originalResourceId !== NullSignedResourceId\n );\n }\n\n public getIsFinal(watcher: Watcher): boolean {\n this.finalChanged?.attachWatcher(watcher);\n return this._finalState;\n }\n\n public getIsReadyOrError(watcher: Watcher): boolean {\n if (!this.isReadyOrError)\n // reverse transition can't happen, so there is no reason to wait for value to change if it is already true\n this.resourceStateChange?.attachWatcher(watcher);\n return this.isReadyOrError;\n }\n\n public getError(watcher: Watcher): SignedResourceId | undefined {\n if (isNullSignedResourceId(this.error)) {\n this.resourceStateChange?.attachWatcher(watcher);\n return undefined;\n } else {\n // reverse transition can't happen, so there is no reason to wait for value to change, if error already set\n return this.error;\n }\n }\n\n public listInputFields(watcher: Watcher): string[] {\n const ret: string[] = [];\n this.fieldsMap.forEach((field, name) => {\n if (field.type === \"Input\" || field.type === \"Service\") ret.push(name);\n });\n if (!this.inputsLocked) this.inputAndServiceFieldListChanged?.attachWatcher(watcher);\n\n return ret;\n }\n\n public listOutputFields(watcher: Watcher): string[] {\n const ret: string[] = [];\n this.fieldsMap.forEach((field, name) => {\n if (field.type === \"Output\") ret.push(name);\n });\n if (!this.outputsLocked) this.outputFieldListChanged?.attachWatcher(watcher);\n\n return ret;\n }\n\n public listDynamicFields(watcher: Watcher): string[] {\n const ret: string[] = [];\n this.fieldsMap.forEach((field, name) => {\n if (field.type !== \"Input\" && field.type !== \"Output\") ret.push(name);\n });\n this.dynamicFieldListChanged?.attachWatcher(watcher);\n\n return ret;\n }\n\n public getKeyValue(watcher: Watcher, key: string): Uint8Array | undefined {\n this.kvChangedPerKey?.attachWatcher(key, watcher);\n return this.kv.get(key);\n }\n\n public getKeyValueString(watcher: Watcher, key: string): string | undefined {\n const bytes = this.getKeyValue(watcher, key);\n if (bytes === undefined) return undefined;\n return cachedDecode(bytes);\n }\n\n public getKeyValueAsJson<T = unknown>(watcher: Watcher, key: string): T | undefined {\n const bytes = this.getKeyValue(watcher, key);\n if (bytes === undefined) return undefined;\n return cachedDeserialize(bytes);\n }\n\n public getDataAsString(): string | undefined {\n if (this.data === undefined) return undefined;\n if (this.dataAsString === undefined) this.dataAsString = cachedDecode(this.data);\n return this.dataAsString;\n }\n\n public getDataAsJson<T = unknown>(): T | undefined {\n if (this.data === undefined) return undefined;\n if (this.dataAsJson === undefined) this.dataAsJson = cachedDeserialize(this.data);\n return this.dataAsJson as T;\n }\n\n verifyReadyState() {\n if (this.resourceReady && !this.inputsLocked)\n throw new Error(\n `ready without input or output lock: ${stringifyWithResourceId(this.basicState)}`,\n );\n }\n\n get basicState(): BasicResourceData {\n return {\n id: this.id,\n kind: this.kind,\n type: this.type,\n data: this.data,\n resourceReady: this.resourceReady,\n inputsLocked: this.inputsLocked,\n outputsLocked: this.outputsLocked,\n error: this.error,\n originalResourceId: this.originalResourceId,\n final: this.finalFlag,\n };\n }\n\n get extendedState(): ExtendedResourceData {\n return {\n ...this.basicState,\n fields: this.fields,\n kv: Array.from(this.kv.entries()).map(([key, value]) => ({ key, value })),\n };\n }\n\n /** Called when {@link FinalResourceDataPredicate} returns true for the state. */\n markFinal() {\n if (this._finalState) return;\n\n this._finalState = true;\n notEmpty(this.finalChanged).markChanged(\"marked final\");\n this.finalChanged = undefined;\n this.resourceStateChange = undefined;\n this.dynamicFieldListChanged = undefined;\n this.inputAndServiceFieldListChanged = undefined;\n this.outputFieldListChanged = undefined;\n this.lockedChange = undefined;\n // this.kvChangedGlobal = undefined;\n this.kvChangedPerKey = undefined;\n }\n\n /** Used for invalidation */\n markAllChanged() {\n this.fieldsMap.forEach((field) => field.change.markChanged(\"marked all changed\"));\n this.finalChanged?.markChanged(\"marked all changed\");\n this.resourceStateChange?.markChanged(\"marked all changed\");\n this.lockedChange?.markChanged(\"marked all changed\");\n this.inputAndServiceFieldListChanged?.markChanged(\"marked all changed\");\n this.outputFieldListChanged?.markChanged(\"marked all changed\");\n this.dynamicFieldListChanged?.markChanged(\"marked all changed\");\n // this.kvChangedGlobal?.markChanged('marked all changed');\n this.kvChangedPerKey?.markAllChanged(\"marked all changed\");\n this.resourceRemoved.markChanged(\"marked all changed\");\n }\n}\n\nexport class PlTreeState {\n /** resource heap */\n private resources: Map<SignedResourceId, PlTreeResource> = new Map();\n private readonly resourcesAdded = new ChangeSource();\n /** Marked when the root set changes (roots discovered or removed), so multi-root\n * accessors that read the set recompute. */\n private readonly rootsChanged = new ChangeSource();\n /** Resets to false if any invalid state transitions are registered,\n * after that tree will produce errors for any read or write operations */\n private _isValid: boolean = true;\n private invalidationMessage?: string;\n\n /** The set of roots — resources protected from GC. Originally a single `root`,\n * generalized to a set so several explicit roots and dynamically-discovered shared\n * roots can coexist in one heap. Mutated through {@link setRoots}. */\n public readonly roots: Set<SignedResourceId>;\n\n constructor(\n /** Resources protected from GC. Accepts a single id (single-root, the original\n * contract) or a set of ids (multi-root). */\n roots: SignedResourceId | Set<SignedResourceId>,\n public readonly isFinalPredicate: FinalResourceDataPredicate,\n ) {\n this.roots = roots instanceof Set ? new Set(roots) : new Set([roots]);\n }\n\n /** Backward-compatible single-root accessor. Returns the sole root and THROWS if the\n * tree has zero or more than one root, so a multi-root tree can never be read as if it\n * had one. Mirrors the {@link entry} guard. */\n public get root(): SignedResourceId {\n if (this.roots.size !== 1)\n throw new Error(\n `single-root accessor used on a tree with ${this.roots.size} roots; use the multi-root API`,\n );\n return this.roots.values().next().value!;\n }\n\n public forEachResource(cb: (res: ResourceDataWithFinalState) => void): void {\n this.resources.forEach((v) => cb(v));\n }\n\n /** False once the tree has been invalidated (an inconsistent update, or termination of\n * its synchronization loop). {@link dumpState} deliberately reads through an invalid\n * tree, so anything persisting that dump must check this first: the contents of an\n * invalidated tree are not something to write to disk. */\n public get isValid(): boolean {\n return this._isValid;\n }\n\n private checkValid() {\n if (!this._isValid) throw new Error(this.invalidationMessage ?? \"tree is in invalid state\");\n }\n\n /** Reactive read of the current root set. Attaches the watcher to {@link rootsChanged}\n * so a Computable recomputes when discovered roots appear or disappear. */\n public getRoots(watcher: Watcher): SignedResourceId[] {\n this.checkValid();\n this.rootsChanged.attachWatcher(watcher);\n return [...this.roots];\n }\n\n public get(watcher: Watcher, rid: SignedResourceId): PlTreeResource {\n this.checkValid();\n const res = this.resources.get(rid);\n if (res === undefined) {\n // to make recovery from resource not found possible, considering some\n // race conditions, where computable is created before tree is updated\n this.resourcesAdded.attachWatcher(watcher);\n throw new Error(`resource ${resourceIdToString(rid)} not found in the tree`);\n }\n res.resourceRemoved.attachWatcher(watcher);\n return res;\n }\n\n updateFromResourceData(\n resourceData: ExtendedResourceData[],\n opts: { allowOrphanInputs?: boolean; stat?: ResourceUpdateStat } = {},\n ) {\n const { allowOrphanInputs = false, stat } = opts;\n this.checkValid();\n\n // All resources for which recount should be incremented, first are aggregated in this list\n const incrementRefs: SignedResourceId[] = [];\n const decrementRefs: SignedResourceId[] = [];\n\n // patching / creating resources\n for (const rd of resourceData) {\n let resource = this.resources.get(rd.id);\n const held = resource !== undefined;\n let changed = false;\n // Structural/metadata change (new or removed field). type and kind are readonly, so\n // they never change; this flag isolates value/flag-only changes from real metadata churn.\n let metadataChanged = false;\n\n const statBeforeMutation = resource?.basicState;\n const unexpectedTransitionError = (reason: string): never => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { fields, ...rdWithoutFields } = rd;\n this.invalidateTree();\n throw new TreeStateUpdateError(\n `Unexpected resource state transition (${reason}): ${stringifyWithResourceId(\n rdWithoutFields,\n )} -> ${stringifyWithResourceId(statBeforeMutation)}`,\n );\n };\n\n if (resource !== undefined) {\n // updating existing resource\n\n if (resource.finalState)\n unexpectedTransitionError(\"resource state can\\t be updated after it is marked as final\");\n\n // updating resource version, even if it was not changed\n resource.version += 1;\n\n // duplicate / original\n if (resource.originalResourceId !== rd.originalResourceId) {\n if (resource.originalResourceId !== NullSignedResourceId)\n unexpectedTransitionError(\"originalResourceId can't change after it is set\");\n resource.originalResourceId = rd.originalResourceId;\n // duplicate status of the resource counts as ready for the external observer\n notEmpty(resource.resourceStateChange).markChanged(\n `originalResourceId changed for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.duplicatesResolved++;\n }\n\n // error\n if (isNotNullSignedResourceId(rd.error) && resource.error !== rd.error) {\n if (isNotNullSignedResourceId(resource.error))\n unexpectedTransitionError(\"resource can't change attached error after it is set\");\n resource.error = rd.error;\n incrementRefs.push(resource.error);\n notEmpty(resource.resourceStateChange).markChanged(\n `error changed for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.errorsAttached++;\n }\n\n // updating fields\n for (const fd of rd.fields) {\n let field = resource.fieldsMap.get(fd.name);\n\n if (!field) {\n // new field\n\n field = new PlTreeField(\n fd.name,\n fd.type,\n fd.value,\n fd.error,\n fd.status,\n fd.valueIsFinal,\n resource.version,\n );\n if (isNotNullSignedResourceId(fd.value)) incrementRefs.push(fd.value);\n if (isNotNullSignedResourceId(fd.error)) incrementRefs.push(fd.error);\n\n if (fd.type === \"Input\" || fd.type === \"Service\") {\n if (resource.inputsLocked)\n unexpectedTransitionError(\n `adding ${fd.type} (${fd.name}) field while inputs locked`,\n );\n notEmpty(resource.inputAndServiceFieldListChanged).markChanged(\n `new ${fd.type} field ${fd.name} added to ${resourceIdToString(resource.id)}`,\n );\n } else if (fd.type === \"Output\") {\n if (resource.outputsLocked)\n unexpectedTransitionError(\n `adding ${fd.type} (${fd.name}) field while outputs locked`,\n );\n notEmpty(resource.outputFieldListChanged).markChanged(\n `new ${fd.type} field ${fd.name} added to ${resourceIdToString(resource.id)}`,\n );\n } else {\n notEmpty(resource.dynamicFieldListChanged).markChanged(\n `new ${fd.type} field ${fd.name} added to ${resourceIdToString(resource.id)}`,\n );\n }\n\n resource.fieldsMap.set(fd.name, field);\n\n changed = true;\n metadataChanged = true;\n if (stat) stat.fieldsAdded++;\n } else {\n // change of old field\n\n // in principle this transition is possible, see assertions below\n if (field.type !== fd.type) {\n if (field.type !== \"Dynamic\")\n unexpectedTransitionError(`field changed type ${field.type} -> ${fd.type}`);\n notEmpty(resource.dynamicFieldListChanged).markChanged(\n `field ${fd.name} changed type from Dynamic to ${fd.type} in ${resourceIdToString(resource.id)}`,\n );\n if (field.type === \"Input\" || field.type === \"Service\") {\n if (resource.inputsLocked)\n unexpectedTransitionError(\n `adding input field \"${fd.name}\", while corresponding list is locked`,\n );\n notEmpty(resource.inputAndServiceFieldListChanged).markChanged(\n `field ${fd.name} changed to type ${fd.type} in ${resourceIdToString(resource.id)}`,\n );\n }\n if (field.type === \"Output\") {\n if (resource.outputsLocked)\n unexpectedTransitionError(\n `adding output field \"${fd.name}\", while corresponding list is locked`,\n );\n notEmpty(resource.outputFieldListChanged).markChanged(\n `field ${fd.name} changed to type ${fd.type} in ${resourceIdToString(resource.id)}`,\n );\n }\n field.type = fd.type;\n field.change.markChanged(\n `field ${fd.name} type changed to ${fd.type} in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n }\n\n // field value\n if (field.value !== fd.value) {\n if (isNotNullSignedResourceId(field.value)) decrementRefs.push(field.value);\n field.value = fd.value;\n if (isNotNullSignedResourceId(fd.value)) incrementRefs.push(fd.value);\n field.change.markChanged(\n `field ${fd.name} value changed in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.fieldsChanged++;\n }\n\n // field error\n if (field.error !== fd.error) {\n if (isNotNullSignedResourceId(field.error)) decrementRefs.push(field.error);\n field.error = fd.error;\n if (isNotNullSignedResourceId(fd.error)) incrementRefs.push(fd.error);\n field.change.markChanged(\n `field ${fd.name} error changed in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n }\n\n // field status\n if (field.status !== fd.status) {\n field.status = fd.status;\n field.change.markChanged(\n `field ${fd.name} status changed to ${fd.status} in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n }\n\n // field valueIsFinal flag\n if (field.valueIsFinal !== fd.valueIsFinal) {\n field.valueIsFinal = fd.valueIsFinal;\n field.change.markChanged(\n `field ${fd.name} valueIsFinal changed to ${fd.valueIsFinal} in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n }\n\n field.resourceVersion = resource.version;\n }\n }\n\n // detecting removed fields\n resource.fieldsMap.forEach((field, fieldName, fields) => {\n if (field.resourceVersion !== resource!.version) {\n if (field.type === \"Input\" || field.type === \"Service\" || field.type === \"Output\")\n unexpectedTransitionError(`removal of ${field.type} field ${fieldName}`);\n field.change.markChanged(\n `dynamic field ${fieldName} removed from ${resourceIdToString(resource!.id)}`,\n );\n fields.delete(fieldName);\n metadataChanged = true;\n if (stat) stat.fieldsRemoved++;\n\n if (isNotNullSignedResourceId(field.value)) decrementRefs.push(field.value);\n if (isNotNullSignedResourceId(field.error)) decrementRefs.push(field.error);\n\n notEmpty(resource!.dynamicFieldListChanged).markChanged(\n `dynamic field ${fieldName} removed from ${resourceIdToString(resource!.id)}`,\n );\n }\n });\n\n // inputsLocked\n if (resource.inputsLocked !== rd.inputsLocked) {\n if (resource.inputsLocked) unexpectedTransitionError(\"inputs unlocking is not permitted\");\n resource.inputsLocked = rd.inputsLocked;\n notEmpty(resource.lockedChange).markChanged(\n `inputs locked for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.locksChanged++;\n }\n\n // outputsLocked\n if (resource.outputsLocked !== rd.outputsLocked) {\n if (resource.outputsLocked)\n unexpectedTransitionError(\"outputs unlocking is not permitted\");\n resource.outputsLocked = rd.outputsLocked;\n notEmpty(resource.lockedChange).markChanged(\n `outputs locked for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.locksChanged++;\n }\n\n // ready flag\n if (resource.resourceReady !== rd.resourceReady) {\n const readyStateBefore = resource.resourceReady;\n resource.resourceReady = rd.resourceReady;\n resource.verifyReadyState();\n if (!resource.isReadyOrError)\n unexpectedTransitionError(\n `resource can't lose it's ready or error state (ready state before ${readyStateBefore})`,\n );\n notEmpty(resource.resourceStateChange).markChanged(\n `ready flag changed to ${rd.resourceReady} for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.readyFlips++;\n }\n\n // syncing kv\n for (const kv of rd.kv) {\n const current = resource.kv.get(kv.key);\n if (current === undefined) {\n resource.kv.set(kv.key, kv.value);\n notEmpty(resource.kvChangedPerKey).markChanged(\n kv.key,\n `kv added for ${resourceIdToString(resource.id)}: ${kv.key}`,\n );\n changed = true;\n if (stat) stat.kvChanged++;\n } else if (Buffer.compare(current, kv.value) !== 0) {\n resource.kv.set(kv.key, kv.value);\n notEmpty(resource.kvChangedPerKey).markChanged(\n kv.key,\n `kv changed for ${resourceIdToString(resource.id)}: ${kv.key}`,\n );\n changed = true;\n if (stat) stat.kvChanged++;\n }\n }\n\n if (resource.kv.size > rd.kv.length) {\n // only it this case it makes sense to check for deletions\n const newStateKeys = new Set(rd.kv.map((kv) => kv.key));\n\n // deleting keys not present in resource anymore\n resource.kv.forEach((_value, key, map) => {\n if (!newStateKeys.has(key)) {\n map.delete(key);\n notEmpty(resource!.kvChangedPerKey).markChanged(\n key,\n `kv deleted for ${resourceIdToString(resource!.id)}: ${key}`,\n );\n changed = true;\n if (stat) stat.kvChanged++;\n }\n });\n }\n\n if (changed) {\n // if resource was changed, updating resource data version\n resource.dataVersion = resource.version;\n if (this.isFinalPredicate(resource)) {\n resource.markFinal();\n if (stat) stat.resourcesMarkedFinal++;\n }\n }\n } else {\n // creating new resource\n\n resource = new PlTreeResource(rd);\n resource.verifyReadyState();\n if (isNotNullSignedResourceId(resource.error)) incrementRefs.push(resource.error);\n for (const fd of rd.fields) {\n const field = new PlTreeField(\n fd.name,\n fd.type,\n fd.value,\n fd.error,\n fd.status,\n fd.valueIsFinal,\n InitialResourceVersion,\n );\n if (isNotNullSignedResourceId(fd.value)) incrementRefs.push(fd.value);\n if (isNotNullSignedResourceId(fd.error)) incrementRefs.push(fd.error);\n resource.fieldsMap.set(fd.name, field);\n }\n\n // adding kv\n for (const kv of rd.kv) resource.kv.set(kv.key, kv.value);\n\n // checking that resource is final, and if so, marking it\n if (this.isFinalPredicate(resource)) resource.markFinal();\n\n // adding the resource to the heap\n this.resources.set(resource.id, resource);\n this.resourcesAdded.markChanged(`new resource ${resourceIdToString(resource.id)} added`);\n }\n\n if (stat) {\n if (!held) stat.resourcesNew++;\n else if (changed) {\n stat.resourcesChanged++;\n if (!metadataChanged) stat.metadataStableChanged++;\n } else {\n stat.resourcesUnchanged++;\n stat.bytesUnchanged += rd.data?.length ?? 0;\n for (const kv of rd.kv) stat.bytesUnchanged += kv.value.length;\n if (!stat.usedStreaming) stat.bfsRequestsWasted++;\n }\n }\n }\n\n // applying refCount increments\n for (const rid of incrementRefs) {\n const res = this.resources.get(rid);\n if (!res) {\n this.invalidateTree();\n throw new TreeStateUpdateError(`orphan resource ${rid}`);\n }\n res.refCount++;\n }\n\n // recursively applying refCount decrements / doing garbage collection\n this.collectGarbage(decrementRefs);\n\n // checking for orphans (maybe removed in the future)\n if (!allowOrphanInputs) {\n for (const rd of resourceData) {\n if (!this.resources.has(rd.id)) {\n this.invalidateTree();\n throw new TreeStateUpdateError(`orphan input resource ${rd.id}`);\n }\n }\n }\n }\n\n /** Runs the refcount-decrement garbage-collection cascade over the given worklist.\n * A resource reaching refCount 0 that is not a root is removed, and its outgoing\n * references are decremented in turn, until the worklist drains. Factored out of\n * {@link updateFromResourceData} so {@link setRoots} can reuse the same cascade. */\n private collectGarbage(decrementRefs: SignedResourceId[]) {\n let currentRefs = decrementRefs;\n while (currentRefs.length > 0) {\n const nextRefs: SignedResourceId[] = [];\n for (const rid of currentRefs) {\n const res = this.resources.get(rid);\n if (!res) {\n this.invalidateTree();\n throw new TreeStateUpdateError(`orphan resource ${rid}`);\n }\n res.refCount--;\n\n // garbage collection\n if (res.refCount === 0 && !this.roots.has(res.id)) {\n // removing fields\n res.fieldsMap.forEach((field) => {\n if (isNotNullSignedResourceId(field.value)) nextRefs.push(field.value);\n if (isNotNullSignedResourceId(field.error)) nextRefs.push(field.error);\n field.change.markChanged(\n `field ${field.name} removed during garbage collection of ${resourceIdToString(res.id)}`,\n );\n });\n if (isNotNullSignedResourceId(res.error)) nextRefs.push(res.error);\n res.resourceRemoved.markChanged(\n `resource removed during garbage collection: ${resourceIdToString(res.id)}`,\n );\n this.resources.delete(rid);\n }\n }\n currentRefs = nextRefs;\n }\n }\n\n /** Replaces the current root set with `newRoots`.\n *\n * Roots that leave the set lose their GC protection: each removed root present in the\n * heap is collected directly (a root sits at refCount 0, so it is never enqueued by an\n * ordinary decrement), and its subtree cascades to collection through the existing\n * refcount GC — the standard removal mechanic, applied to a root. Roots newly added to\n * the set are protected immediately; their resources arrive on the next refresh poll. */\n public setRoots(newRoots: Set<SignedResourceId>) {\n this.checkValid();\n\n const removed: SignedResourceId[] = [];\n for (const rid of this.roots) if (!newRoots.has(rid)) removed.push(rid);\n let added = false;\n for (const rid of newRoots) if (!this.roots.has(rid)) added = true;\n if (removed.length === 0 && !added) return; // no change\n\n // commit the new protected set before collecting, so a removed root is no longer\n // protected while its subtree cascades.\n this.roots.clear();\n for (const rid of newRoots) this.roots.add(rid);\n\n this.rootsChanged.markChanged(\"root set changed\");\n\n for (const rid of removed) {\n const res = this.resources.get(rid);\n if (res === undefined) continue; // root not yet materialized in the heap\n\n // if the (former) root is still referenced by another resource, dropping protection\n // is enough — ordinary refcounting keeps it alive and will collect it later.\n if (res.refCount > 0) continue;\n\n // collect the (now-unprotected) root itself and seed the cascade with the refs it holds\n const seed: SignedResourceId[] = [];\n res.fieldsMap.forEach((field) => {\n if (isNotNullSignedResourceId(field.value)) seed.push(field.value);\n if (isNotNullSignedResourceId(field.error)) seed.push(field.error);\n field.change.markChanged(\n `field ${field.name} removed after root ${resourceIdToString(res.id)} left the root set`,\n );\n });\n if (isNotNullSignedResourceId(res.error)) seed.push(res.error);\n res.resourceRemoved.markChanged(\n `resource removed after leaving the root set: ${resourceIdToString(res.id)}`,\n );\n this.resources.delete(rid);\n\n this.collectGarbage(seed);\n }\n }\n\n /** @deprecated use \"entry\" instead */\n public accessor(rid: SignedResourceId = this.root): PlTreeEntry {\n this.checkValid();\n return this.entry(rid);\n }\n\n public entry(rid: SignedResourceId = this.root): PlTreeEntry {\n this.checkValid();\n return new PlTreeEntry({ treeProvider: () => this }, rid);\n }\n\n public invalidateTree(msg?: string) {\n this._isValid = false;\n this.invalidationMessage = msg;\n this.rootsChanged.markChanged(\"tree invalidated\");\n this.resources.forEach((res) => {\n res.markAllChanged();\n });\n }\n\n public dumpState(): ExtendedResourceData[] {\n return Array.from(this.resources.values()).map((res) => res.extendedState);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"state.js","names":[],"sources":["../src/state.ts"],"sourcesContent":["import type {\n BasicResourceData,\n FieldData,\n FieldStatus,\n FieldType,\n KeyValue,\n OptionalSignedResourceId,\n ResourceData,\n SignedResourceId,\n ResourceKind,\n ResourceType,\n} from \"@milaboratories/pl-client\";\nimport {\n isNotNullSignedResourceId,\n isNullSignedResourceId,\n NullSignedResourceId,\n resourceIdToString,\n stringifyWithResourceId,\n} from \"@milaboratories/pl-client\";\nimport type { Watcher } from \"@milaboratories/computable\";\nimport { ChangeSource, KeyedChangeSource } from \"@milaboratories/computable\";\nimport { PlTreeEntry } from \"./accessors\";\nimport type { ValueAndError } from \"./value_and_error\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { cachedDecode, cachedDeserialize, notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { FieldTraversalStep, GetFieldStep } from \"./traversal_ops\";\nimport type { FinalResourceDataPredicate } from \"@milaboratories/pl-client\";\n\nexport type ExtendedResourceData = ResourceData & {\n kv: KeyValue[];\n};\n\nexport class TreeStateUpdateError extends Error {\n constructor(message: string) {\n super(message);\n }\n}\n\nclass PlTreeField implements FieldData {\n readonly change = new ChangeSource();\n\n constructor(\n public readonly name: string,\n public type: FieldType,\n public value: OptionalSignedResourceId,\n public error: OptionalSignedResourceId,\n public status: FieldStatus,\n public valueIsFinal: boolean,\n /** Last version of resource this field was observed, used to garbage collect fields in tree patching procedure */\n public resourceVersion: number,\n ) {}\n\n get state(): FieldData {\n return {\n name: this.name,\n type: this.type,\n status: this.status,\n value: this.value,\n error: this.error,\n valueIsFinal: this.valueIsFinal,\n };\n }\n}\n\nconst InitialResourceVersion = 0;\n\nexport type ResourceDataWithFinalState = ResourceData & {\n finalState: boolean;\n};\n\n/** Cross-cycle re-fetch duplication counters, populated by {@link PlTreeState.updateFromResourceData}.\n * Separates genuinely new/changed resources from redundant re-fetches of unchanged ones\n * (the delta-skip opportunity: intra-cycle dedup already happens in the loader, so all waste here\n * is cross-cycle). */\nexport interface ResourceUpdateStat {\n /** Resources seen for the first time in this update. */\n resourcesNew: number;\n /** Resources already held whose state actually changed. */\n resourcesChanged: number;\n /** Resources already held that came back unchanged: a pure duplicate re-fetch. */\n resourcesUnchanged: number;\n /** data + KV bytes of the unchanged bucket: wasted downlink. */\n bytesUnchanged: number;\n /** Changed resources whose immutable metadata (type, kind, field set) did not change,\n * i.e. metadata re-streamed even though only a value or flag flipped. */\n metadataStableChanged: number;\n /** Per-resource fetches spent on unchanged resources (BFS only; streaming re-sends them\n * inside one stream, so this stays 0). */\n bfsRequestsWasted: number;\n /** Whether the current load used backend streaming; set by the loader, read here to\n * attribute {@link bfsRequestsWasted}. */\n usedStreaming: boolean;\n // Breakdown of what changed among resourcesChanged (a resource can bump several).\n /** New fields appeared on a held resource. */\n fieldsAdded: number;\n /** Dynamic fields disappeared from a held resource. */\n fieldsRemoved: number;\n /** Existing field value pointer changed (a new value/result was attached). */\n fieldsChanged: number;\n /** KV entries added, changed, or deleted. */\n kvChanged: number;\n /** resourceReady flipped. */\n readyFlips: number;\n /** inputsLocked or outputsLocked transitioned. */\n locksChanged: number;\n /** An error resource was attached. */\n errorsAttached: number;\n /** originalResourceId resolved (duplicate -> original). */\n duplicatesResolved: number;\n /** Held resources that transitioned to final this update. */\n resourcesMarkedFinal: number;\n}\n\n/** Never store instances of this class, always get fresh instance from {@link PlTreeState} */\nexport class PlTreeResource implements ResourceDataWithFinalState {\n /** Tracks number of other resources referencing this resource. Used to perform garbage collection in tree patching procedure */\n refCount: number = 0;\n\n /** Increments each time resource is checked for difference with new state */\n version: number = InitialResourceVersion;\n /** Set to resource version when resource state, or it's fields have changed */\n dataVersion: number = InitialResourceVersion;\n\n readonly fieldsMap: Map<string, PlTreeField> = new Map();\n\n readonly kv = new Map<string, Uint8Array>();\n\n readonly resourceRemoved = new ChangeSource();\n\n // following change source are removed when resource is marked as final\n\n finalChanged? = new ChangeSource();\n\n resourceStateChange? = new ChangeSource();\n\n lockedChange? = new ChangeSource();\n inputAndServiceFieldListChanged? = new ChangeSource();\n outputFieldListChanged? = new ChangeSource();\n dynamicFieldListChanged? = new ChangeSource();\n\n // kvChangedGlobal? = new ChangeSource();\n kvChangedPerKey? = new KeyedChangeSource();\n\n readonly id: SignedResourceId;\n originalResourceId: OptionalSignedResourceId;\n\n readonly kind: ResourceKind;\n readonly type: ResourceType;\n\n readonly data?: Uint8Array;\n private dataAsString?: string;\n private dataAsJson?: unknown;\n\n error: OptionalSignedResourceId;\n\n inputsLocked: boolean;\n outputsLocked: boolean;\n resourceReady: boolean;\n finalFlag: boolean;\n\n /** Set externally by the tree, using {@link FinalResourceDataPredicate} */\n _finalState: boolean = false;\n\n private readonly logger?: MiLogger;\n\n constructor(initialState: BasicResourceData, logger?: MiLogger) {\n this.id = initialState.id;\n this.originalResourceId = initialState.originalResourceId;\n this.kind = initialState.kind;\n this.type = initialState.type;\n this.data = initialState.data;\n this.error = initialState.error;\n this.inputsLocked = initialState.inputsLocked;\n this.outputsLocked = initialState.outputsLocked;\n this.resourceReady = initialState.resourceReady;\n this.finalFlag = initialState.final;\n this.logger = logger;\n }\n\n // TODO add logging\n\n private info(msg: string) {\n if (this.logger !== undefined) this.logger.info(msg);\n }\n\n private warn(msg: string) {\n if (this.logger !== undefined) this.logger.warn(msg);\n }\n\n get final(): boolean {\n return this.finalFlag;\n }\n\n get finalState(): boolean {\n return this._finalState;\n }\n\n get fields(): FieldData[] {\n return [...this.fieldsMap.values()];\n }\n\n public getField(\n watcher: Watcher,\n _step:\n | (Omit<GetFieldStep, \"errorIfFieldNotFound\"> & { errorIfFieldNotFound: true })\n | (Omit<GetFieldStep, \"errorIfFieldNotSet\"> & { errorIfFieldNotSet: true }),\n onUnstable: (marker: string) => void,\n ): ValueAndError<SignedResourceId>;\n public getField(\n watcher: Watcher,\n _step: string | GetFieldStep,\n onUnstable: (marker: string) => void,\n ): ValueAndError<SignedResourceId> | undefined;\n public getField(\n watcher: Watcher,\n _step: string | GetFieldStep,\n onUnstable: (marker: string) => void = () => {},\n ): ValueAndError<SignedResourceId> | undefined {\n const step: FieldTraversalStep = typeof _step === \"string\" ? { field: _step } : _step;\n\n const field = this.fieldsMap.get(step.field);\n if (field === undefined) {\n if (step.errorIfFieldNotFound || step.errorIfFieldNotSet)\n throw new Error(\n `Field \"${step.field}\" not found in resource ${resourceIdToString(this.id)}`,\n );\n\n if (!this.inputsLocked) this.inputAndServiceFieldListChanged?.attachWatcher(watcher);\n else if (step.assertFieldType === \"Service\" || step.assertFieldType === \"Input\") {\n if (step.allowPermanentAbsence)\n // stable absence of field\n return undefined;\n else throw new Error(`Service or input field not found ${step.field}.`);\n }\n\n if (!this.outputsLocked) this.outputFieldListChanged?.attachWatcher(watcher);\n else if (step.assertFieldType === \"Output\") {\n if (step.allowPermanentAbsence)\n // stable absence of field\n return undefined;\n else throw new Error(`Output field not found ${step.field}.`);\n }\n\n this.dynamicFieldListChanged?.attachWatcher(watcher);\n if (!this._finalState && !step.stableIfNotFound) onUnstable(\"field_not_found:\" + step.field);\n\n return undefined;\n } else {\n if (step.assertFieldType !== undefined && field.type !== step.assertFieldType)\n throw new Error(\n `Unexpected field type: expected ${step.assertFieldType} but got ${field.type} for the field name ${step.field}`,\n );\n\n const ret = {} as ValueAndError<SignedResourceId>;\n if (isNotNullSignedResourceId(field.value)) ret.value = field.value;\n if (isNotNullSignedResourceId(field.error)) ret.error = field.error;\n if (ret.value === undefined && ret.error === undefined)\n // this method returns value and error of the field, thus those values are considered to be accessed;\n // any existing but not resolved field here is considered to be unstable, in the sense it is\n // considered to acquire some resolved value eventually\n onUnstable(\"field_not_resolved:\" + step.field);\n field.change.attachWatcher(watcher);\n return ret;\n }\n }\n\n public getInputsLocked(watcher: Watcher): boolean {\n if (!this.inputsLocked)\n // reverse transition can't happen, so there is no reason to wait for value to change\n this.resourceStateChange?.attachWatcher(watcher);\n return this.inputsLocked;\n }\n\n public getOutputsLocked(watcher: Watcher): boolean {\n if (!this.outputsLocked)\n // reverse transition can't happen, so there is no reason to wait for value to change\n this.resourceStateChange?.attachWatcher(watcher);\n return this.outputsLocked;\n }\n\n public get isReadyOrError(): boolean {\n return (\n this.error !== NullSignedResourceId ||\n this.resourceReady ||\n this.originalResourceId !== NullSignedResourceId\n );\n }\n\n public getIsFinal(watcher: Watcher): boolean {\n this.finalChanged?.attachWatcher(watcher);\n return this._finalState;\n }\n\n public getIsReadyOrError(watcher: Watcher): boolean {\n if (!this.isReadyOrError)\n // reverse transition can't happen, so there is no reason to wait for value to change if it is already true\n this.resourceStateChange?.attachWatcher(watcher);\n return this.isReadyOrError;\n }\n\n public getError(watcher: Watcher): SignedResourceId | undefined {\n if (isNullSignedResourceId(this.error)) {\n this.resourceStateChange?.attachWatcher(watcher);\n return undefined;\n } else {\n // reverse transition can't happen, so there is no reason to wait for value to change, if error already set\n return this.error;\n }\n }\n\n public listInputFields(watcher: Watcher): string[] {\n const ret: string[] = [];\n this.fieldsMap.forEach((field, name) => {\n if (field.type === \"Input\" || field.type === \"Service\") ret.push(name);\n });\n if (!this.inputsLocked) this.inputAndServiceFieldListChanged?.attachWatcher(watcher);\n\n return ret;\n }\n\n public listOutputFields(watcher: Watcher): string[] {\n const ret: string[] = [];\n this.fieldsMap.forEach((field, name) => {\n if (field.type === \"Output\") ret.push(name);\n });\n if (!this.outputsLocked) this.outputFieldListChanged?.attachWatcher(watcher);\n\n return ret;\n }\n\n public listDynamicFields(watcher: Watcher): string[] {\n const ret: string[] = [];\n this.fieldsMap.forEach((field, name) => {\n if (field.type !== \"Input\" && field.type !== \"Output\") ret.push(name);\n });\n this.dynamicFieldListChanged?.attachWatcher(watcher);\n\n return ret;\n }\n\n public getKeyValue(watcher: Watcher, key: string): Uint8Array | undefined {\n this.kvChangedPerKey?.attachWatcher(key, watcher);\n return this.kv.get(key);\n }\n\n public getKeyValueString(watcher: Watcher, key: string): string | undefined {\n const bytes = this.getKeyValue(watcher, key);\n if (bytes === undefined) return undefined;\n return cachedDecode(bytes);\n }\n\n public getKeyValueAsJson<T = unknown>(watcher: Watcher, key: string): T | undefined {\n const bytes = this.getKeyValue(watcher, key);\n if (bytes === undefined) return undefined;\n return cachedDeserialize(bytes);\n }\n\n public getDataAsString(): string | undefined {\n if (this.data === undefined) return undefined;\n if (this.dataAsString === undefined) this.dataAsString = cachedDecode(this.data);\n return this.dataAsString;\n }\n\n public getDataAsJson<T = unknown>(): T | undefined {\n if (this.data === undefined) return undefined;\n if (this.dataAsJson === undefined) this.dataAsJson = cachedDeserialize(this.data);\n return this.dataAsJson as T;\n }\n\n verifyReadyState() {\n if (this.resourceReady && !this.inputsLocked)\n throw new Error(\n `ready without input or output lock: ${stringifyWithResourceId(this.basicState)}`,\n );\n }\n\n get basicState(): BasicResourceData {\n return {\n id: this.id,\n kind: this.kind,\n type: this.type,\n data: this.data,\n resourceReady: this.resourceReady,\n inputsLocked: this.inputsLocked,\n outputsLocked: this.outputsLocked,\n error: this.error,\n originalResourceId: this.originalResourceId,\n final: this.finalFlag,\n };\n }\n\n get extendedState(): ExtendedResourceData {\n return {\n ...this.basicState,\n fields: this.fields,\n kv: Array.from(this.kv.entries()).map(([key, value]) => ({ key, value })),\n };\n }\n\n /** Called when {@link FinalResourceDataPredicate} returns true for the state. */\n markFinal() {\n if (this._finalState) return;\n\n this._finalState = true;\n notEmpty(this.finalChanged).markChanged(\"marked final\");\n this.finalChanged = undefined;\n this.resourceStateChange = undefined;\n this.dynamicFieldListChanged = undefined;\n this.inputAndServiceFieldListChanged = undefined;\n this.outputFieldListChanged = undefined;\n this.lockedChange = undefined;\n // this.kvChangedGlobal = undefined;\n this.kvChangedPerKey = undefined;\n }\n\n /** Used for invalidation */\n markAllChanged() {\n this.fieldsMap.forEach((field) => field.change.markChanged(\"marked all changed\"));\n this.finalChanged?.markChanged(\"marked all changed\");\n this.resourceStateChange?.markChanged(\"marked all changed\");\n this.lockedChange?.markChanged(\"marked all changed\");\n this.inputAndServiceFieldListChanged?.markChanged(\"marked all changed\");\n this.outputFieldListChanged?.markChanged(\"marked all changed\");\n this.dynamicFieldListChanged?.markChanged(\"marked all changed\");\n // this.kvChangedGlobal?.markChanged('marked all changed');\n this.kvChangedPerKey?.markAllChanged(\"marked all changed\");\n this.resourceRemoved.markChanged(\"marked all changed\");\n }\n}\n\nexport class PlTreeState {\n /** resource heap */\n private resources: Map<SignedResourceId, PlTreeResource> = new Map();\n private readonly resourcesAdded = new ChangeSource();\n /** Marked when the root set changes (roots discovered or removed), so multi-root\n * accessors that read the set recompute. */\n private readonly rootsChanged = new ChangeSource();\n /** Resets to false if any invalid state transitions are registered,\n * after that tree will produce errors for any read or write operations */\n private _isValid: boolean = true;\n private invalidationMessage?: string;\n\n /** The set of roots — resources protected from GC. Originally a single `root`,\n * generalized to a set so several explicit roots and dynamically-discovered shared\n * roots can coexist in one heap. Mutated through {@link setRoots}. */\n public readonly roots: Set<SignedResourceId>;\n\n constructor(\n /** Resources protected from GC. Accepts a single id (single-root, the original\n * contract) or a set of ids (multi-root). */\n roots: SignedResourceId | Set<SignedResourceId>,\n public readonly isFinalPredicate: FinalResourceDataPredicate,\n ) {\n this.roots = roots instanceof Set ? new Set(roots) : new Set([roots]);\n }\n\n /** Backward-compatible single-root accessor. Returns the sole root and THROWS if the\n * tree has zero or more than one root, so a multi-root tree can never be read as if it\n * had one. Mirrors the {@link entry} guard. */\n public get root(): SignedResourceId {\n if (this.roots.size !== 1)\n throw new Error(\n `single-root accessor used on a tree with ${this.roots.size} roots; use the multi-root API`,\n );\n return this.roots.values().next().value!;\n }\n\n public forEachResource(cb: (res: ResourceDataWithFinalState) => void): void {\n this.resources.forEach((v) => cb(v));\n }\n\n /** False once the tree has been invalidated (an inconsistent update, or termination of\n * its synchronization loop). {@link dumpState} deliberately reads through an invalid\n * tree, so anything persisting that dump must check this first: the contents of an\n * invalidated tree are not something to write to disk. */\n public get isValid(): boolean {\n return this._isValid;\n }\n\n private checkValid() {\n if (!this._isValid) throw new Error(this.invalidationMessage ?? \"tree is in invalid state\");\n }\n\n /** Reactive read of the current root set. Attaches the watcher to {@link rootsChanged}\n * so a Computable recomputes when discovered roots appear or disappear. */\n public getRoots(watcher: Watcher): SignedResourceId[] {\n this.checkValid();\n this.rootsChanged.attachWatcher(watcher);\n return [...this.roots];\n }\n\n public get(watcher: Watcher, rid: SignedResourceId): PlTreeResource {\n this.checkValid();\n const res = this.resources.get(rid);\n if (res === undefined) {\n // to make recovery from resource not found possible, considering some\n // race conditions, where computable is created before tree is updated\n this.resourcesAdded.attachWatcher(watcher);\n throw new Error(`resource ${resourceIdToString(rid)} not found in the tree`);\n }\n res.resourceRemoved.attachWatcher(watcher);\n return res;\n }\n\n updateFromResourceData(\n resourceData: ExtendedResourceData[],\n opts: { allowOrphanInputs?: boolean; stat?: ResourceUpdateStat } = {},\n ) {\n const { allowOrphanInputs = false, stat } = opts;\n this.checkValid();\n\n // All resources for which recount should be incremented, first are aggregated in this list\n const incrementRefs: SignedResourceId[] = [];\n const decrementRefs: SignedResourceId[] = [];\n\n // `resourcesAdded` is notified once, after the loop: markChanged detaches every watcher on\n // its first call and no watcher can attach in the middle of this synchronous loop, so a\n // single notification covers every resource added here. It also builds the marker string\n // just once, which is worth doing because resourceIdToString on a signed id parses a\n // BigInt and hex-decodes a Buffer.\n let addedCount = 0;\n let firstAdded: SignedResourceId | undefined;\n\n // Diagnostic context for unexpectedTransitionError, refreshed once per resource. It lives\n // out here so the loop body allocates neither a closure nor a snapshot per resource on the\n // path where nothing goes wrong. Only the mutable half of BasicResourceData needs\n // capturing: id, kind, type and data are readonly on PlTreeResource, so they are read back\n // from the resource when the message is built.\n let errRd: ExtendedResourceData;\n let errRes: PlTreeResource | undefined;\n let errOriginalResourceId: OptionalSignedResourceId = NullSignedResourceId;\n let errError: OptionalSignedResourceId = NullSignedResourceId;\n let errInputsLocked = false;\n let errOutputsLocked = false;\n let errResourceReady = false;\n let errFinal = false;\n const unexpectedTransitionError = (reason: string): never => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { fields, ...rdWithoutFields } = errRd;\n const statBeforeMutation: BasicResourceData | undefined =\n errRes === undefined\n ? undefined\n : {\n id: errRes.id,\n kind: errRes.kind,\n type: errRes.type,\n data: errRes.data,\n resourceReady: errResourceReady,\n inputsLocked: errInputsLocked,\n outputsLocked: errOutputsLocked,\n error: errError,\n originalResourceId: errOriginalResourceId,\n final: errFinal,\n };\n this.invalidateTree();\n throw new TreeStateUpdateError(\n `Unexpected resource state transition (${reason}): ${stringifyWithResourceId(\n rdWithoutFields,\n )} -> ${stringifyWithResourceId(statBeforeMutation)}`,\n );\n };\n\n // patching / creating resources\n for (const rd of resourceData) {\n let resource = this.resources.get(rd.id);\n const held = resource !== undefined;\n let changed = false;\n // Structural/metadata change (new or removed field). type and kind are readonly, so\n // they never change; this flag isolates value/flag-only changes from real metadata churn.\n let metadataChanged = false;\n\n errRd = rd;\n errRes = resource;\n\n if (resource !== undefined) {\n // updating existing resource\n errOriginalResourceId = resource.originalResourceId;\n errError = resource.error;\n errInputsLocked = resource.inputsLocked;\n errOutputsLocked = resource.outputsLocked;\n errResourceReady = resource.resourceReady;\n errFinal = resource.finalFlag;\n\n if (resource.finalState)\n unexpectedTransitionError(\"resource state can\\t be updated after it is marked as final\");\n\n // updating resource version, even if it was not changed\n resource.version += 1;\n\n // duplicate / original\n if (resource.originalResourceId !== rd.originalResourceId) {\n if (resource.originalResourceId !== NullSignedResourceId)\n unexpectedTransitionError(\"originalResourceId can't change after it is set\");\n resource.originalResourceId = rd.originalResourceId;\n // duplicate status of the resource counts as ready for the external observer\n notEmpty(resource.resourceStateChange).markChanged(\n `originalResourceId changed for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.duplicatesResolved++;\n }\n\n // error\n if (isNotNullSignedResourceId(rd.error) && resource.error !== rd.error) {\n if (isNotNullSignedResourceId(resource.error))\n unexpectedTransitionError(\"resource can't change attached error after it is set\");\n resource.error = rd.error;\n incrementRefs.push(resource.error);\n notEmpty(resource.resourceStateChange).markChanged(\n `error changed for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.errorsAttached++;\n }\n\n // updating fields.\n //\n // Fields normally arrive in the order they are already stored, so the stored ones are\n // walked in lockstep with the incoming ones and matched by direct name comparison\n // rather than through a fieldsMap lookup: every field name in a poll response is\n // freshly decoded from protobuf, so V8 has no cached hash for any of them.\n //\n // Any divergence (reordering, an insertion, a removal) abandons the walk, and the rest\n // of the resource resolves through fieldsMap.get, which is always correct. The walk is\n // abandoned before any fieldsMap.set, so it never observes an insertion made through\n // the live iterator.\n let walk: MapIterator<PlTreeField> | undefined =\n rd.fields.length > 0 ? resource.fieldsMap.values() : undefined;\n for (const fd of rd.fields) {\n let field: PlTreeField | undefined;\n if (walk !== undefined) {\n const next = walk.next();\n if (next.done === true || next.value.name !== fd.name) walk = undefined;\n else field = next.value;\n }\n if (field === undefined) field = resource.fieldsMap.get(fd.name);\n\n if (!field) {\n // new field\n\n field = new PlTreeField(\n fd.name,\n fd.type,\n fd.value,\n fd.error,\n fd.status,\n fd.valueIsFinal,\n resource.version,\n );\n if (isNotNullSignedResourceId(fd.value)) incrementRefs.push(fd.value);\n if (isNotNullSignedResourceId(fd.error)) incrementRefs.push(fd.error);\n\n if (fd.type === \"Input\" || fd.type === \"Service\") {\n if (resource.inputsLocked)\n unexpectedTransitionError(\n `adding ${fd.type} (${fd.name}) field while inputs locked`,\n );\n notEmpty(resource.inputAndServiceFieldListChanged).markChanged(\n `new ${fd.type} field ${fd.name} added to ${resourceIdToString(resource.id)}`,\n );\n } else if (fd.type === \"Output\") {\n if (resource.outputsLocked)\n unexpectedTransitionError(\n `adding ${fd.type} (${fd.name}) field while outputs locked`,\n );\n notEmpty(resource.outputFieldListChanged).markChanged(\n `new ${fd.type} field ${fd.name} added to ${resourceIdToString(resource.id)}`,\n );\n } else {\n notEmpty(resource.dynamicFieldListChanged).markChanged(\n `new ${fd.type} field ${fd.name} added to ${resourceIdToString(resource.id)}`,\n );\n }\n\n resource.fieldsMap.set(fd.name, field);\n\n changed = true;\n metadataChanged = true;\n if (stat) stat.fieldsAdded++;\n } else {\n // change of old field\n\n // in principle this transition is possible, see assertions below\n if (field.type !== fd.type) {\n if (field.type !== \"Dynamic\")\n unexpectedTransitionError(`field changed type ${field.type} -> ${fd.type}`);\n notEmpty(resource.dynamicFieldListChanged).markChanged(\n `field ${fd.name} changed type from Dynamic to ${fd.type} in ${resourceIdToString(resource.id)}`,\n );\n if (field.type === \"Input\" || field.type === \"Service\") {\n if (resource.inputsLocked)\n unexpectedTransitionError(\n `adding input field \"${fd.name}\", while corresponding list is locked`,\n );\n notEmpty(resource.inputAndServiceFieldListChanged).markChanged(\n `field ${fd.name} changed to type ${fd.type} in ${resourceIdToString(resource.id)}`,\n );\n }\n if (field.type === \"Output\") {\n if (resource.outputsLocked)\n unexpectedTransitionError(\n `adding output field \"${fd.name}\", while corresponding list is locked`,\n );\n notEmpty(resource.outputFieldListChanged).markChanged(\n `field ${fd.name} changed to type ${fd.type} in ${resourceIdToString(resource.id)}`,\n );\n }\n field.type = fd.type;\n field.change.markChanged(\n `field ${fd.name} type changed to ${fd.type} in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n }\n\n // field value\n if (field.value !== fd.value) {\n if (isNotNullSignedResourceId(field.value)) decrementRefs.push(field.value);\n field.value = fd.value;\n if (isNotNullSignedResourceId(fd.value)) incrementRefs.push(fd.value);\n field.change.markChanged(\n `field ${fd.name} value changed in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.fieldsChanged++;\n }\n\n // field error\n if (field.error !== fd.error) {\n if (isNotNullSignedResourceId(field.error)) decrementRefs.push(field.error);\n field.error = fd.error;\n if (isNotNullSignedResourceId(fd.error)) incrementRefs.push(fd.error);\n field.change.markChanged(\n `field ${fd.name} error changed in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n }\n\n // field status\n if (field.status !== fd.status) {\n field.status = fd.status;\n field.change.markChanged(\n `field ${fd.name} status changed to ${fd.status} in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n }\n\n // field valueIsFinal flag\n if (field.valueIsFinal !== fd.valueIsFinal) {\n field.valueIsFinal = fd.valueIsFinal;\n field.change.markChanged(\n `field ${fd.name} valueIsFinal changed to ${fd.valueIsFinal} in ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n }\n\n field.resourceVersion = resource.version;\n }\n }\n\n // detecting removed fields\n resource.fieldsMap.forEach((field, fieldName, fields) => {\n if (field.resourceVersion !== resource!.version) {\n if (field.type === \"Input\" || field.type === \"Service\" || field.type === \"Output\")\n unexpectedTransitionError(`removal of ${field.type} field ${fieldName}`);\n field.change.markChanged(\n `dynamic field ${fieldName} removed from ${resourceIdToString(resource!.id)}`,\n );\n fields.delete(fieldName);\n metadataChanged = true;\n if (stat) stat.fieldsRemoved++;\n\n if (isNotNullSignedResourceId(field.value)) decrementRefs.push(field.value);\n if (isNotNullSignedResourceId(field.error)) decrementRefs.push(field.error);\n\n notEmpty(resource!.dynamicFieldListChanged).markChanged(\n `dynamic field ${fieldName} removed from ${resourceIdToString(resource!.id)}`,\n );\n }\n });\n\n // inputsLocked\n if (resource.inputsLocked !== rd.inputsLocked) {\n if (resource.inputsLocked) unexpectedTransitionError(\"inputs unlocking is not permitted\");\n resource.inputsLocked = rd.inputsLocked;\n notEmpty(resource.lockedChange).markChanged(\n `inputs locked for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.locksChanged++;\n }\n\n // outputsLocked\n if (resource.outputsLocked !== rd.outputsLocked) {\n if (resource.outputsLocked)\n unexpectedTransitionError(\"outputs unlocking is not permitted\");\n resource.outputsLocked = rd.outputsLocked;\n notEmpty(resource.lockedChange).markChanged(\n `outputs locked for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.locksChanged++;\n }\n\n // ready flag\n if (resource.resourceReady !== rd.resourceReady) {\n const readyStateBefore = resource.resourceReady;\n resource.resourceReady = rd.resourceReady;\n resource.verifyReadyState();\n if (!resource.isReadyOrError)\n unexpectedTransitionError(\n `resource can't lose it's ready or error state (ready state before ${readyStateBefore})`,\n );\n notEmpty(resource.resourceStateChange).markChanged(\n `ready flag changed to ${rd.resourceReady} for ${resourceIdToString(resource.id)}`,\n );\n changed = true;\n if (stat) stat.readyFlips++;\n }\n\n // syncing kv. Same lockstep walk as the fields above, for the same reason: kv keys\n // arrive in a stable order and are freshly decoded strings. The walk is only set up\n // when there is something to compare against, so a resource with no kv allocates no\n // iterator.\n let kvWalk: MapIterator<[string, Uint8Array]> | undefined =\n rd.kv.length > 0 ? resource.kv.entries() : undefined;\n for (const kv of rd.kv) {\n let current: Uint8Array | undefined;\n if (kvWalk !== undefined) {\n const next = kvWalk.next();\n if (next.done === true || next.value[0] !== kv.key) kvWalk = undefined;\n else current = next.value[1];\n }\n if (current === undefined) current = resource.kv.get(kv.key);\n if (current === undefined) {\n resource.kv.set(kv.key, kv.value);\n kvWalk = undefined;\n notEmpty(resource.kvChangedPerKey).markChanged(\n kv.key,\n `kv added for ${resourceIdToString(resource.id)}: ${kv.key}`,\n );\n changed = true;\n if (stat) stat.kvChanged++;\n } else if (Buffer.compare(current, kv.value) !== 0) {\n resource.kv.set(kv.key, kv.value);\n notEmpty(resource.kvChangedPerKey).markChanged(\n kv.key,\n `kv changed for ${resourceIdToString(resource.id)}: ${kv.key}`,\n );\n changed = true;\n if (stat) stat.kvChanged++;\n }\n }\n\n if (resource.kv.size > rd.kv.length) {\n // only it this case it makes sense to check for deletions\n const newStateKeys = new Set(rd.kv.map((kv) => kv.key));\n\n // deleting keys not present in resource anymore\n resource.kv.forEach((_value, key, map) => {\n if (!newStateKeys.has(key)) {\n map.delete(key);\n notEmpty(resource!.kvChangedPerKey).markChanged(\n key,\n `kv deleted for ${resourceIdToString(resource!.id)}: ${key}`,\n );\n changed = true;\n if (stat) stat.kvChanged++;\n }\n });\n }\n\n if (changed) {\n // if resource was changed, updating resource data version\n resource.dataVersion = resource.version;\n if (this.isFinalPredicate(resource)) {\n resource.markFinal();\n if (stat) stat.resourcesMarkedFinal++;\n }\n }\n } else {\n // creating new resource\n\n resource = new PlTreeResource(rd);\n resource.verifyReadyState();\n if (isNotNullSignedResourceId(resource.error)) incrementRefs.push(resource.error);\n for (const fd of rd.fields) {\n const field = new PlTreeField(\n fd.name,\n fd.type,\n fd.value,\n fd.error,\n fd.status,\n fd.valueIsFinal,\n InitialResourceVersion,\n );\n if (isNotNullSignedResourceId(fd.value)) incrementRefs.push(fd.value);\n if (isNotNullSignedResourceId(fd.error)) incrementRefs.push(fd.error);\n resource.fieldsMap.set(fd.name, field);\n }\n\n // adding kv\n for (const kv of rd.kv) resource.kv.set(kv.key, kv.value);\n\n // checking that resource is final, and if so, marking it\n if (this.isFinalPredicate(resource)) resource.markFinal();\n\n // adding the resource to the heap\n this.resources.set(resource.id, resource);\n if (addedCount === 0) firstAdded = resource.id;\n addedCount++;\n }\n\n if (stat) {\n if (!held) stat.resourcesNew++;\n else if (changed) {\n stat.resourcesChanged++;\n if (!metadataChanged) stat.metadataStableChanged++;\n } else {\n stat.resourcesUnchanged++;\n stat.bytesUnchanged += rd.data?.length ?? 0;\n for (const kv of rd.kv) stat.bytesUnchanged += kv.value.length;\n if (!stat.usedStreaming) stat.bfsRequestsWasted++;\n }\n }\n }\n\n if (firstAdded !== undefined)\n this.resourcesAdded.markChanged(\n addedCount === 1\n ? `new resource ${resourceIdToString(firstAdded)} added`\n : `${addedCount} new resources added, first ${resourceIdToString(firstAdded)}`,\n );\n\n // applying refCount increments\n for (const rid of incrementRefs) {\n const res = this.resources.get(rid);\n if (!res) {\n this.invalidateTree();\n throw new TreeStateUpdateError(`orphan resource ${rid}`);\n }\n res.refCount++;\n }\n\n // recursively applying refCount decrements / doing garbage collection\n this.collectGarbage(decrementRefs);\n\n // checking for orphans (maybe removed in the future)\n if (!allowOrphanInputs) {\n for (const rd of resourceData) {\n if (!this.resources.has(rd.id)) {\n this.invalidateTree();\n throw new TreeStateUpdateError(`orphan input resource ${rd.id}`);\n }\n }\n }\n }\n\n /** Runs the refcount-decrement garbage-collection cascade over the given worklist.\n * A resource reaching refCount 0 that is not a root is removed, and its outgoing\n * references are decremented in turn, until the worklist drains. Factored out of\n * {@link updateFromResourceData} so {@link setRoots} can reuse the same cascade. */\n private collectGarbage(decrementRefs: SignedResourceId[]) {\n let currentRefs = decrementRefs;\n while (currentRefs.length > 0) {\n const nextRefs: SignedResourceId[] = [];\n for (const rid of currentRefs) {\n const res = this.resources.get(rid);\n if (!res) {\n this.invalidateTree();\n throw new TreeStateUpdateError(`orphan resource ${rid}`);\n }\n res.refCount--;\n\n // garbage collection\n if (res.refCount === 0 && !this.roots.has(res.id)) {\n // removing fields\n res.fieldsMap.forEach((field) => {\n if (isNotNullSignedResourceId(field.value)) nextRefs.push(field.value);\n if (isNotNullSignedResourceId(field.error)) nextRefs.push(field.error);\n field.change.markChanged(\n `field ${field.name} removed during garbage collection of ${resourceIdToString(res.id)}`,\n );\n });\n if (isNotNullSignedResourceId(res.error)) nextRefs.push(res.error);\n res.resourceRemoved.markChanged(\n `resource removed during garbage collection: ${resourceIdToString(res.id)}`,\n );\n this.resources.delete(rid);\n }\n }\n currentRefs = nextRefs;\n }\n }\n\n /** Replaces the current root set with `newRoots`.\n *\n * Roots that leave the set lose their GC protection: each removed root present in the\n * heap is collected directly (a root sits at refCount 0, so it is never enqueued by an\n * ordinary decrement), and its subtree cascades to collection through the existing\n * refcount GC — the standard removal mechanic, applied to a root. Roots newly added to\n * the set are protected immediately; their resources arrive on the next refresh poll. */\n public setRoots(newRoots: Set<SignedResourceId>) {\n this.checkValid();\n\n const removed: SignedResourceId[] = [];\n for (const rid of this.roots) if (!newRoots.has(rid)) removed.push(rid);\n let added = false;\n for (const rid of newRoots) if (!this.roots.has(rid)) added = true;\n if (removed.length === 0 && !added) return; // no change\n\n // commit the new protected set before collecting, so a removed root is no longer\n // protected while its subtree cascades.\n this.roots.clear();\n for (const rid of newRoots) this.roots.add(rid);\n\n this.rootsChanged.markChanged(\"root set changed\");\n\n for (const rid of removed) {\n const res = this.resources.get(rid);\n if (res === undefined) continue; // root not yet materialized in the heap\n\n // if the (former) root is still referenced by another resource, dropping protection\n // is enough — ordinary refcounting keeps it alive and will collect it later.\n if (res.refCount > 0) continue;\n\n // collect the (now-unprotected) root itself and seed the cascade with the refs it holds\n const seed: SignedResourceId[] = [];\n res.fieldsMap.forEach((field) => {\n if (isNotNullSignedResourceId(field.value)) seed.push(field.value);\n if (isNotNullSignedResourceId(field.error)) seed.push(field.error);\n field.change.markChanged(\n `field ${field.name} removed after root ${resourceIdToString(res.id)} left the root set`,\n );\n });\n if (isNotNullSignedResourceId(res.error)) seed.push(res.error);\n res.resourceRemoved.markChanged(\n `resource removed after leaving the root set: ${resourceIdToString(res.id)}`,\n );\n this.resources.delete(rid);\n\n this.collectGarbage(seed);\n }\n }\n\n /** @deprecated use \"entry\" instead */\n public accessor(rid: SignedResourceId = this.root): PlTreeEntry {\n this.checkValid();\n return this.entry(rid);\n }\n\n public entry(rid: SignedResourceId = this.root): PlTreeEntry {\n this.checkValid();\n return new PlTreeEntry({ treeProvider: () => this }, rid);\n }\n\n public invalidateTree(msg?: string) {\n this._isValid = false;\n this.invalidationMessage = msg;\n this.rootsChanged.markChanged(\"tree invalidated\");\n this.resources.forEach((res) => {\n res.markAllChanged();\n });\n }\n\n public dumpState(): ExtendedResourceData[] {\n return Array.from(this.resources.values()).map((res) => res.extendedState);\n }\n}\n"],"mappings":"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"}
package/dist/sync.cjs CHANGED
@@ -247,7 +247,7 @@ async function processResourceTreeStream(treeItems, finalResources, pruningFunct
247
247
  }
248
248
  async function loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) {
249
249
  const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } = loadingRequest;
250
- const { result, followUpSeeds } = await processResourceTreeStream(tx.resourceTree(seedResources, {
250
+ let { result, followUpSeeds } = await processResourceTreeStream(tx.resourceTree(seedResources, {
251
251
  includeKv: true,
252
252
  fieldFilter,
253
253
  traverseStopRules
@@ -266,7 +266,7 @@ async function loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) {
266
266
  includeKv: true,
267
267
  fieldFilter
268
268
  }), finalResources, pruningFunction, stats);
269
- result.push(...followUpResult);
269
+ result = result.concat(followUpResult);
270
270
  if (stats) {
271
271
  logger?.info?.(`loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`);
272
272
  stats.roundTrips++;
package/dist/sync.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sync.cjs","names":["ConcurrencyLimitingExecutor","Denque","loadDeltaTreeState"],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n Filter,\n OptionalSignedResourceId,\n PlTransaction,\n ResourceTreeFrame,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { hasCapability, isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** Applied to each resource field list in fallback BFS mode and to streamed results. */\n readonly pruningFunction?: PruningFunction;\n\n /** ResourceTree field filter passed to the backend when supported. */\n readonly fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules passed to the backend when supported.\n * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v1`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\n * - `\"client-bfs\"`: always use client-side BFS, even on capable backends.\n * - `\"backend-streaming\"`: always prefer backend streaming; if the capability is absent,\n * logs a warning and falls back to BFS (never throws).\n * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v1` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v1 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n /** Stop-marker frames whose id was already final locally and were skipped. */\n stopMarkersSkipped: number;\n /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */\n stopMarkerFollowUpRoundTrips: number;\n /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n stopMarkersSkipped: 0,\n stopMarkerFollowUpRoundTrips: 0,\n streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v1\");\n}\n\nexport function collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {\n if (!stats) return;\n stats.retrievedResources++;\n stats.retrievedFields += resource.fields.length;\n stats.retrievedKeyValues += resource.kv.length;\n stats.retrievedResourceDataBytes += resource.data?.length ?? 0;\n for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n}\n\nasync function loadTreeStateViaBfs(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n if (stats) stats.bfsResourcesRequested++;\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n collectStatsForResource(nextResource, stats);\n\n // aggregating the state\n result.push(nextResource);\n }\n\n if (stats) stats.roundTrips += numberOfRoundTrips;\n\n return result;\n}\n\nasync function processResourceTreeStream(\n treeItems: AsyncIterable<ResourceTreeFrame>,\n finalResources: Set<SignedResourceId>,\n pruningFunction: PruningFunction | undefined,\n stats: TreeLoadingStat | undefined,\n): Promise<{ result: ExtendedResourceData[]; followUpSeeds: SignedResourceId[] }> {\n const result: ExtendedResourceData[] = [];\n const followUpSeeds: SignedResourceId[] = [];\n\n // backend returns two types of frames:\n // - 'resource' frames contain the resource state and are processed normally\n // - 'stopMarker' frames indicate resources that are ignored due stop rules fired\n //\n // Usually stop rules indicates the resources with final state. In that case middle layer\n // should make a decision: has it already loaded the resource or should it be requested for get the latest state?\n for await (const frame of treeItems) {\n if (frame.frameKind === \"stopMarker\") {\n if (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n let nextResource: ExtendedResourceData = {\n id: frame.id,\n type: frame.type,\n kind: frame.kind,\n data: frame.data,\n resourceReady: frame.resourceReady,\n error: frame.error,\n originalResourceId: frame.originalResourceId,\n // traverseWasStopped: backend matched traverse stop rules — children were not streamed.\n // Mark as terminal; fields are resolved below.\n final: frame.final || frame.traverseWasStopped,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields: frame.fields,\n kv: frame.kv,\n };\n\n // Apply field rules: traverseWasStopped drops all fields to keep the refCount\n // invariant; pruning function further filters the remaining fields.\n const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;\n const resolvedFields =\n pruningFunction !== undefined\n ? pruningFunction({ ...nextResource, fields: rawFields })\n : rawFields;\n if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;\n nextResource = { ...nextResource, fields: resolvedFields };\n\n collectStatsForResource(nextResource, stats);\n result.push(nextResource);\n }\n\n return { result, followUpSeeds };\n}\n\nasync function loadTreeStateViaResourceTree(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } =\n loadingRequest;\n\n // Round 0: initial tree traversal.\n const treeItems = tx.resourceTree(seedResources, {\n includeKv: true,\n fieldFilter,\n traverseStopRules,\n });\n\n const { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n result.push(...followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\n }\n\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n capabilities: readonly string[] = [],\n mode: TraversalMode = \"auto\",\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const startTimestamp = Date.now();\n if (stats) stats.requests++;\n\n try {\n // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sync.cjs","names":["ConcurrencyLimitingExecutor","Denque","loadDeltaTreeState"],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n Filter,\n OptionalSignedResourceId,\n PlTransaction,\n ResourceTreeFrame,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { hasCapability, isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** Applied to each resource field list in fallback BFS mode and to streamed results. */\n readonly pruningFunction?: PruningFunction;\n\n /** ResourceTree field filter passed to the backend when supported. */\n readonly fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules passed to the backend when supported.\n * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v1`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\n * - `\"client-bfs\"`: always use client-side BFS, even on capable backends.\n * - `\"backend-streaming\"`: always prefer backend streaming; if the capability is absent,\n * logs a warning and falls back to BFS (never throws).\n * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v1` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v1 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n /** Stop-marker frames whose id was already final locally and were skipped. */\n stopMarkersSkipped: number;\n /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */\n stopMarkerFollowUpRoundTrips: number;\n /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n stopMarkersSkipped: 0,\n stopMarkerFollowUpRoundTrips: 0,\n streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v1\");\n}\n\nexport function collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {\n if (!stats) return;\n stats.retrievedResources++;\n stats.retrievedFields += resource.fields.length;\n stats.retrievedKeyValues += resource.kv.length;\n stats.retrievedResourceDataBytes += resource.data?.length ?? 0;\n for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n}\n\nasync function loadTreeStateViaBfs(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n if (stats) stats.bfsResourcesRequested++;\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n collectStatsForResource(nextResource, stats);\n\n // aggregating the state\n result.push(nextResource);\n }\n\n if (stats) stats.roundTrips += numberOfRoundTrips;\n\n return result;\n}\n\nasync function processResourceTreeStream(\n treeItems: AsyncIterable<ResourceTreeFrame>,\n finalResources: Set<SignedResourceId>,\n pruningFunction: PruningFunction | undefined,\n stats: TreeLoadingStat | undefined,\n): Promise<{ result: ExtendedResourceData[]; followUpSeeds: SignedResourceId[] }> {\n const result: ExtendedResourceData[] = [];\n const followUpSeeds: SignedResourceId[] = [];\n\n // backend returns two types of frames:\n // - 'resource' frames contain the resource state and are processed normally\n // - 'stopMarker' frames indicate resources that are ignored due stop rules fired\n //\n // Usually stop rules indicates the resources with final state. In that case middle layer\n // should make a decision: has it already loaded the resource or should it be requested for get the latest state?\n for await (const frame of treeItems) {\n if (frame.frameKind === \"stopMarker\") {\n if (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n let nextResource: ExtendedResourceData = {\n id: frame.id,\n type: frame.type,\n kind: frame.kind,\n data: frame.data,\n resourceReady: frame.resourceReady,\n error: frame.error,\n originalResourceId: frame.originalResourceId,\n // traverseWasStopped: backend matched traverse stop rules — children were not streamed.\n // Mark as terminal; fields are resolved below.\n final: frame.final || frame.traverseWasStopped,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields: frame.fields,\n kv: frame.kv,\n };\n\n // Apply field rules: traverseWasStopped drops all fields to keep the refCount\n // invariant; pruning function further filters the remaining fields.\n const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;\n const resolvedFields =\n pruningFunction !== undefined\n ? pruningFunction({ ...nextResource, fields: rawFields })\n : rawFields;\n if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;\n nextResource = { ...nextResource, fields: resolvedFields };\n\n collectStatsForResource(nextResource, stats);\n result.push(nextResource);\n }\n\n return { result, followUpSeeds };\n}\n\nasync function loadTreeStateViaResourceTree(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } =\n loadingRequest;\n\n // Round 0: initial tree traversal.\n const treeItems = tx.resourceTree(seedResources, {\n includeKv: true,\n fieldFilter,\n traverseStopRules,\n });\n\n let { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n // spreading fails due to exceeding stack argument size\n result = result.concat(followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\n }\n\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n capabilities: readonly string[] = [],\n mode: TraversalMode = \"auto\",\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const startTimestamp = Date.now();\n if (stats) stats.requests++;\n\n try {\n // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;YAeY,mBAAmB,UAAU,yBAAyB;iBAEjD;;;;WAIN,eAAe;;;;WAKf,gBAAgB,IAAI;;WAGpB,kBAAkB;;WAGlB,cAAc;;;WAId,oBAAoB;;;WAIpB,gBAAgB;;;WAIhB,gBAAgB,YAAY;;;WAI5B,oBAAoB;;;;;;;;;;;;YAanB;;;YAIA;;;;wBAKI,4BACd,MAAM,eACN,kCACA;EAAW,OAAO;IACjB;;;wBA6Ba,4BACd,MAAM,aACN,UAAS,KACP,qGAGD;YAwBS,kBAAkB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;;;EAGA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;EAGA;;;EAGA;;;;EAIA;;wBAGc,0BAA0B;wBA2C1B,sBAAsB,MAAM;wBAyB5B,8BAA8B;wBAQ9B,wBAAwB,UAAU,sBAAsB,QAAQ;;;;wBA0P1D,cACpB,IAAI,eACJ,gBAAgB,oBAChB,QAAQ,iBACR,kCACA,OAAM,eACN;EAAW,OAAO;EAAsB,QAAQ;IAC/C,QAAQ"}
1
+ {"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;YAeY,mBAAmB,UAAU,yBAAyB;iBAEjD;;;;WAIN,eAAe;;;;WAKf,gBAAgB,IAAI;;WAGpB,kBAAkB;;WAGlB,cAAc;;;WAId,oBAAoB;;;WAIpB,gBAAgB;;;WAIhB,gBAAgB,YAAY;;;WAI5B,oBAAoB;;;;;;;;;;;;YAanB;;;YAIA;;;;wBAKI,4BACd,MAAM,eACN,kCACA;EAAW,OAAO;IACjB;;;wBA6Ba,4BACd,MAAM,aACN,UAAS,KACP,qGAGD;YAwBS,kBAAkB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;;;EAGA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;EAGA;;;EAGA;;;;EAIA;;wBAGc,0BAA0B;wBA2C1B,sBAAsB,MAAM;wBAyB5B,8BAA8B;wBAQ9B,wBAAwB,UAAU,sBAAsB,QAAQ;;;;wBA2P1D,cACpB,IAAI,eACJ,gBAAgB,oBAChB,QAAQ,iBACR,kCACA,OAAM,eACN;EAAW,OAAO;EAAsB,QAAQ;IAC/C,QAAQ"}
package/dist/sync.js CHANGED
@@ -245,7 +245,7 @@ async function processResourceTreeStream(treeItems, finalResources, pruningFunct
245
245
  }
246
246
  async function loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) {
247
247
  const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } = loadingRequest;
248
- const { result, followUpSeeds } = await processResourceTreeStream(tx.resourceTree(seedResources, {
248
+ let { result, followUpSeeds } = await processResourceTreeStream(tx.resourceTree(seedResources, {
249
249
  includeKv: true,
250
250
  fieldFilter,
251
251
  traverseStopRules
@@ -264,7 +264,7 @@ async function loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) {
264
264
  includeKv: true,
265
265
  fieldFilter
266
266
  }), finalResources, pruningFunction, stats);
267
- result.push(...followUpResult);
267
+ result = result.concat(followUpResult);
268
268
  if (stats) {
269
269
  logger?.info?.(`loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`);
270
270
  stats.roundTrips++;
package/dist/sync.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","names":[],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n Filter,\n OptionalSignedResourceId,\n PlTransaction,\n ResourceTreeFrame,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { hasCapability, isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** Applied to each resource field list in fallback BFS mode and to streamed results. */\n readonly pruningFunction?: PruningFunction;\n\n /** ResourceTree field filter passed to the backend when supported. */\n readonly fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules passed to the backend when supported.\n * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v1`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\n * - `\"client-bfs\"`: always use client-side BFS, even on capable backends.\n * - `\"backend-streaming\"`: always prefer backend streaming; if the capability is absent,\n * logs a warning and falls back to BFS (never throws).\n * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v1` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v1 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n /** Stop-marker frames whose id was already final locally and were skipped. */\n stopMarkersSkipped: number;\n /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */\n stopMarkerFollowUpRoundTrips: number;\n /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n stopMarkersSkipped: 0,\n stopMarkerFollowUpRoundTrips: 0,\n streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v1\");\n}\n\nexport function collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {\n if (!stats) return;\n stats.retrievedResources++;\n stats.retrievedFields += resource.fields.length;\n stats.retrievedKeyValues += resource.kv.length;\n stats.retrievedResourceDataBytes += resource.data?.length ?? 0;\n for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n}\n\nasync function loadTreeStateViaBfs(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n if (stats) stats.bfsResourcesRequested++;\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n collectStatsForResource(nextResource, stats);\n\n // aggregating the state\n result.push(nextResource);\n }\n\n if (stats) stats.roundTrips += numberOfRoundTrips;\n\n return result;\n}\n\nasync function processResourceTreeStream(\n treeItems: AsyncIterable<ResourceTreeFrame>,\n finalResources: Set<SignedResourceId>,\n pruningFunction: PruningFunction | undefined,\n stats: TreeLoadingStat | undefined,\n): Promise<{ result: ExtendedResourceData[]; followUpSeeds: SignedResourceId[] }> {\n const result: ExtendedResourceData[] = [];\n const followUpSeeds: SignedResourceId[] = [];\n\n // backend returns two types of frames:\n // - 'resource' frames contain the resource state and are processed normally\n // - 'stopMarker' frames indicate resources that are ignored due stop rules fired\n //\n // Usually stop rules indicates the resources with final state. In that case middle layer\n // should make a decision: has it already loaded the resource or should it be requested for get the latest state?\n for await (const frame of treeItems) {\n if (frame.frameKind === \"stopMarker\") {\n if (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n let nextResource: ExtendedResourceData = {\n id: frame.id,\n type: frame.type,\n kind: frame.kind,\n data: frame.data,\n resourceReady: frame.resourceReady,\n error: frame.error,\n originalResourceId: frame.originalResourceId,\n // traverseWasStopped: backend matched traverse stop rules — children were not streamed.\n // Mark as terminal; fields are resolved below.\n final: frame.final || frame.traverseWasStopped,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields: frame.fields,\n kv: frame.kv,\n };\n\n // Apply field rules: traverseWasStopped drops all fields to keep the refCount\n // invariant; pruning function further filters the remaining fields.\n const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;\n const resolvedFields =\n pruningFunction !== undefined\n ? pruningFunction({ ...nextResource, fields: rawFields })\n : rawFields;\n if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;\n nextResource = { ...nextResource, fields: resolvedFields };\n\n collectStatsForResource(nextResource, stats);\n result.push(nextResource);\n }\n\n return { result, followUpSeeds };\n}\n\nasync function loadTreeStateViaResourceTree(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } =\n loadingRequest;\n\n // Round 0: initial tree traversal.\n const treeItems = tx.resourceTree(seedResources, {\n includeKv: true,\n fieldFilter,\n traverseStopRules,\n });\n\n const { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n result.push(...followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\n }\n\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n capabilities: readonly string[] = [],\n mode: TraversalMode = \"auto\",\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const startTimestamp = Date.now();\n if (stats) stats.requests++;\n\n try {\n // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sync.js","names":[],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n Filter,\n OptionalSignedResourceId,\n PlTransaction,\n ResourceTreeFrame,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { hasCapability, isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** Applied to each resource field list in fallback BFS mode and to streamed results. */\n readonly pruningFunction?: PruningFunction;\n\n /** ResourceTree field filter passed to the backend when supported. */\n readonly fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules passed to the backend when supported.\n * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v1`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\n * - `\"client-bfs\"`: always use client-side BFS, even on capable backends.\n * - `\"backend-streaming\"`: always prefer backend streaming; if the capability is absent,\n * logs a warning and falls back to BFS (never throws).\n * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v1` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v1 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n /** Stop-marker frames whose id was already final locally and were skipped. */\n stopMarkersSkipped: number;\n /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */\n stopMarkerFollowUpRoundTrips: number;\n /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n stopMarkersSkipped: 0,\n stopMarkerFollowUpRoundTrips: 0,\n streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v1\");\n}\n\nexport function collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {\n if (!stats) return;\n stats.retrievedResources++;\n stats.retrievedFields += resource.fields.length;\n stats.retrievedKeyValues += resource.kv.length;\n stats.retrievedResourceDataBytes += resource.data?.length ?? 0;\n for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n}\n\nasync function loadTreeStateViaBfs(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n if (stats) stats.bfsResourcesRequested++;\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n collectStatsForResource(nextResource, stats);\n\n // aggregating the state\n result.push(nextResource);\n }\n\n if (stats) stats.roundTrips += numberOfRoundTrips;\n\n return result;\n}\n\nasync function processResourceTreeStream(\n treeItems: AsyncIterable<ResourceTreeFrame>,\n finalResources: Set<SignedResourceId>,\n pruningFunction: PruningFunction | undefined,\n stats: TreeLoadingStat | undefined,\n): Promise<{ result: ExtendedResourceData[]; followUpSeeds: SignedResourceId[] }> {\n const result: ExtendedResourceData[] = [];\n const followUpSeeds: SignedResourceId[] = [];\n\n // backend returns two types of frames:\n // - 'resource' frames contain the resource state and are processed normally\n // - 'stopMarker' frames indicate resources that are ignored due stop rules fired\n //\n // Usually stop rules indicates the resources with final state. In that case middle layer\n // should make a decision: has it already loaded the resource or should it be requested for get the latest state?\n for await (const frame of treeItems) {\n if (frame.frameKind === \"stopMarker\") {\n if (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n let nextResource: ExtendedResourceData = {\n id: frame.id,\n type: frame.type,\n kind: frame.kind,\n data: frame.data,\n resourceReady: frame.resourceReady,\n error: frame.error,\n originalResourceId: frame.originalResourceId,\n // traverseWasStopped: backend matched traverse stop rules — children were not streamed.\n // Mark as terminal; fields are resolved below.\n final: frame.final || frame.traverseWasStopped,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields: frame.fields,\n kv: frame.kv,\n };\n\n // Apply field rules: traverseWasStopped drops all fields to keep the refCount\n // invariant; pruning function further filters the remaining fields.\n const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;\n const resolvedFields =\n pruningFunction !== undefined\n ? pruningFunction({ ...nextResource, fields: rawFields })\n : rawFields;\n if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;\n nextResource = { ...nextResource, fields: resolvedFields };\n\n collectStatsForResource(nextResource, stats);\n result.push(nextResource);\n }\n\n return { result, followUpSeeds };\n}\n\nasync function loadTreeStateViaResourceTree(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } =\n loadingRequest;\n\n // Round 0: initial tree traversal.\n const treeItems = tx.resourceTree(seedResources, {\n includeKv: true,\n fieldFilter,\n traverseStopRules,\n });\n\n let { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n // spreading fails due to exceeding stack argument size\n result = result.concat(followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\n }\n\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n capabilities: readonly string[] = [],\n mode: TraversalMode = \"auto\",\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const startTimestamp = Date.now();\n if (stats) stats.requests++;\n\n try {\n // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}