@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.
Files changed (78) hide show
  1. package/README.md +26 -13
  2. package/dist/auth-headers.js +3 -3
  3. package/dist/internal/cache-engine/engine.d.ts +3 -1
  4. package/dist/internal/cache-engine/engine.js +137 -22
  5. package/dist/internal/cache-engine/errors.d.ts +4 -0
  6. package/dist/internal/cache-engine/errors.js +8 -0
  7. package/dist/internal/cache-engine/index.d.ts +2 -2
  8. package/dist/internal/cache-engine/index.js +1 -1
  9. package/dist/internal/cache-engine/types.d.ts +16 -0
  10. package/dist/internal/cache-engine.d.ts +2 -2
  11. package/dist/internal/cache-engine.js +1 -1
  12. package/dist/protocol.d.ts +9 -2
  13. package/dist/protocol.js +35 -7
  14. package/dist/replica/command-runtime/create.js +499 -57
  15. package/dist/replica/command-runtime/index.d.ts +1 -1
  16. package/dist/replica/command-runtime/index.js +1 -1
  17. package/dist/replica/command-runtime/lib/effects.d.ts +22 -9
  18. package/dist/replica/command-runtime/lib/effects.js +118 -20
  19. package/dist/replica/command-runtime/lib/projection.d.ts +2 -2
  20. package/dist/replica/command-runtime/lib/projection.js +14 -6
  21. package/dist/replica/command-runtime/lib/status.js +18 -10
  22. package/dist/replica/command-runtime/lib/transport.js +4 -0
  23. package/dist/replica/command-runtime/lib/util.js +8 -3
  24. package/dist/replica/command-runtime/symbols.d.ts +9 -0
  25. package/dist/replica/command-runtime/symbols.js +9 -0
  26. package/dist/replica/command-runtime/types.d.ts +15 -6
  27. package/dist/replica/command-runtime/types.js +1 -1
  28. package/dist/replica/command-runtime.d.ts +1 -1
  29. package/dist/replica/command-runtime.js +1 -1
  30. package/dist/replica/commands/clone.js +2 -1
  31. package/dist/replica/commands/index.d.ts +1 -0
  32. package/dist/replica/commands/prepare.js +7 -4
  33. package/dist/replica/commands/receipt.js +30 -16
  34. package/dist/replica/commands/types.d.ts +10 -5
  35. package/dist/replica/commands/validate.d.ts +5 -1
  36. package/dist/replica/commands/validate.js +129 -62
  37. package/dist/replica/diagnostics/inspect.js +51 -19
  38. package/dist/replica/diagnostics/types.d.ts +7 -7
  39. package/dist/replica/distributed-replica/helpers.js +22 -8
  40. package/dist/replica/distributed-replica/hydration.js +14 -2
  41. package/dist/replica/distributed-replica/impl-diagnostics.d.ts +1 -1
  42. package/dist/replica/distributed-replica/impl-diagnostics.js +4 -3
  43. package/dist/replica/distributed-replica/impl-hydration-orchestrate.d.ts +1 -1
  44. package/dist/replica/distributed-replica/impl-hydration-orchestrate.js +76 -42
  45. package/dist/replica/distributed-replica/impl-optimistic.d.ts +11 -2
  46. package/dist/replica/distributed-replica/impl-optimistic.js +22 -6
  47. package/dist/replica/distributed-replica/impl-protocol.js +4 -2
  48. package/dist/replica/distributed-replica/impl.d.ts +8 -2
  49. package/dist/replica/distributed-replica/impl.js +22 -5
  50. package/dist/replica/distributed-replica/optimistic.js +31 -3
  51. package/dist/replica/distributed-replica/types.d.ts +1 -0
  52. package/dist/replica/index-maintenance/engine.js +14 -6
  53. package/dist/replica/index.d.ts +4 -0
  54. package/dist/replica/index.js +2 -0
  55. package/dist/replica/materialize.js +25 -0
  56. package/dist/replica/mutation-cache.d.ts +62 -0
  57. package/dist/replica/mutation-cache.js +91 -0
  58. package/dist/replica/operation-binding.js +17 -6
  59. package/dist/replica/persistence/state.js +9 -1
  60. package/dist/replica/projection-delta/canonical.d.ts +6 -0
  61. package/dist/replica/projection-delta/canonical.js +29 -0
  62. package/dist/replica/projection-delta/index.d.ts +5 -0
  63. package/dist/replica/projection-delta/index.js +3 -0
  64. package/dist/replica/projection-delta/resolve.d.ts +8 -0
  65. package/dist/replica/projection-delta/resolve.js +266 -0
  66. package/dist/replica/projection-delta/types.d.ts +415 -0
  67. package/dist/replica/projection-delta/types.js +6 -0
  68. package/dist/replica/projection-delta/validate.d.ts +18 -0
  69. package/dist/replica/projection-delta/validate.js +1718 -0
  70. package/dist/replica/projection-delta/wasm-pure.d.ts +36 -0
  71. package/dist/replica/projection-delta/wasm-pure.js +71 -0
  72. package/dist/replica/query-plan/pagination.js +18 -8
  73. package/dist/replica/types.d.ts +13 -2
  74. package/dist/sveltekit/replica.d.ts +1 -1
  75. package/dist/sveltekit/replica.js +3 -0
  76. package/dist/sveltekit/vite.d.ts +6 -6
  77. package/dist/sveltekit/vite.js +6 -6
  78. package/package.json +1 -1
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Framework host for pure reduces backed by a wasm-bindgen module.
3
+ *
4
+ * Domain pures validate record/args and return assign fields (or null).
5
+ * This helper only: lazy-load, JSON bridge, fail-closed on any host error.
6
+ */
7
+ import type { ReplicaPureFunction } from './types.js';
8
+ /** Minimal wasm-bindgen module shape used by {@link createWasmJsonPure}. */
9
+ export type WasmJsonModule = {
10
+ default: () => Promise<unknown>;
11
+ [exportName: string]: unknown;
12
+ };
13
+ export type CreateWasmJsonPureOptions = Readonly<{
14
+ /** Dynamic import of the wasm-pack module (e.g. `() => import('./pkg/x.js')`). */
15
+ load: () => Promise<WasmJsonModule>;
16
+ /**
17
+ * Named export that accepts `(recordJson: string, argsJson: string)` and
18
+ * returns a JSON object string of assign fields, or undefined/null to skip.
19
+ */
20
+ exportName: string;
21
+ /** When true (default in browsers), start loading immediately. */
22
+ warm?: boolean;
23
+ }>;
24
+ export type WasmJsonPureHost = Readonly<{
25
+ /** Resolve when the module is instantiated (no-op on non-browser). */
26
+ ensureReady: () => Promise<void>;
27
+ /** Sync pure for `pureFunctions` / pureReduces. */
28
+ pure: ReplicaPureFunction;
29
+ }>;
30
+ /**
31
+ * Build a {@link ReplicaPureFunction} that JSON-roundtrips through a WASM export.
32
+ *
33
+ * All field validation belongs in the WASM/domain pure. The host never inspects
34
+ * individual record/arg keys — missing module or throw → null (fail closed).
35
+ */
36
+ export declare function createWasmJsonPure(options: CreateWasmJsonPureOptions): WasmJsonPureHost;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Framework host for pure reduces backed by a wasm-bindgen module.
3
+ *
4
+ * Domain pures validate record/args and return assign fields (or null).
5
+ * This helper only: lazy-load, JSON bridge, fail-closed on any host error.
6
+ */
7
+ function isBrowser() {
8
+ return typeof globalThis !== 'undefined' && 'window' in globalThis;
9
+ }
10
+ /**
11
+ * Build a {@link ReplicaPureFunction} that JSON-roundtrips through a WASM export.
12
+ *
13
+ * All field validation belongs in the WASM/domain pure. The host never inspects
14
+ * individual record/arg keys — missing module or throw → null (fail closed).
15
+ */
16
+ export function createWasmJsonPure(options) {
17
+ let module = null;
18
+ let initPromise = null;
19
+ const ensureReady = () => {
20
+ if (!isBrowser()) {
21
+ return Promise.resolve();
22
+ }
23
+ if (module) {
24
+ return Promise.resolve();
25
+ }
26
+ if (initPromise) {
27
+ return initPromise;
28
+ }
29
+ initPromise = options
30
+ .load()
31
+ .then(async (loaded) => {
32
+ await loaded.default();
33
+ module = loaded;
34
+ })
35
+ .catch((error) => {
36
+ initPromise = null;
37
+ module = null;
38
+ throw error;
39
+ });
40
+ return initPromise;
41
+ };
42
+ if (options.warm !== false && isBrowser()) {
43
+ void ensureReady().catch(() => {
44
+ /* fail closed on first pure call */
45
+ });
46
+ }
47
+ const pure = (record, args) => {
48
+ if (!module) {
49
+ return null;
50
+ }
51
+ const fn = module[options.exportName];
52
+ if (typeof fn !== 'function') {
53
+ return null;
54
+ }
55
+ try {
56
+ const out = fn(JSON.stringify(record), JSON.stringify(args));
57
+ if (out === undefined || out === null || out === '') {
58
+ return null;
59
+ }
60
+ const parsed = JSON.parse(out);
61
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
62
+ return null;
63
+ }
64
+ return Object.freeze(parsed);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ };
70
+ return Object.freeze({ ensureReady, pure });
71
+ }
@@ -21,19 +21,29 @@ export function decideReplicaPaginationMaintenance(artifact, coverage, change) {
21
21
  }
