@hops-ops/distributed 0.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -13
- package/dist/auth-headers.js +3 -3
- package/dist/internal/cache-engine/engine.d.ts +3 -1
- package/dist/internal/cache-engine/engine.js +137 -22
- package/dist/internal/cache-engine/errors.d.ts +4 -0
- package/dist/internal/cache-engine/errors.js +8 -0
- package/dist/internal/cache-engine/index.d.ts +2 -2
- package/dist/internal/cache-engine/index.js +1 -1
- package/dist/internal/cache-engine/types.d.ts +16 -0
- package/dist/internal/cache-engine.d.ts +2 -2
- package/dist/internal/cache-engine.js +1 -1
- package/dist/protocol.d.ts +9 -2
- package/dist/protocol.js +35 -7
- package/dist/replica/command-runtime/create.js +499 -57
- package/dist/replica/command-runtime/index.d.ts +1 -1
- package/dist/replica/command-runtime/index.js +1 -1
- package/dist/replica/command-runtime/lib/effects.d.ts +22 -9
- package/dist/replica/command-runtime/lib/effects.js +118 -20
- package/dist/replica/command-runtime/lib/projection.d.ts +2 -2
- package/dist/replica/command-runtime/lib/projection.js +14 -6
- package/dist/replica/command-runtime/lib/status.js +18 -10
- package/dist/replica/command-runtime/lib/transport.js +4 -0
- package/dist/replica/command-runtime/lib/util.js +8 -3
- package/dist/replica/command-runtime/symbols.d.ts +9 -0
- package/dist/replica/command-runtime/symbols.js +9 -0
- package/dist/replica/command-runtime/types.d.ts +15 -6
- package/dist/replica/command-runtime/types.js +1 -1
- package/dist/replica/command-runtime.d.ts +1 -1
- package/dist/replica/command-runtime.js +1 -1
- package/dist/replica/commands/clone.js +2 -1
- package/dist/replica/commands/index.d.ts +1 -0
- package/dist/replica/commands/prepare.js +7 -4
- package/dist/replica/commands/receipt.js +30 -16
- package/dist/replica/commands/types.d.ts +10 -5
- package/dist/replica/commands/validate.d.ts +5 -1
- package/dist/replica/commands/validate.js +129 -62
- package/dist/replica/diagnostics/inspect.js +51 -19
- package/dist/replica/diagnostics/types.d.ts +7 -7
- package/dist/replica/distributed-replica/helpers.js +22 -8
- package/dist/replica/distributed-replica/hydration.js +14 -2
- package/dist/replica/distributed-replica/impl-diagnostics.d.ts +1 -1
- package/dist/replica/distributed-replica/impl-diagnostics.js +4 -3
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.d.ts +1 -1
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.js +76 -42
- package/dist/replica/distributed-replica/impl-optimistic.d.ts +11 -2
- package/dist/replica/distributed-replica/impl-optimistic.js +22 -6
- package/dist/replica/distributed-replica/impl-protocol.js +4 -2
- package/dist/replica/distributed-replica/impl.d.ts +8 -2
- package/dist/replica/distributed-replica/impl.js +22 -5
- package/dist/replica/distributed-replica/optimistic.js +31 -3
- package/dist/replica/distributed-replica/types.d.ts +1 -0
- package/dist/replica/index-maintenance/engine.js +14 -6
- package/dist/replica/index.d.ts +4 -0
- package/dist/replica/index.js +2 -0
- package/dist/replica/materialize.js +25 -0
- package/dist/replica/mutation-cache.d.ts +62 -0
- package/dist/replica/mutation-cache.js +91 -0
- package/dist/replica/operation-binding.js +17 -6
- package/dist/replica/persistence/state.js +9 -1
- package/dist/replica/projection-delta/canonical.d.ts +6 -0
- package/dist/replica/projection-delta/canonical.js +29 -0
- package/dist/replica/projection-delta/index.d.ts +5 -0
- package/dist/replica/projection-delta/index.js +3 -0
- package/dist/replica/projection-delta/resolve.d.ts +8 -0
- package/dist/replica/projection-delta/resolve.js +266 -0
- package/dist/replica/projection-delta/types.d.ts +415 -0
- package/dist/replica/projection-delta/types.js +6 -0
- package/dist/replica/projection-delta/validate.d.ts +18 -0
- package/dist/replica/projection-delta/validate.js +1718 -0
- package/dist/replica/projection-delta/wasm-pure.d.ts +36 -0
- package/dist/replica/projection-delta/wasm-pure.js +71 -0
- package/dist/replica/query-plan/pagination.js +18 -8
- package/dist/replica/types.d.ts +13 -2
- package/dist/sveltekit/replica.d.ts +1 -1
- package/dist/sveltekit/replica.js +3 -0
- package/dist/sveltekit/vite.d.ts +6 -6
- package/dist/sveltekit/vite.js +6 -6
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-safe cache lowering for event-independent mutation IR (v1).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `distributed::mutation::cache::lower_mutation_cache`. Mutation IR is
|
|
5
|
+
* the semantic source; this module produces atomic optimistic-layer effects
|
|
6
|
+
* without a second browser CRUD interpreter.
|
|
7
|
+
*/
|
|
8
|
+
export type MutationCacheVisibility = {
|
|
9
|
+
readonly authorized: boolean;
|
|
10
|
+
readonly hasBaseRecord: boolean;
|
|
11
|
+
readonly relationshipCovered: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type MutationTarget = {
|
|
14
|
+
readonly model: string;
|
|
15
|
+
readonly storage: string;
|
|
16
|
+
};
|
|
17
|
+
export type MutationFieldAssignment = {
|
|
18
|
+
readonly kind: "set" | "unset" | "unknown";
|
|
19
|
+
readonly expression?: unknown;
|
|
20
|
+
};
|
|
21
|
+
export type MutationField = {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly assignment: MutationFieldAssignment;
|
|
24
|
+
};
|
|
25
|
+
export type MutationOperation = {
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly target: MutationTarget;
|
|
28
|
+
readonly fields?: readonly MutationField[];
|
|
29
|
+
readonly invalidations?: readonly unknown[];
|
|
30
|
+
};
|
|
31
|
+
export type MutationProgram = {
|
|
32
|
+
readonly ir_version: number;
|
|
33
|
+
readonly operations: readonly MutationOperation[];
|
|
34
|
+
readonly name?: string;
|
|
35
|
+
};
|
|
36
|
+
export type MutationCacheEffect = {
|
|
37
|
+
readonly kind: "upsert";
|
|
38
|
+
readonly target: MutationTarget;
|
|
39
|
+
readonly fields: readonly string[];
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: "patch";
|
|
42
|
+
readonly target: MutationTarget;
|
|
43
|
+
readonly fields: readonly string[];
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: "delete";
|
|
46
|
+
readonly target: MutationTarget;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: "invalidate";
|
|
49
|
+
readonly invalidations: readonly unknown[];
|
|
50
|
+
};
|
|
51
|
+
export type MutationCacheProgram = {
|
|
52
|
+
readonly effects: readonly MutationCacheEffect[];
|
|
53
|
+
};
|
|
54
|
+
export declare const MUTATION_CACHE_VISIBILITY_FULL: MutationCacheVisibility;
|
|
55
|
+
export declare const MUTATION_CACHE_VISIBILITY_UNAUTHORIZED: MutationCacheVisibility;
|
|
56
|
+
/**
|
|
57
|
+
* Lower a canonical mutation program to role-safe cache effects.
|
|
58
|
+
*
|
|
59
|
+
* Partial patches never create records. Unauthorized or unprovable operations
|
|
60
|
+
* fail closed to the narrowest model invalidation.
|
|
61
|
+
*/
|
|
62
|
+
export declare function lowerMutationCache(program: MutationProgram, visibility?: MutationCacheVisibility): MutationCacheProgram;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-safe cache lowering for event-independent mutation IR (v1).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `distributed::mutation::cache::lower_mutation_cache`. Mutation IR is
|
|
5
|
+
* the semantic source; this module produces atomic optimistic-layer effects
|
|
6
|
+
* without a second browser CRUD interpreter.
|
|
7
|
+
*/
|
|
8
|
+
export const MUTATION_CACHE_VISIBILITY_FULL = Object.freeze({
|
|
9
|
+
authorized: true,
|
|
10
|
+
hasBaseRecord: true,
|
|
11
|
+
relationshipCovered: true,
|
|
12
|
+
});
|
|
13
|
+
export const MUTATION_CACHE_VISIBILITY_UNAUTHORIZED = Object.freeze({
|
|
14
|
+
authorized: false,
|
|
15
|
+
hasBaseRecord: true,
|
|
16
|
+
relationshipCovered: true,
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Lower a canonical mutation program to role-safe cache effects.
|
|
20
|
+
*
|
|
21
|
+
* Partial patches never create records. Unauthorized or unprovable operations
|
|
22
|
+
* fail closed to the narrowest model invalidation.
|
|
23
|
+
*/
|
|
24
|
+
export function lowerMutationCache(program, visibility = MUTATION_CACHE_VISIBILITY_FULL) {
|
|
25
|
+
const effects = (program.operations ?? []).map((operation) => lowerOperation(operation, visibility));
|
|
26
|
+
return Object.freeze({ effects: Object.freeze(effects) });
|
|
27
|
+
}
|
|
28
|
+
function lowerOperation(operation, visibility) {
|
|
29
|
+
if (!visibility.authorized) {
|
|
30
|
+
return invalidateModel(operation);
|
|
31
|
+
}
|
|
32
|
+
switch (operation.kind) {
|
|
33
|
+
case "insert":
|
|
34
|
+
case "upsert":
|
|
35
|
+
case "recreate":
|
|
36
|
+
case "insert_related":
|
|
37
|
+
case "upsert_related": {
|
|
38
|
+
const fields = concreteFieldNames(operation);
|
|
39
|
+
if (fields.length === 0) {
|
|
40
|
+
return invalidateModel(operation);
|
|
41
|
+
}
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
kind: "upsert",
|
|
44
|
+
target: operation.target,
|
|
45
|
+
fields: Object.freeze(fields),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
case "patch":
|
|
49
|
+
case "upsert_patch": {
|
|
50
|
+
if (!visibility.hasBaseRecord) {
|
|
51
|
+
return invalidateModel(operation);
|
|
52
|
+
}
|
|
53
|
+
const fields = concreteFieldNames(operation);
|
|
54
|
+
if (fields.length === 0) {
|
|
55
|
+
return invalidateModel(operation);
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
kind: "patch",
|
|
59
|
+
target: operation.target,
|
|
60
|
+
fields: Object.freeze(fields),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
case "delete":
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
kind: "delete",
|
|
66
|
+
target: operation.target,
|
|
67
|
+
});
|
|
68
|
+
case "invalidate":
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
kind: "invalidate",
|
|
71
|
+
invalidations: Object.freeze(operation.invalidations?.length
|
|
72
|
+
? [...operation.invalidations]
|
|
73
|
+
: [{ kind: "model", model: operation.target.model }]),
|
|
74
|
+
});
|
|
75
|
+
default:
|
|
76
|
+
throw new Error(`unsupported mutation kind: ${operation.kind}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function concreteFieldNames(operation) {
|
|
80
|
+
return (operation.fields ?? [])
|
|
81
|
+
.filter((field) => field.assignment?.kind === "set" || field.assignment?.kind === "unset")
|
|
82
|
+
.map((field) => field.name);
|
|
83
|
+
}
|
|
84
|
+
function invalidateModel(operation) {
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
kind: "invalidate",
|
|
87
|
+
invalidations: Object.freeze([
|
|
88
|
+
{ kind: "model", model: operation.target.model },
|
|
89
|
+
]),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -39,14 +39,25 @@ function validateReplicaSurfaceIdentity(value) {
|
|
|
39
39
|
return JSON.stringify(['role', value.name]);
|
|
40
40
|
}
|
|
41
41
|
if (value.kind !== 'application' ||
|
|
42
|
-
!Array.isArray(value.
|
|
43
|
-
value.
|
|
44
|
-
value.
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
!Array.isArray(value.eligible_roles) ||
|
|
43
|
+
!Array.isArray(value.schema_roles) ||
|
|
44
|
+
value.eligible_roles.length === 0 ||
|
|
45
|
+
value.schema_roles.length === 0 ||
|
|
46
|
+
value.eligible_roles.some((role) => typeof role !== 'string' || role.length === 0) ||
|
|
47
|
+
value.schema_roles.some((role) => typeof role !== 'string' || role.length === 0) ||
|
|
48
|
+
new Set(value.eligible_roles).size !== value.eligible_roles.length ||
|
|
49
|
+
new Set(value.schema_roles).size !== value.schema_roles.length ||
|
|
50
|
+
[...value.eligible_roles].sort().some((role, index) => role !== value.eligible_roles[index]) ||
|
|
51
|
+
[...value.schema_roles].sort().some((role, index) => role !== value.schema_roles[index]) ||
|
|
52
|
+
value.schema_roles.some((role) => !value.eligible_roles.includes(role))) {
|
|
47
53
|
throw new TypeError('replica artifact client surface is invalid');
|
|
48
54
|
}
|
|
49
|
-
return JSON.stringify([
|
|
55
|
+
return JSON.stringify([
|
|
56
|
+
'application',
|
|
57
|
+
value.name,
|
|
58
|
+
value.eligible_roles,
|
|
59
|
+
value.schema_roles
|
|
60
|
+
]);
|
|
50
61
|
}
|
|
51
62
|
function canonicalTrustedPresetDescriptors(value) {
|
|
52
63
|
if (!Array.isArray(value)) {
|
|
@@ -214,13 +214,19 @@ export function parseReplicaState(value) {
|
|
|
214
214
|
return Object.freeze({ state: normalizedState, scope, payload });
|
|
215
215
|
}
|
|
216
216
|
export function parseAuthoritativeScope(value) {
|
|
217
|
-
const scope = exactRecord(value, 'authoritative scope', [
|
|
217
|
+
const scope = exactRecord(value, 'authoritative scope', [
|
|
218
|
+
'protocolVersion',
|
|
219
|
+
'schemaHash',
|
|
220
|
+
'authorizationGeneration',
|
|
221
|
+
'cacheScope'
|
|
222
|
+
]);
|
|
218
223
|
if (scope.protocolVersion !== 1) {
|
|
219
224
|
throw new TypeError('unsupported authoritative protocol version');
|
|
220
225
|
}
|
|
221
226
|
return Object.freeze({
|
|
222
227
|
protocolVersion: 1,
|
|
223
228
|
schemaHash: nonEmptyString(scope.schemaHash, 'authoritative scope schemaHash'),
|
|
229
|
+
authorizationGeneration: nonEmptyString(scope.authorizationGeneration, 'authoritative scope authorizationGeneration'),
|
|
224
230
|
cacheScope: nonEmptyString(scope.cacheScope, 'authoritative scope cacheScope')
|
|
225
231
|
});
|
|
226
232
|
}
|
|
@@ -569,12 +575,14 @@ export function persistenceIdentity(scope) {
|
|
|
569
575
|
ENTRY_FORMAT_VERSION,
|
|
570
576
|
scope.protocolVersion,
|
|
571
577
|
scope.schemaHash,
|
|
578
|
+
scope.authorizationGeneration,
|
|
572
579
|
scope.cacheScope
|
|
573
580
|
]);
|
|
574
581
|
}
|
|
575
582
|
export function sameScope(left, right) {
|
|
576
583
|
return (left.protocolVersion === right.protocolVersion &&
|
|
577
584
|
left.schemaHash === right.schemaHash &&
|
|
585
|
+
left.authorizationGeneration === right.authorizationGeneration &&
|
|
578
586
|
left.cacheScope === right.cacheScope);
|
|
579
587
|
}
|
|
580
588
|
export function modelFromRecordKey(key) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CommandProjectionMetadata, ProjectionDelta } from './types.js';
|
|
2
|
+
/** Canonical bytes use the exact field order of the frozen Rust serde structs. */
|
|
3
|
+
export declare function canonicalProjectionDelta(delta: ProjectionDelta): string;
|
|
4
|
+
export declare function canonicalCommandProjectionMetadata(metadata: CommandProjectionMetadata): string;
|
|
5
|
+
export declare function projectionDeltaByteLength(delta: ProjectionDelta): number;
|
|
6
|
+
export declare function commandProjectionMetadataByteLength(metadata: CommandProjectionMetadata): number;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
/** Canonical bytes use the exact field order of the frozen Rust serde structs. */
|
|
3
|
+
export function canonicalProjectionDelta(delta) {
|
|
4
|
+
return JSON.stringify({
|
|
5
|
+
wire_version: delta.wire_version,
|
|
6
|
+
identity: delta.identity,
|
|
7
|
+
projections: delta.projections,
|
|
8
|
+
occurrences: delta.occurrences,
|
|
9
|
+
operations: delta.operations,
|
|
10
|
+
...(delta.recoveries.length === 0 ? {} : { recoveries: delta.recoveries })
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function canonicalCommandProjectionMetadata(metadata) {
|
|
14
|
+
return JSON.stringify({
|
|
15
|
+
wireVersion: metadata.wireVersion,
|
|
16
|
+
issuedAtUnixMs: metadata.issuedAtUnixMs,
|
|
17
|
+
expiresAtUnixMs: metadata.expiresAtUnixMs,
|
|
18
|
+
delta: JSON.parse(canonicalProjectionDelta(metadata.delta)),
|
|
19
|
+
lifecycleProofs: metadata.lifecycleProofs,
|
|
20
|
+
obligations: metadata.obligations,
|
|
21
|
+
revalidate: metadata.revalidate
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function projectionDeltaByteLength(delta) {
|
|
25
|
+
return encoder.encode(canonicalProjectionDelta(delta)).byteLength;
|
|
26
|
+
}
|
|
27
|
+
export function commandProjectionMetadataByteLength(metadata) {
|
|
28
|
+
return encoder.encode(canonicalCommandProjectionMetadata(metadata)).byteLength;
|
|
29
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { canonicalCommandProjectionMetadata, canonicalProjectionDelta, parseCommandProjectionMetadata, parseProjectionDelta, ProjectionDeltaValidationError, validateCommandProjectionArtifact, validateProjectionMetadataAuthority } from './validate.js';
|
|
2
|
+
export { deltaValue, operationsFromProjectionDelta, prepareCommandProjection } from './resolve.js';
|
|
3
|
+
export { createWasmJsonPure } from './wasm-pure.js';
|
|
4
|
+
export type { CreateWasmJsonPureOptions, WasmJsonModule, WasmJsonPureHost } from './wasm-pure.js';
|
|
5
|
+
export type { AppliedProjectionDelta, CommandProjectionMetadata, PreparedCommandProjection, PreparedProjectionOperation, PreparedProjectionScope, ProjectionCapabilityArm, ProjectionCapabilityMutation, ProjectionDelta, ProjectionDeltaMutation, ProjectionDeltaRecoveryTarget, ProjectionDeltaScope, ProjectionDeltaValue, ProjectionPreviewPureReduce, ProjectionPreviewScope, ProjectionPreviewValue, ReplicaCommandProjection, ReplicaPureFunction } from './types.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { canonicalCommandProjectionMetadata, canonicalProjectionDelta, parseCommandProjectionMetadata, parseProjectionDelta, ProjectionDeltaValidationError, validateCommandProjectionArtifact, validateProjectionMetadataAuthority } from './validate.js';
|
|
2
|
+
export { deltaValue, operationsFromProjectionDelta, prepareCommandProjection } from './resolve.js';
|
|
3
|
+
export { createWasmJsonPure } from './wasm-pure.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DistributedTrustedPreset } from '../../protocol.js';
|
|
2
|
+
import type { ReplicaValue } from '../types.js';
|
|
3
|
+
import type { PreparedCommandProjection, PreparedProjectionOperation, ProjectionDeltaValue, ReplicaCommandProjection } from './types.js';
|
|
4
|
+
export declare function prepareCommandProjection(contract: ReplicaCommandProjection, input: unknown, trustedPresets: readonly DistributedTrustedPreset[]): PreparedCommandProjection;
|
|
5
|
+
export declare function operationsFromProjectionDelta(scopes: readonly {
|
|
6
|
+
readonly mutation: import('./types.js').ProjectionDeltaMutation;
|
|
7
|
+
}[]): readonly PreparedProjectionOperation[];
|
|
8
|
+
export declare function deltaValue(value: ProjectionDeltaValue): ReplicaValue;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
const missing = Symbol('projection-preview-missing');
|
|
2
|
+
const emptyUnsetFields = Object.freeze([]);
|
|
3
|
+
export function prepareCommandProjection(contract, input, trustedPresets) {
|
|
4
|
+
const presets = new Map(trustedPresets.map((preset) => [preset.name, preset]));
|
|
5
|
+
try {
|
|
6
|
+
const preview = contract.preview.operations.map(({ mutation }) => resolvePreviewMutation(mutation, input, presets));
|
|
7
|
+
const pure = (contract.pureReduces ?? []).map((reduce) => resolvePureReduce(reduce, input, presets));
|
|
8
|
+
return Object.freeze({
|
|
9
|
+
contract,
|
|
10
|
+
preview: Object.freeze([...preview, ...pure]),
|
|
11
|
+
revalidate: contract.preview.recoveries.length !== 0
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Preview is only a convenience. Missing client authority must never be
|
|
16
|
+
// promoted into invented cache truth.
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
contract,
|
|
19
|
+
preview: Object.freeze([]),
|
|
20
|
+
revalidate: true
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function resolvePureReduce(reduce, input, presets) {
|
|
25
|
+
const args = Object.create(null);
|
|
26
|
+
for (const { name, value } of reduce.args) {
|
|
27
|
+
args[name] = requireValue(resolvePreviewValue(value, input, presets));
|
|
28
|
+
}
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
kind: 'reduce_known_record',
|
|
31
|
+
fn: reduce.fn,
|
|
32
|
+
scope: previewScope(reduce.scope, input, presets),
|
|
33
|
+
args: Object.freeze(args),
|
|
34
|
+
assign: Object.freeze([...reduce.assign])
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function operationsFromProjectionDelta(scopes) {
|
|
38
|
+
return Object.freeze(scopes.map(({ mutation }) => {
|
|
39
|
+
switch (mutation.op) {
|
|
40
|
+
case 'upsert':
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
kind: 'upsert',
|
|
43
|
+
scope: actualScope(mutation.scope),
|
|
44
|
+
fields: fieldsFromDelta(mutation.fields),
|
|
45
|
+
replace: mutation.replace
|
|
46
|
+
});
|
|
47
|
+
case 'patch':
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
kind: 'patch',
|
|
50
|
+
scope: actualScope(mutation.scope),
|
|
51
|
+
fields: fieldsFromDelta(mutation.set),
|
|
52
|
+
unset: mutation.unset,
|
|
53
|
+
ifPresent: true
|
|
54
|
+
});
|
|
55
|
+
case 'delete':
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
kind: 'delete',
|
|
58
|
+
scope: actualScope(mutation.scope)
|
|
59
|
+
});
|
|
60
|
+
case 'link':
|
|
61
|
+
case 'unlink':
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
kind: mutation.op,
|
|
64
|
+
relationship: mutation.relationship,
|
|
65
|
+
source: actualScope(mutation.source),
|
|
66
|
+
target: actualScope(mutation.target)
|
|
67
|
+
});
|
|
68
|
+
case 'invalidate_model':
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
kind: 'invalidate_model',
|
|
71
|
+
model: mutation.model
|
|
72
|
+
});
|
|
73
|
+
case 'invalidate_relationship':
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
kind: 'invalidate_relationship',
|
|
76
|
+
relationship: mutation.relationship,
|
|
77
|
+
source: actualScope(mutation.source)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
function resolvePreviewMutation(mutation, input, presets) {
|
|
83
|
+
switch (mutation.op) {
|
|
84
|
+
case 'upsert':
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
kind: 'upsert',
|
|
87
|
+
scope: previewScope(mutation.scope, input, presets),
|
|
88
|
+
fields: previewFields(mutation.fields, input, presets),
|
|
89
|
+
replace: mutation.replace
|
|
90
|
+
});
|
|
91
|
+
case 'patch':
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
kind: 'patch',
|
|
94
|
+
scope: previewScope(mutation.scope, input, presets),
|
|
95
|
+
fields: previewFields(mutation.set, input, presets),
|
|
96
|
+
unset: mutation.unset ?? emptyUnsetFields,
|
|
97
|
+
ifPresent: true
|
|
98
|
+
});
|
|
99
|
+
case 'delete':
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
kind: 'delete',
|
|
102
|
+
scope: previewScope(mutation.scope, input, presets)
|
|
103
|
+
});
|
|
104
|
+
case 'link':
|
|
105
|
+
case 'unlink':
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
kind: mutation.op,
|
|
108
|
+
relationship: mutation.relationship,
|
|
109
|
+
source: previewScope(mutation.source, input, presets),
|
|
110
|
+
target: previewScope(mutation.target, input, presets)
|
|
111
|
+
});
|
|
112
|
+
case 'invalidate_model':
|
|
113
|
+
if (mutation.partition?.kind === 'expression') {
|
|
114
|
+
requireValue(resolvePreviewValue(mutation.partition.expression, input, presets));
|
|
115
|
+
}
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
kind: 'invalidate_model',
|
|
118
|
+
model: mutation.model
|
|
119
|
+
});
|
|
120
|
+
case 'invalidate_relationship':
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
kind: 'invalidate_relationship',
|
|
123
|
+
relationship: mutation.relationship,
|
|
124
|
+
source: previewScope(mutation.source, input, presets)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function previewScope(scope, input, presets) {
|
|
129
|
+
if (scope.partition.kind === 'expression') {
|
|
130
|
+
requireValue(resolvePreviewValue(scope.partition.expression, input, presets));
|
|
131
|
+
}
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
model: scope.model,
|
|
134
|
+
key: Object.freeze(scope.key.map(({ field, value }) => Object.freeze({
|
|
135
|
+
field,
|
|
136
|
+
value: requireScalar(requireValue(resolvePreviewValue(value, input, presets)))
|
|
137
|
+
})))
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function actualScope(scope) {
|
|
141
|
+
return Object.freeze({
|
|
142
|
+
model: scope.model,
|
|
143
|
+
key: Object.freeze(scope.key.map(({ field, value }) => Object.freeze({ field, value: requireScalar(deltaValue(value)) })))
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function previewFields(fields, input, presets) {
|
|
147
|
+
return Object.freeze(Object.fromEntries(fields.map(({ field, value }) => [
|
|
148
|
+
field,
|
|
149
|
+
requireValue(resolvePreviewValue(value, input, presets))
|
|
150
|
+
])));
|
|
151
|
+
}
|
|
152
|
+
function fieldsFromDelta(fields) {
|
|
153
|
+
return Object.freeze(Object.fromEntries(fields.map(({ field, value }) => [field, deltaValue(value)])));
|
|
154
|
+
}
|
|
155
|
+
function resolvePreviewValue(expression, input, presets) {
|
|
156
|
+
switch (expression.kind) {
|
|
157
|
+
case 'input':
|
|
158
|
+
case 'generated_default':
|
|
159
|
+
return valueAtPath(input, expression.path);
|
|
160
|
+
case 'trusted_preset': {
|
|
161
|
+
const preset = presets.get(expression.name);
|
|
162
|
+
if (preset === undefined || preset.codec !== expression.codec)
|
|
163
|
+
return missing;
|
|
164
|
+
return preset.value;
|
|
165
|
+
}
|
|
166
|
+
case 'constant':
|
|
167
|
+
return deltaValue(expression.value);
|
|
168
|
+
case 'null':
|
|
169
|
+
return null;
|
|
170
|
+
case 'list': {
|
|
171
|
+
const values = expression.values.map((value) => resolvePreviewValue(value, input, presets));
|
|
172
|
+
return values.some((value) => value === missing)
|
|
173
|
+
? missing
|
|
174
|
+
: Object.freeze(values);
|
|
175
|
+
}
|
|
176
|
+
case 'object': {
|
|
177
|
+
const fields = [];
|
|
178
|
+
for (const field of expression.fields) {
|
|
179
|
+
const value = resolvePreviewValue(field.value, input, presets);
|
|
180
|
+
if (value === missing)
|
|
181
|
+
return missing;
|
|
182
|
+
fields.push([field.name, value]);
|
|
183
|
+
}
|
|
184
|
+
return Object.freeze(Object.fromEntries(fields));
|
|
185
|
+
}
|
|
186
|
+
case 'transform':
|
|
187
|
+
if (expression.transform === 'first_present') {
|
|
188
|
+
for (const argument of expression.arguments) {
|
|
189
|
+
const value = resolvePreviewValue(argument, input, presets);
|
|
190
|
+
if (value !== missing)
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
return missing;
|
|
194
|
+
}
|
|
195
|
+
return expression.arguments
|
|
196
|
+
.map((argument) => requireValue(resolvePreviewValue(argument, input, presets)))
|
|
197
|
+
.map((value) => {
|
|
198
|
+
if (typeof value !== 'string')
|
|
199
|
+
throw new TypeError('projection');
|
|
200
|
+
return value;
|
|
201
|
+
})
|
|
202
|
+
.join('');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function valueAtPath(value, path) {
|
|
206
|
+
let current = value;
|
|
207
|
+
for (const segment of path) {
|
|
208
|
+
if (current === null ||
|
|
209
|
+
typeof current !== 'object' ||
|
|
210
|
+
!Object.hasOwn(current, segment))
|
|
211
|
+
return missing;
|
|
212
|
+
current = current[segment];
|
|
213
|
+
}
|
|
214
|
+
return isReplicaValue(current) ? current : missing;
|
|
215
|
+
}
|
|
216
|
+
export function deltaValue(value) {
|
|
217
|
+
switch (value.type) {
|
|
218
|
+
case 'null':
|
|
219
|
+
return null;
|
|
220
|
+
case 'boolean':
|
|
221
|
+
return value.value;
|
|
222
|
+
case 'string':
|
|
223
|
+
return value.value;
|
|
224
|
+
case 'enum':
|
|
225
|
+
return value.value.variant;
|
|
226
|
+
case 'i64':
|
|
227
|
+
case 'u64': {
|
|
228
|
+
const parsed = BigInt(value.value);
|
|
229
|
+
return parsed >= BigInt(Number.MIN_SAFE_INTEGER) &&
|
|
230
|
+
parsed <= BigInt(Number.MAX_SAFE_INTEGER)
|
|
231
|
+
? Number(parsed)
|
|
232
|
+
: value.value;
|
|
233
|
+
}
|
|
234
|
+
case 'f64':
|
|
235
|
+
return Number(value.value);
|
|
236
|
+
case 'list':
|
|
237
|
+
return Object.freeze(value.value.map(deltaValue));
|
|
238
|
+
case 'object':
|
|
239
|
+
return Object.freeze(Object.fromEntries(value.value.map((field) => [field.field, deltaValue(field.value)])));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function requireValue(value) {
|
|
243
|
+
if (value === missing)
|
|
244
|
+
throw new TypeError('projection preview is unavailable');
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
function requireScalar(value) {
|
|
248
|
+
if (Array.isArray(value) || (value !== null && typeof value === 'object')) {
|
|
249
|
+
throw new TypeError('projection identity must be scalar');
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
function isReplicaValue(value, depth = 0) {
|
|
254
|
+
if (depth > 64)
|
|
255
|
+
return false;
|
|
256
|
+
if (value === null ||
|
|
257
|
+
typeof value === 'string' ||
|
|
258
|
+
typeof value === 'boolean' ||
|
|
259
|
+
(typeof value === 'number' && Number.isFinite(value)))
|
|
260
|
+
return true;
|
|
261
|
+
if (Array.isArray(value))
|
|
262
|
+
return value.every((item) => isReplicaValue(item, depth + 1));
|
|
263
|
+
if (typeof value !== 'object')
|
|
264
|
+
return false;
|
|
265
|
+
return Object.entries(value).every(([, item]) => isReplicaValue(item, depth + 1));
|
|
266
|
+
}
|