-
Notifications
You must be signed in to change notification settings - Fork 0
feat(materialize): add retained CAS materialization handles #751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3b55d1a
feat(materialize): add retained materialization handles
flyingrobots 701f4c7
perf(materialize): decode handle bundle in one pass
flyingrobots 6c1e483
fix(test): derive CAS page handle hash algorithm
flyingrobots a66466b
refactor(materialize): centralize root names
flyingrobots cb0b68c
fix(materialize): derive entries from root names
flyingrobots File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import WarpError from '../errors/WarpError.ts'; | ||
| import { compareStrings } from '../utils/StringComparison.ts'; | ||
|
|
||
| export type MaterializationFrontierEntry = Readonly<{ | ||
| writerId: string; | ||
| patchSha: string; | ||
| }>; | ||
|
|
||
| /** Immutable causal coordinate identifying one exact materialization. */ | ||
| export default class MaterializationCoordinate { | ||
| readonly frontierEntries: readonly MaterializationFrontierEntry[]; | ||
| readonly ceiling: number | null; | ||
|
|
||
| constructor(options: { | ||
| readonly frontier: Map<string, string>; | ||
| readonly ceiling: number | null; | ||
| }) { | ||
| requireOptions(options); | ||
| this.frontierEntries = freezeFrontier(options.frontier); | ||
| this.ceiling = requireCeiling(options.ceiling); | ||
| Object.freeze(this); | ||
| } | ||
|
|
||
| frontier(): Map<string, string> { | ||
| return new Map( | ||
| this.frontierEntries.map((entry) => [entry.writerId, entry.patchSha]), | ||
| ); | ||
| } | ||
|
|
||
| equals(other: MaterializationCoordinate | null | undefined): boolean { | ||
| if (!(other instanceof MaterializationCoordinate) || this.ceiling !== other.ceiling) { | ||
| return false; | ||
| } | ||
| if (this.frontierEntries.length !== other.frontierEntries.length) { | ||
| return false; | ||
| } | ||
| return this.frontierEntries.every((entry, index) => { | ||
| const candidate = other.frontierEntries[index]; | ||
| return candidate?.writerId === entry.writerId && candidate.patchSha === entry.patchSha; | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| function freezeFrontier(frontier: Map<string, string>): readonly MaterializationFrontierEntry[] { | ||
| if (!(frontier instanceof Map)) { | ||
| throw coordinateError('frontier must be a Map'); | ||
| } | ||
| return Object.freeze( | ||
| [...frontier.entries()] | ||
| .sort(([left], [right]) => compareStrings(left, right)) | ||
| .map(([writerId, patchSha]) => Object.freeze({ | ||
| writerId: requireNonEmpty(writerId, 'frontier writerId'), | ||
| patchSha: requireNonEmpty(patchSha, 'frontier patchSha'), | ||
| })), | ||
| ); | ||
| } | ||
|
|
||
| function requireCeiling(ceiling: number | null): number | null { | ||
| if (ceiling === null) { | ||
| return null; | ||
| } | ||
| if (!Number.isSafeInteger(ceiling) || ceiling < 0) { | ||
| throw coordinateError('ceiling must be a non-negative safe integer or null'); | ||
| } | ||
| return ceiling; | ||
| } | ||
|
|
||
| function requireNonEmpty(value: string, field: string): string { | ||
| if (typeof value !== 'string' || value.length === 0) { | ||
| throw coordinateError(`${field} must be a non-empty string`); | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| function requireOptions(options: object): void { | ||
| if (options === null || typeof options !== 'object' || Array.isArray(options)) { | ||
| throw coordinateError('options must be an object'); | ||
| } | ||
| } | ||
|
|
||
| function coordinateError(message: string): WarpError { | ||
| return new WarpError(`Materialization coordinate ${message}`, 'E_MATERIALIZATION_COORDINATE'); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import WarpError from '../errors/WarpError.ts'; | ||
| import BundleHandle from '../storage/BundleHandle.ts'; | ||
| import StorageRetentionWitness from '../storage/StorageRetentionWitness.ts'; | ||
| import MaterializationCoordinate from './MaterializationCoordinate.ts'; | ||
| import MaterializationRoots from './MaterializationRoots.ts'; | ||
|
|
||
| /** Retained immutable locator and causal identity for one materialization. */ | ||
| export default class MaterializationHandle { | ||
| readonly laneName: string; | ||
| readonly bundle: BundleHandle; | ||
| readonly coordinate: MaterializationCoordinate; | ||
| readonly roots: MaterializationRoots; | ||
| readonly stateHash: string; | ||
| readonly retention: StorageRetentionWitness; | ||
|
|
||
| constructor(options: { | ||
| readonly laneName: string; | ||
| readonly bundle: BundleHandle; | ||
| readonly coordinate: MaterializationCoordinate; | ||
| readonly roots: MaterializationRoots; | ||
| readonly stateHash: string; | ||
| readonly retention: StorageRetentionWitness; | ||
| }) { | ||
| requireOptions(options); | ||
| this.laneName = requireNonEmpty(options.laneName, 'laneName'); | ||
| this.bundle = requireInstance(options.bundle, BundleHandle, 'bundle'); | ||
| this.coordinate = requireInstance( | ||
| options.coordinate, | ||
| MaterializationCoordinate, | ||
| 'coordinate', | ||
| ); | ||
| this.roots = requireInstance(options.roots, MaterializationRoots, 'roots'); | ||
| this.stateHash = requireNonEmpty(options.stateHash, 'stateHash'); | ||
| this.retention = requireInstance( | ||
| options.retention, | ||
| StorageRetentionWitness, | ||
| 'retention', | ||
| ); | ||
| if (!this.retention.handle.equals(this.bundle)) { | ||
| throw handleError('retention witness does not retain the materialization bundle'); | ||
| } | ||
| Object.freeze(this); | ||
| } | ||
| } | ||
|
|
||
| type RuntimeClass<T> = abstract new (...args: never[]) => T; | ||
|
|
||
| function requireInstance<T>(value: T, runtimeClass: RuntimeClass<T>, field: string): T { | ||
| if (!(value instanceof runtimeClass)) { | ||
| throw handleError(`${field} has an invalid runtime identity`); | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| function requireNonEmpty(value: string, field: string): string { | ||
| if (typeof value !== 'string' || value.length === 0) { | ||
| throw handleError(`${field} must be a non-empty string`); | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| function requireOptions(options: object): void { | ||
| if (options === null || typeof options !== 'object' || Array.isArray(options)) { | ||
| throw handleError('options must be an object'); | ||
| } | ||
| } | ||
|
|
||
| function handleError(message: string): WarpError { | ||
| return new WarpError(`Materialization handle ${message}`, 'E_MATERIALIZATION_HANDLE'); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import WarpError from '../errors/WarpError.ts'; | ||
| import BundleHandle from '../storage/BundleHandle.ts'; | ||
|
|
||
| export const MATERIALIZATION_ROOT_NAMES = defineRootNames( | ||
| 'adjacency', | ||
| 'edge-alive', | ||
| 'edge-births', | ||
| 'frontier', | ||
| 'node-alive', | ||
| 'properties', | ||
| 'provenance-support', | ||
| 'roaring-indexes', | ||
| ); | ||
|
|
||
| export type MaterializationRootName = (typeof MATERIALIZATION_ROOT_NAMES)[number]; | ||
|
|
||
| export type MaterializationRootsOptions = Readonly<{ | ||
| adjacency: BundleHandle; | ||
| edgeAlive: BundleHandle; | ||
| edgeBirths: BundleHandle; | ||
| frontier: BundleHandle; | ||
| nodeAlive: BundleHandle; | ||
| properties: BundleHandle; | ||
| provenanceSupport: BundleHandle; | ||
| roaringIndexes: BundleHandle; | ||
| }>; | ||
|
|
||
| /** Independently addressable retained roots for one materialized causal chart. */ | ||
| export default class MaterializationRoots { | ||
| readonly adjacency: BundleHandle; | ||
| readonly edgeAlive: BundleHandle; | ||
| readonly edgeBirths: BundleHandle; | ||
| readonly frontier: BundleHandle; | ||
| readonly nodeAlive: BundleHandle; | ||
| readonly properties: BundleHandle; | ||
| readonly provenanceSupport: BundleHandle; | ||
| readonly roaringIndexes: BundleHandle; | ||
|
|
||
| constructor(options: MaterializationRootsOptions) { | ||
| requireOptions(options); | ||
| this.adjacency = requireBundle(options.adjacency, 'adjacency'); | ||
| this.edgeAlive = requireBundle(options.edgeAlive, 'edgeAlive'); | ||
| this.edgeBirths = requireBundle(options.edgeBirths, 'edgeBirths'); | ||
| this.frontier = requireBundle(options.frontier, 'frontier'); | ||
| this.nodeAlive = requireBundle(options.nodeAlive, 'nodeAlive'); | ||
| this.properties = requireBundle(options.properties, 'properties'); | ||
| this.provenanceSupport = requireBundle(options.provenanceSupport, 'provenanceSupport'); | ||
| this.roaringIndexes = requireBundle(options.roaringIndexes, 'roaringIndexes'); | ||
| Object.freeze(this); | ||
| } | ||
|
|
||
| entries(): readonly (readonly [MaterializationRootName, BundleHandle])[] { | ||
| return Object.freeze([ | ||
| rootEntry('adjacency', this.adjacency), | ||
| rootEntry('edge-alive', this.edgeAlive), | ||
| rootEntry('edge-births', this.edgeBirths), | ||
| rootEntry('frontier', this.frontier), | ||
| rootEntry('node-alive', this.nodeAlive), | ||
| rootEntry('properties', this.properties), | ||
| rootEntry('provenance-support', this.provenanceSupport), | ||
| rootEntry('roaring-indexes', this.roaringIndexes), | ||
| ]); | ||
| } | ||
| } | ||
|
|
||
| function rootEntry( | ||
| name: MaterializationRootName, | ||
| handle: BundleHandle, | ||
| ): readonly [MaterializationRootName, BundleHandle] { | ||
| return Object.freeze([name, handle]); | ||
| } | ||
|
|
||
| function defineRootNames<const Names extends readonly string[]>(...names: Names): Names { | ||
| Object.freeze(names); | ||
| return names; | ||
| } | ||
|
|
||
| function requireBundle(handle: BundleHandle, field: string): BundleHandle { | ||
| if (!(handle instanceof BundleHandle)) { | ||
| throw rootsError(`${field} must be a BundleHandle`); | ||
| } | ||
| return handle; | ||
| } | ||
|
|
||
| function requireOptions(options: object): void { | ||
| if (options === null || typeof options !== 'object' || Array.isArray(options)) { | ||
| throw rootsError('options must be an object'); | ||
| } | ||
| } | ||
|
|
||
| function rootsError(message: string): WarpError { | ||
| return new WarpError(`Materialization roots ${message}`, 'E_MATERIALIZATION_ROOTS'); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.