22
22
  if (coverage.kind === 'offset' &&
23
23
  coverage.offset === 0 &&
24
- coverage.limit !== undefined &&
25
- coverage.returned !== undefined &&
26
- coverage.returned < coverage.limit) {
27
- // A non-full first page proves that the server returned the complete
28
- // ordered set. The index runtime can therefore apply all optimistic
29
- // membership/order changes and truncate back to the declared limit.
30
- return PAGINATION_LOCAL;
24
+ coverage.limit !== undefined) {
25
+ /*
26
+ * First-page (offset 0) locality:
27
+ * - insert: always local when a limit is known. The maintainer
28
+ * re-sorts known members and truncates to the window, so a full
29
+ * lobby page still shows an optimistic chat/todo at the front.
30
+ * - delete/reorder: only when the page is non-full (complete ordered
31
+ * set). A full page with hasNext cannot prove the next boundary
32
+ * after a delete or order change.
33
+ */
34
+ if (change.kind === 'insert') {
35
+ return PAGINATION_LOCAL;
36
+ }
37
+ if (coverage.returned !== undefined &&
38
+ coverage.returned < coverage.limit) {
39
+ return PAGINATION_LOCAL;
40
+ }
31
41
  }
32
42
  }
33
43
  const [code, message] = change.kind === 'insert'
34
44
  ? [
35
45
  'insert_changes_offset_window',
36
- 'an insert is local only for a proven non-full first offset page'
46
+ 'an insert is local only for the first offset page (offset 0) with a known limit'
37
47
  ]
38
48
  : change.kind === 'delete'
39
49
  ? [
@@ -10,6 +10,8 @@ export type ReplicaIndexRecordChange = {
10
10
  readonly model: string;
11
11
  readonly key: string;
12
12
  readonly fields: Readonly<Record<string, ReplicaValue>>;
13
+ readonly unset?: readonly string[];
14
+ readonly ifPresent?: boolean;
13
15
  readonly dependencies?: readonly string[];
14
16
  } | {
15
17
  readonly kind: 'delete';
@@ -432,7 +434,8 @@ export type ReplicaClientSurface = {
432
434
  } | {
433
435
  readonly kind: 'application';
434
436
  readonly name: string;
435
- readonly roles: readonly string[];
437
+ readonly eligible_roles: readonly string[];
438
+ readonly schema_roles: readonly string[];
436
439
  };
437
440
  /** Compiler-owned causal artifact. Protocol and variable identity are inseparable. */
438
441
  export type ReplicaProtocolOperationArtifact<TData = Record<string, unknown>, TVariables extends GraphqlVariables = GraphqlVariables> = ReplicaOperationArtifactBase<TData, TVariables> & {
@@ -441,7 +444,7 @@ export type ReplicaProtocolOperationArtifact<TData = Record<string, unknown>, TV
441
444
  readonly variableCodec: ReplicaVariableCodecArtifact;
442
445
  };
443
446
  export type ReplicaOperationArtifact<TData = Record<string, unknown>, TVariables extends GraphqlVariables = GraphqlVariables> = ReplicaProtocolOperationArtifact<TData, TVariables>;
444
- export type ReplicaWriteSource = 'network' | 'live' | 'ssr' | 'restore' | 'projected';
447
+ export type ReplicaWriteSource = 'network' | 'live' | 'ssr' | 'restore' | 'atomic';
445
448
  export type ReplicaResultEnvelope<TData = unknown> = {
446
449
  readonly data?: TData | null;
447
450
  readonly errors?: readonly GqlError[];
@@ -536,6 +539,7 @@ export type DistributedReplicaOptions = {
536
539
  export type ReplicaAuthoritativeScope = {
537
540
  readonly protocolVersion: 1;
538
541
  readonly schemaHash: string;
542
+ readonly authorizationGeneration: string;
539
543
  readonly cacheScope: string;
540
544
  };
541
545
  /**
@@ -597,6 +601,8 @@ export type ReplicaIndexTarget = {
597
601
  };
598
602
  export type ReplicaRecordPatch = {
599
603
  readonly fields?: Readonly<Record<string, ReplicaValue>>;
604
+ readonly unset?: readonly string[];
605
+ readonly ifPresent?: boolean;
600
606
  readonly links?: Readonly<Record<string, string | readonly string[] | null>>;
601
607
  };
602
608
  export interface ReplicaOptimisticWriter {
@@ -640,6 +646,11 @@ export interface DistributedReplica {
640
646
  * scope for the current SSR response. Returns false on scope/schema mismatch
641
647
  * and never partially restores malformed state. Persisted state must wait for
642
648
  * a fresh server scope and pass that scope here.
649
+ *
650
+ * Cold clients (no active scope) replace from the seed. Warm same-scope
651
+ * re-hydrate merges seed records/indexes and retains confirmed keys the seed
652
+ * omitted—soft navigation must not wipe session cache because a route only
653
+ * dehydrated its page subset. Auth/scope change still purges.
643
654
  */
644
655
  hydrate(state: ReplicaDehydratedState, authoritativeScope: ReplicaAuthoritativeScope): boolean;
645
656
  createOptimisticLayer(id: string, update: (writer: ReplicaOptimisticWriter) => void, semanticChanges?: readonly ReplicaIndexSemanticChange[]): void;
@@ -68,7 +68,7 @@ export type CreateDistributedSvelteKitOptions<TCommands = Readonly<Record<never,
68
68
  export type SveltekitQuerySnapshot<TData> = ReplicaSnapshot<TData> & Readonly<{
69
69
  loading: boolean;
70
70
  error?: GqlError;
71
- /** Causally accepted commands still waiting for projection visibility. */
71
+ /** Successful causal commands still waiting for projection visibility. */
72
72
  pending: readonly ReplicaCommandReceipt<unknown>[];
73
73
  refetch(): Promise<void>;
74
74
  }>;
@@ -478,6 +478,8 @@ function validatedHydrationAuthority(value) {
478
478
  scope.protocolVersion !== 1 ||
479
479
  typeof scope.schemaHash !== 'string' ||
480
480
  scope.schemaHash.length === 0 ||
481
+ typeof scope.authorizationGeneration !== 'string' ||
482
+ scope.authorizationGeneration.length === 0 ||
481
483
  typeof scope.cacheScope !== 'string' ||
482
484
  scope.cacheScope.length === 0) {
483
485
  throw new TypeError('Distributed SvelteKit hydration authority is invalid');
@@ -487,5 +489,6 @@ function validatedHydrationAuthority(value) {
487
489
  function sameReplicaScope(left, right) {
488
490
  return (left.protocolVersion === right.protocolVersion &&
489
491
  left.schemaHash === right.schemaHash &&
492
+ left.authorizationGeneration === right.authorizationGeneration &&
490
493
  left.cacheScope === right.cacheScope);
491
494
  }
@@ -12,7 +12,7 @@ export declare function distributedGraphqlProxy(options: DistributedGraphqlProxy
12
12
  }>;
13
13
  export type DistributedSvelteKitManifestSource = string | Readonly<{
14
14
  /**
15
- * Arguments passed to the configured dctl command. The first value
15
+ * Arguments passed to the configured distributed command. The first value
16
16
  * must be `client-manifest`; stdout becomes ephemeral compiler input.
17
17
  */
18
18
  args: readonly string[];
@@ -20,13 +20,13 @@ export type DistributedSvelteKitManifestSource = string | Readonly<{
20
20
  export type DistributedSvelteKitClientCompiler = Readonly<{
21
21
  /** `$distributed` or an explicit elevated entrypoint such as `$distributed/admin`. */
22
22
  module: string;
23
- /** Existing manifest path, or canonical `dctl client-manifest` argv. */
23
+ /** Existing manifest path, or canonical `distributed client-manifest` argv. */
24
24
  manifest: DistributedSvelteKitManifestSource;
25
25
  /** Verify exactly one concrete role. Mutually exclusive with `surface`. */
26
26
  role?: string;
27
27
  /** Verify exactly one Rust-declared application surface. */
28
28
  surface?: string;
29
- /** GraphQL globs passed verbatim as repeated `dctl client --documents`. */
29
+ /** GraphQL globs passed verbatim as repeated `distributed client --documents`. */
30
30
  documents: readonly string[];
31
31
  /** Explicit `OPERATION=/route` fallbacks. */
32
32
  routes?: readonly string[];
@@ -34,9 +34,9 @@ export type DistributedSvelteKitClientCompiler = Readonly<{
34
34
  out: string;
35
35
  }>;
36
36
  export type DistributedSvelteKitViteOptions = Readonly<{
37
- /** Project root used for dctl cwd, document globs, and output containment. */
37
+ /** Project root used for distributed cwd, document globs, and output containment. */
38
38
  cwd?: string;
39
- /** Executable invoked without a shell. Defaults to `dctl`. */
39
+ /** Executable invoked without a shell. Defaults to `distributed`. */
40
40
  command?: string;
41
41
  /** Prefix argv, e.g. `cargo run ... --`; never interpreted by a shell. */
42
42
  commandArgs?: readonly string[];
@@ -83,7 +83,7 @@ export type DistributedSvelteKitVitePlugin = Readonly<{
83
83
  }>;
84
84
  /** Generate every configured surface through the same transaction used by Vite. */
85
85
  export declare function generateDistributedSvelteKit(options: DistributedSvelteKitViteOptions): Promise<void>;
86
- /** Check every configured surface through canonical `dctl client --check`; never write. */
86
+ /** Check every configured surface through canonical `distributed client --check`; never write. */
87
87
  export declare function checkDistributedSvelteKit(options: DistributedSvelteKitViteOptions): Promise<void>;
88
88
  /**
89
89
  * Compiler-watching Vite integration for one or more authorization surfaces.
@@ -26,7 +26,7 @@ export function distributedGraphqlProxy(options) {
26
26
  export async function generateDistributedSvelteKit(options) {
27
27
  await runCompilerOnce(options, 'generate');
28
28
  }
29
- /** Check every configured surface through canonical `dctl client --check`; never write. */
29
+ /** Check every configured surface through canonical `distributed client --check`; never write. */
30
30
  export async function checkDistributedSvelteKit(options) {
31
31
  await runCompilerOnce(options, 'check');
32
32
  }
@@ -219,7 +219,7 @@ async function runCompilerOnce(options, mode) {
219
219
  * generation and is safe to call from `svelte.config.js`.
220
220
  */
221
221
  export function distributedSvelteKitAliases(options) {
222
- const integration = resolveIntegration({ ...options, command: 'dctl', commandArgs: [] }, options.cwd ?? process.cwd());
222
+ const integration = resolveIntegration({ ...options, command: 'distributed', commandArgs: [] }, options.cwd ?? process.cwd());
223
223
  validateResolvedPathsSync(integration);
224
224
  return Object.freeze(Object.fromEntries([...integration.clients]
225
225
  .sort((left, right) => right.module.length - left.module.length ||
@@ -231,7 +231,7 @@ function resolveIntegration(options, fallbackCwd) {
231
231
  throw new TypeError('distributedSvelteKit requires configuration');
232
232
  }
233
233
  const cwd = resolve(options.cwd ?? fallbackCwd);
234
- const command = (options.command ?? 'dctl').trim();
234
+ const command = (options.command ?? 'distributed').trim();
235
235
  if (command.length === 0) {
236
236
  throw new TypeError('Distributed SvelteKit command must not be empty');
237
237
  }
@@ -467,7 +467,7 @@ async function materializeManifest(integration, client, transaction, index, chil
467
467
  JSON.parse(result.stdout);
468
468
  }
469
469
  catch (error) {
470
- throw new Error(`dctl client-manifest for ${client.module} did not emit valid JSON`, { cause: error });
470
+ throw new Error(`distributed client-manifest for ${client.module} did not emit valid JSON`, { cause: error });
471
471
  }
472
472
  const temporary = join(transaction, `manifest-${index}.json.tmp`);
473
473
  const manifest = join(transaction, `manifest-${index}.json`);
@@ -483,11 +483,11 @@ async function validateGeneratedEntrypoint(cwd, output, module) {
483
483
  const entry = join(output, GENERATED_SVELTEKIT_MODULE);
484
484
  const metadata = await lstat(entry);
485
485
  if (metadata.isSymbolicLink() || !metadata.isFile()) {
486
- throw new Error(`dctl client for ${module} did not emit a regular ${GENERATED_SVELTEKIT_MODULE}`);
486
+ throw new Error(`distributed client for ${module} did not emit a regular ${GENERATED_SVELTEKIT_MODULE}`);
487
487
  }
488
488
  const canonical = await realpath(entry);
489
489
  if (!isWithin(root, canonical)) {
490
- throw new Error(`dctl client entrypoint ${canonical} escaped project root ${root}`);
490
+ throw new Error(`distributed client entrypoint ${canonical} escaped project root ${root}`);
491
491
  }
492
492
  }
493
493
  async function commitOutputs(integration, staged, signal) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hops-ops/distributed",
3
- "version": "0.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Typed GraphQL client, causal replica, command runtime, and framework adapters for Distributed services",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",