@astrale-os/sdk 0.6.0-beta.41 → 0.6.0-beta.42

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 (175) hide show
  1. package/README.md +22 -2
  2. package/dist/application/action/context.d.ts +49 -18
  3. package/dist/application/action/index.d.ts +1 -1
  4. package/dist/application/error/error.d.ts +16 -1
  5. package/dist/application/error/error.js +39 -5
  6. package/dist/application/error/index.d.ts +1 -1
  7. package/dist/application/error/index.js +1 -1
  8. package/dist/application/mutation/authoring/builder.d.ts +81 -33
  9. package/dist/application/mutation/authoring/builder.js +100 -30
  10. package/dist/application/mutation/authoring/guard.d.ts +37 -0
  11. package/dist/application/mutation/authoring/guard.js +150 -0
  12. package/dist/application/mutation/authoring/index.d.ts +5 -2
  13. package/dist/application/mutation/authoring/index.js +2 -1
  14. package/dist/application/mutation/authoring/names.d.ts +19 -0
  15. package/dist/application/mutation/authoring/names.js +83 -0
  16. package/dist/application/mutation/authoring/transition.d.ts +3 -2
  17. package/dist/application/mutation/define.d.ts +24 -10
  18. package/dist/application/mutation/define.js +49 -53
  19. package/dist/application/mutation/execution/execute.d.ts +2 -2
  20. package/dist/application/mutation/execution/execute.js +78 -7
  21. package/dist/application/mutation/execution/executor.d.ts +4 -1
  22. package/dist/application/mutation/execution/executor.js +6 -1
  23. package/dist/application/mutation/failure.d.ts +11 -1
  24. package/dist/application/mutation/failure.js +18 -3
  25. package/dist/application/mutation/index.d.ts +8 -4
  26. package/dist/application/mutation/index.js +5 -2
  27. package/dist/application/mutation/legacy/v6/builder.d.ts +84 -0
  28. package/dist/application/mutation/legacy/v6/define.d.ts +18 -0
  29. package/dist/application/mutation/legacy/v6/define.js +56 -0
  30. package/dist/application/mutation/legacy/v6/mutation.d.ts +28 -0
  31. package/dist/application/mutation/legacy/v6/mutation.js +1 -0
  32. package/dist/application/mutation/{authoring → legacy/v6}/precondition.d.ts +4 -0
  33. package/dist/application/mutation/mutation.d.ts +30 -19
  34. package/dist/application/query/answer/answer.d.ts +117 -0
  35. package/dist/application/query/answer/answer.js +1 -0
  36. package/dist/application/query/answer/conformance.d.ts +4 -0
  37. package/dist/application/query/answer/conformance.js +27 -0
  38. package/dist/application/query/answer/facts.d.ts +17 -0
  39. package/dist/application/query/answer/facts.js +57 -0
  40. package/dist/application/query/composite/builder.d.ts +1 -1
  41. package/dist/application/query/composite/builder.js +1 -1
  42. package/dist/application/query/composite/plan.d.ts +1 -1
  43. package/dist/application/query/composite/plan.js +1 -1
  44. package/dist/application/query/composite/query.d.ts +1 -2
  45. package/dist/application/query/define.d.ts +2 -33
  46. package/dist/application/query/define.js +3 -130
  47. package/dist/application/query/definition.d.ts +12 -0
  48. package/dist/application/query/definition.js +47 -0
  49. package/dist/application/query/execution/bounds.d.ts +20 -0
  50. package/dist/application/query/execution/bounds.js +86 -0
  51. package/dist/application/query/execution/crossings.d.ts +25 -0
  52. package/dist/application/query/execution/crossings.js +169 -0
  53. package/dist/application/query/execution/error.d.ts +68 -0
  54. package/dist/application/query/execution/error.js +50 -0
  55. package/dist/application/query/execution/execute.d.ts +2 -7
  56. package/dist/application/query/execution/execute.js +3 -51
  57. package/dist/application/query/execution/recipe.d.ts +70 -0
  58. package/dist/application/query/execution/recipe.js +276 -0
  59. package/dist/application/query/execution/refusal.d.ts +37 -0
  60. package/dist/application/query/execution/refusal.js +80 -0
  61. package/dist/application/query/execution/rows.d.ts +42 -0
  62. package/dist/application/query/execution/rows.js +308 -0
  63. package/dist/application/query/execution/split.d.ts +24 -0
  64. package/dist/application/query/execution/split.js +190 -0
  65. package/dist/application/query/handles/build.d.ts +10 -0
  66. package/dist/application/query/handles/build.js +53 -0
  67. package/dist/application/query/handles/conditions.d.ts +94 -0
  68. package/dist/application/query/handles/conditions.js +1 -0
  69. package/dist/application/query/handles/domain.d.ts +157 -0
  70. package/dist/application/query/handles/domain.js +1 -0
  71. package/dist/application/query/handles/handles.d.ts +268 -0
  72. package/dist/application/query/handles/handles.js +1 -0
  73. package/dist/application/query/handles/model.d.ts +96 -0
  74. package/dist/application/query/handles/model.js +246 -0
  75. package/dist/application/query/handles/plan.d.ts +99 -0
  76. package/dist/application/query/handles/plan.js +37 -0
  77. package/dist/application/query/handles/reads.d.ts +106 -0
  78. package/dist/application/query/handles/reads.js +839 -0
  79. package/dist/application/query/handles/relations.d.ts +100 -0
  80. package/dist/application/query/handles/relations.js +1 -0
  81. package/dist/application/query/handles/rows.d.ts +94 -0
  82. package/dist/application/query/handles/rows.js +4 -0
  83. package/dist/application/query/handles/terms.d.ts +95 -0
  84. package/dist/application/query/handles/terms.js +265 -0
  85. package/dist/application/query/index.d.ts +10 -7
  86. package/dist/application/query/index.js +9 -4
  87. package/dist/application/query/language/index.d.ts +2 -0
  88. package/dist/application/query/language/index.js +2 -0
  89. package/dist/application/query/language/query.d.ts +323 -0
  90. package/dist/application/query/language/query.js +2 -0
  91. package/dist/application/query/{collection → legacy/v6/collection}/identity.d.ts +2 -2
  92. package/dist/application/query/{collection → legacy/v6/collection}/metadata.d.ts +1 -1
  93. package/dist/application/query/{collection → legacy/v6/collection}/union.d.ts +3 -3
  94. package/dist/application/query/{collection → legacy/v6/collection}/union.js +1 -1
  95. package/dist/application/query/{conformance.d.ts → legacy/v6/conformance.d.ts} +3 -3
  96. package/dist/application/query/{conformance.js → legacy/v6/conformance.js} +1 -1
  97. package/dist/application/query/legacy/v6/define.d.ts +37 -0
  98. package/dist/application/query/legacy/v6/define.js +90 -0
  99. package/dist/application/query/legacy/v6/execution/execute.d.ts +16 -0
  100. package/dist/application/query/legacy/v6/execution/execute.js +50 -0
  101. package/dist/application/query/legacy/v6/index.d.ts +6 -0
  102. package/dist/application/query/legacy/v6/index.js +3 -0
  103. package/dist/application/query/legacy/v6/kernel.d.ts +6 -0
  104. package/dist/application/query/legacy/v6/kernel.js +5 -0
  105. package/dist/application/query/{single → legacy/v6/single}/pagination.js +1 -1
  106. package/dist/application/query/{single → legacy/v6/single}/query.d.ts +2 -14
  107. package/dist/application/query/legacy/v6/single/query.js +1 -0
  108. package/dist/application/query/query.d.ts +16 -1
  109. package/dist/application/query/recipe/define.d.ts +45 -0
  110. package/dist/application/query/recipe/define.js +102 -0
  111. package/dist/application/query/recipe/recipe.d.ts +134 -0
  112. package/dist/application/query/recipe/recipe.js +1 -0
  113. package/dist/application/workflow/durable.d.ts +22 -8
  114. package/dist/execution/invocation/capabilities/bind.d.ts +6 -4
  115. package/dist/execution/invocation/capabilities/bind.js +39 -3
  116. package/dist/execution/invocation/dispatch/action.js +4 -2
  117. package/dist/execution/invocation/dispatch/context.js +1 -1
  118. package/dist/execution/invocation/dispatch/workflow.js +4 -2
  119. package/dist/execution/invocation/result/index.d.ts +1 -0
  120. package/dist/execution/invocation/result/index.js +1 -0
  121. package/dist/execution/invocation/result/recipe.d.ts +6 -0
  122. package/dist/execution/invocation/result/recipe.js +17 -0
  123. package/dist/execution/service/create.js +1 -0
  124. package/dist/execution/workflows/failure.d.ts +23 -6
  125. package/dist/execution/workflows/failure.js +78 -18
  126. package/dist/execution/workflows/receiver.d.ts +3 -0
  127. package/dist/execution/workflows/receiver.js +1 -0
  128. package/dist/execution/workflows/run.js +13 -9
  129. package/dist/execution/workflows/steps.d.ts +3 -0
  130. package/dist/execution/workflows/steps.js +2 -2
  131. package/dist/platform/schema/kernel-compiled.d.ts +2 -0
  132. package/dist/platform/schema/kernel-compiled.js +2 -0
  133. package/dist/testing/index.d.ts +2 -0
  134. package/dist/testing/index.js +1 -0
  135. package/dist/testing/recipe.d.ts +17 -0
  136. package/dist/testing/recipe.js +29 -0
  137. package/dist/tooling/cli/orchestrate.js +6 -2
  138. package/dist/tooling/linter/adapters/typescript/index.d.ts +2 -1
  139. package/dist/tooling/linter/adapters/typescript/index.js +2 -1
  140. package/dist/tooling/linter/adapters/typescript/query-observation.js +37 -12
  141. package/dist/tooling/linter/adapters/typescript/recipe-definitions.d.ts +34 -0
  142. package/dist/tooling/linter/adapters/typescript/recipe-definitions.js +88 -0
  143. package/dist/tooling/linter/adapters/typescript/type-analysis.d.ts +2 -0
  144. package/dist/tooling/linter/adapters/typescript/type-analysis.js +10 -0
  145. package/dist/tooling/linter/implementations/source/global.js +15 -9
  146. package/dist/tooling/linter/implementations/source/mutations.js +128 -19
  147. package/dist/tooling/linter/implementations/source/policy-edges.d.ts +2 -0
  148. package/dist/tooling/linter/implementations/source/policy-edges.js +113 -0
  149. package/dist/tooling/linter/implementations/source/recipes.d.ts +2 -0
  150. package/dist/tooling/linter/implementations/source/recipes.js +409 -0
  151. package/dist/tooling/linter/implementations/source/shared.d.ts +7 -1
  152. package/dist/tooling/linter/implementations/source/shared.js +31 -10
  153. package/dist/tooling/linter/implementations/source/verifiers.js +4 -0
  154. package/dist/tooling/linter/policy/generated.js +2 -2
  155. package/dist/tooling/linter/requirements/registry.js +4 -0
  156. package/docs/durable-workflows.md +48 -20
  157. package/docs/handler-dependencies.md +76 -4
  158. package/docs/legacy/query-v6.md +62 -0
  159. package/docs/queries.md +438 -0
  160. package/package.json +11 -8
  161. /package/dist/application/{query/single/query.js → mutation/legacy/v6/builder.js} +0 -0
  162. /package/dist/application/mutation/{authoring → legacy/v6}/precondition.js +0 -0
  163. /package/dist/application/query/{collection → legacy/v6/collection}/identity.js +0 -0
  164. /package/dist/application/query/{collection → legacy/v6/collection}/limits.d.ts +0 -0
  165. /package/dist/application/query/{collection → legacy/v6/collection}/limits.js +0 -0
  166. /package/dist/application/query/{collection → legacy/v6/collection}/metadata.js +0 -0
  167. /package/dist/application/query/{execution → legacy/v6/execution}/pages.d.ts +0 -0
  168. /package/dist/application/query/{execution → legacy/v6/execution}/pages.js +0 -0
  169. /package/dist/application/query/{single → legacy/v6/single}/index.d.ts +0 -0
  170. /package/dist/application/query/{single → legacy/v6/single}/index.js +0 -0
  171. /package/dist/application/query/{single → legacy/v6/single}/node.d.ts +0 -0
  172. /package/dist/application/query/{single → legacy/v6/single}/node.js +0 -0
  173. /package/dist/application/query/{single → legacy/v6/single}/pagination.d.ts +0 -0
  174. /package/dist/application/query/{single → legacy/v6/single}/selection.d.ts +0 -0
  175. /package/dist/application/query/{single → legacy/v6/single}/selection.js +0 -0
@@ -0,0 +1,50 @@
1
+ function sentence(code, details) {
2
+ switch (code) {
3
+ case 'NOT_FOUND':
4
+ return details.target === undefined
5
+ ? `no ${String(details.class)} was found`
6
+ : `no ${String(details.class)} ${String(details.target)} was found`;
7
+ case 'NOT_READABLE':
8
+ return `${String(details.class)} ${typeof details.id === 'string' ? details.id : JSON.stringify(details.id)} may not be read`;
9
+ case 'INTEGRITY': {
10
+ const of = details.parent === undefined ? '' : ` of ${String(details.parent)}`;
11
+ return details.found === 0
12
+ ? `${String(details.relation)}${of} found none where one is required`
13
+ : `${String(details.relation)}${of} found a second row where one is allowed`;
14
+ }
15
+ case 'TOO_MANY':
16
+ return `past ${String(details.bound)} ${details.limit === 'max' ? 'rows (max)' : String(details.limit)}`;
17
+ case 'REFUSED':
18
+ return `the Kernel refused the read (${String(details.kernelCode)})`;
19
+ case 'INPUT_INVALID':
20
+ return details.limit === 'in'
21
+ ? `an in takes at most 16 values (at ${String(details.path)})`
22
+ : `the Kernel refused the query (${String(details.kernelCode)}${details.path === undefined ? '' : ` at ${String(details.path)}`})`;
23
+ case 'OUTDATED':
24
+ return "the Host's Registry no longer matches this Domain's build";
25
+ case 'UNSUPPORTED':
26
+ return details.feature === 'v7'
27
+ ? 'the Host does not serve Query v7'
28
+ : 'the Host does not give the Class of each row';
29
+ }
30
+ }
31
+ export class QueryError extends Error {
32
+ code;
33
+ recipe;
34
+ details;
35
+ name = 'QueryError';
36
+ constructor(code,
37
+ /** The recipe's id. */
38
+ recipe, details, options) {
39
+ const reason = options?.reason === undefined ? '' : `: ${options.reason}`;
40
+ super(`${recipe}: ${sentence(code, details)}${reason}`, options);
41
+ this.code = code;
42
+ this.recipe = recipe;
43
+ this.details = details;
44
+ Object.freeze(this.details);
45
+ }
46
+ }
47
+ /** Whether `error` is a `QueryError`, of `code` if given. */
48
+ export function isQueryError(error, code) {
49
+ return error instanceof QueryError && (code === undefined || error.code === code);
50
+ }
@@ -1,13 +1,8 @@
1
- import type { QueryPageRequest, QueryResponse } from '@astrale-os/kernel-client/graph';
2
- import type { QueryAST, QueryResultFor } from '@astrale-os/kernel-core/graph/query';
3
1
  import type { Domain } from '../../../platform/schema/index.js';
4
2
  import type { CompositeQuery } from '../composite/index.js';
3
+ import type { QueryClient } from '../legacy/v6/execution/execute.js';
5
4
  import type { QueryDefinition } from '../query.js';
6
- export interface QueryClient {
7
- query<Ast extends QueryAST>(ast: Ast, options: {
8
- readonly page: QueryPageRequest;
9
- }): Promise<QueryResponse<QueryResultFor<Ast>>>;
10
- }
5
+ export type { QueryClient } from '../legacy/v6/execution/execute.js';
11
6
  export declare function executeQuery<DomainValue extends Domain, Output>(client: QueryClient, domain: DomainValue, query: QueryDefinition<DomainValue, void, Output>): Promise<Output>;
12
7
  export declare function executeQuery<DomainValue extends Domain, Input, Output>(client: QueryClient, domain: DomainValue, query: QueryDefinition<DomainValue, Input, Output>, input: Input): Promise<Output>;
13
8
  export type ExecutableCompositeQuery = CompositeQuery<unknown, unknown, unknown>;
@@ -1,21 +1,13 @@
1
- import { COLLECTION_QUERY_LIMITS } from '../collection/limits.js';
2
- import { compileCollectionUnion, partitionCollectionUnion } from '../collection/union.js';
3
1
  import { createQueryComposition, executeQueryPlan } from '../composite/index.js';
4
- import { requireConformance } from '../conformance.js';
5
2
  import { realizeQuery } from '../define.js';
3
+ import { isPromiseLike } from '../definition.js';
6
4
  import { queryFailure } from '../failure.js';
7
- import { completeSelection, queryResult } from '../single/index.js';
8
- import { executePages } from './pages.js';
5
+ import { conformingClient, realizeSingle, runCollectionUnion, runSingle, } from '../legacy/v6/execution/execute.js';
9
6
  export async function executeQuery(client, domain, query, input) {
10
7
  if (client === null || typeof client !== 'object' || typeof client.query !== 'function') {
11
8
  throw new TypeError('Query execution requires a Client query capability.');
12
9
  }
13
- // The Kernel answers in its active Schema; each value is checked against this client's Domain.
14
- const execute = async (ast, options) => {
15
- const response = await client.query(ast, options);
16
- requireConformance(domain, response.result);
17
- return response;
18
- };
10
+ const execute = conformingClient(client, domain);
19
11
  const operation = realizeQuery(query, domain);
20
12
  try {
21
13
  if (operation.kind === 'single') {
@@ -38,43 +30,3 @@ export async function executeQuery(client, domain, query, input) {
38
30
  throw cause;
39
31
  }
40
32
  }
41
- async function runCollectionUnion(execute, domain, definitions, outputPath) {
42
- const compiled = compileCollectionUnion(domain, definitions, outputPath);
43
- const pages = await executePages(execute, compiled.ast, { size: COLLECTION_QUERY_LIMITS.pageSize }, { maximumPages: COLLECTION_QUERY_LIMITS.maximumPages });
44
- const context = outputPath.length === 0 ? 'collection union' : outputPath.join('.');
45
- const values = compiled.kind === 'node'
46
- ? queryResult.completeNodeValues(pages, context)
47
- : Object.freeze(pages.flatMap((page) => queryResult.edgeItems(page, context).map((item) => {
48
- if (item.edge.kind !== 'value') {
49
- throw new TypeError(`${context} must project Edge values.`);
50
- }
51
- return item.edge.value;
52
- })));
53
- return partitionCollectionUnion(domain, compiled, values);
54
- }
55
- function realizeSingle(definition, domain) {
56
- const operation = realizeQuery(definition, domain);
57
- if (operation.kind !== 'single')
58
- throw new TypeError('Composite Query leaves must be single.');
59
- return operation;
60
- }
61
- async function runSingle(execute, definition, input) {
62
- const prepared = definition.prepare?.(input) ?? input;
63
- const ast = definition.build(prepared);
64
- if (definition.pagination !== undefined) {
65
- const collected = await executePages(execute, ast, definition.page, definition.pagination);
66
- return definition.project(collected, prepared, {
67
- ast,
68
- complete: completeSelection(collected),
69
- });
70
- }
71
- const response = await execute(ast, { page: definition.page });
72
- return definition.project === undefined
73
- ? response.result
74
- : definition.project(response.result, prepared, { ast });
75
- }
76
- function isPromiseLike(input) {
77
- return (input !== null &&
78
- (typeof input === 'object' || typeof input === 'function') &&
79
- typeof input.then === 'function');
80
- }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Run a v7 recipe (WP-S3): its stages in order, each one's reads together, at most 8 in flight,
3
+ * each read at its own snapshot (the stand-in); then `output`. A stage's bounds (S11) are checked
4
+ * before it sends anything, and every Kernel refusal of a read is a `QueryError`.
5
+ */
6
+ import type { Domain } from '../../../platform/schema/index.js';
7
+ import type { ReadPlan } from '../handles/plan.js';
8
+ import type { AnyQuery } from '../language/query.js';
9
+ import type { Errors, InputOf, OutputContext, OutputOf, Recipe } from '../recipe/recipe.js';
10
+ import { type Model } from '../handles/model.js';
11
+ import { type DeclaredClass } from './crossings.js';
12
+ /** What a recipe sends a read with: the cursor a page continues. */
13
+ export interface RecipeQueryOptions {
14
+ readonly paging?: {
15
+ readonly after: string;
16
+ };
17
+ }
18
+ /** The client a recipe reads through: one v7 document per request. */
19
+ export interface RecipeClient {
20
+ query(document: AnyQuery, options: RecipeQueryOptions): Promise<unknown>;
21
+ }
22
+ /** A page recipe's call: its size, from 1 to its `max` (by default), and the cursor it continues. */
23
+ export interface RecipeCallOptions {
24
+ readonly size?: number;
25
+ readonly after?: string;
26
+ }
27
+ /** A recipe call's options: the authority it reads under, and a page recipe's size and cursor. */
28
+ export type RecipeOptions<R, As extends string> = {
29
+ readonly as: As;
30
+ } & ('~page' extends keyof R ? RecipeCallOptions : {});
31
+ /** One recipe call: its reads so far, against S11, and the Classes they cross, against S12. */
32
+ export declare class Run {
33
+ #private;
34
+ readonly client: RecipeClient;
35
+ readonly domain: Domain;
36
+ readonly recipe: string;
37
+ readonly model: Model;
38
+ constructor(client: RecipeClient, domain: Domain, recipe: string, declared?: readonly DeclaredClass[]);
39
+ /**
40
+ * S11, before a stage sends: its reads, its verbs' bounds, its rows and each property `in` its
41
+ * documents write. Then S12: each foreign Class its relations cross.
42
+ */
43
+ admit(plans: readonly ReadPlan[]): void;
44
+ /** One read's result: its verb's empty answer, or its documents' answers merged and rendered. */
45
+ read(plan: ReadPlan, paging?: RecipeCallOptions): Promise<unknown>;
46
+ }
47
+ /** Where each facet's results go: one read's index, a list of them, or nothing. */
48
+ type Shape = readonly (readonly [string, number | readonly number[] | undefined])[];
49
+ /**
50
+ * The reads of one stage, admitted (S11, S12): a single read, or each facet's in order and the
51
+ * shape that puts their results back. A facet takes a name no `earlier` stage read: the merged
52
+ * results keep one value per name.
53
+ */
54
+ export declare function planStage(run: Run, built: unknown, single: boolean, earlier?: Readonly<Record<string, unknown>>): {
55
+ readonly plans: readonly ReadPlan[];
56
+ readonly shape?: Shape;
57
+ };
58
+ /**
59
+ * Run a recipe for `input` against `domain`, the resolved Domain it was written for: its stages,
60
+ * then its `output`. A page recipe takes its call's `size` and `after`. A read the Kernel refuses
61
+ * raises a `QueryError`; `output` raises the recipe's declared errors. With the Class requirements
62
+ * the Domain's build `declared`, each stage's relations are checked against them first (S12).
63
+ */
64
+ export declare function runQueryRecipe<R extends Recipe>(client: RecipeClient, domain: Domain, recipe: R, input: InputOf<R>, call?: RecipeCallOptions, declared?: readonly DeclaredClass[]): Promise<OutputOf<R>>;
65
+ /**
66
+ * The context a recipe's `output`, or a Mutation recipe's `reject`, receives: its declared errors,
67
+ * and `is`, which narrows a row by Class.
68
+ */
69
+ export declare function outputContext(recipe: object, model: Model): OutputContext<unknown, Errors>;
70
+ export {};
@@ -0,0 +1,276 @@
1
+ import * as errors from '@astrale-os/kernel-protocol/errors';
2
+ import { isSnapshotStale, ResponseError, retryAfterOf } from '../../../platform/client/index.js';
3
+ import { declaredErrorFactory } from '../../error/error.js';
4
+ import { requireAnswerConformance } from '../answer/conformance.js';
5
+ import { handlesOf } from '../handles/build.js';
6
+ import { modelOf } from '../handles/model.js';
7
+ import { isRead, planOf } from '../handles/plan.js';
8
+ import { describe } from '../handles/terms.js';
9
+ import { recipeSpecOf } from '../recipe/define.js';
10
+ import { MAXIMUM_BOUND, MAXIMUM_READS, MAXIMUM_ROWS, pastPage, rowsOf, wideIn } from './bounds.js';
11
+ import { Crossings } from './crossings.js';
12
+ import { QueryError } from './error.js';
13
+ import { describeRefusals, isUnserved, refusalsOf } from './refusal.js';
14
+ import { emptyResult, rootResult } from './rows.js';
15
+ import { merge, split } from './split.js';
16
+ /** Reads of one stage in flight at once. */
17
+ const IN_FLIGHT = 8;
18
+ /** `QUERY_SNAPSHOT_STALE`: nothing was read, so the read is sent again at once, this many times. */
19
+ const STALE_RESENDS = 3;
20
+ /** A refusal's protocol code by name, for one that gives no reason. */
21
+ const CODE_NAMES = new Map(Object.entries(errors.CODES).map(([name, code]) => [code, name]));
22
+ /** One recipe call: its reads so far, against S11, and the Classes they cross, against S12. */
23
+ export class Run {
24
+ client;
25
+ domain;
26
+ recipe;
27
+ #reads = 0;
28
+ #rows = 0;
29
+ model;
30
+ #crossings;
31
+ constructor(client, domain, recipe, declared) {
32
+ this.client = client;
33
+ this.domain = domain;
34
+ this.recipe = recipe;
35
+ this.model = modelOf(domain);
36
+ this.#crossings =
37
+ declared === undefined ? undefined : new Crossings(this.model, domain, declared);
38
+ }
39
+ /**
40
+ * S11, before a stage sends: its reads, its verbs' bounds, its rows and each property `in` its
41
+ * documents write. Then S12: each foreign Class its relations cross.
42
+ */
43
+ admit(plans) {
44
+ this.#reads += plans.length;
45
+ if (this.#reads > MAXIMUM_READS)
46
+ throw new QueryError('TOO_MANY', this.recipe, { limit: 'reads', bound: MAXIMUM_READS });
47
+ if (plans.some(pastPage))
48
+ throw new QueryError('TOO_MANY', this.recipe, { limit: 'rows', bound: MAXIMUM_BOUND });
49
+ this.#rows += plans.reduce((total, plan) => total + rowsOf(plan), 0);
50
+ if (this.#rows > MAXIMUM_ROWS)
51
+ throw new QueryError('TOO_MANY', this.recipe, { limit: 'rows', bound: MAXIMUM_ROWS });
52
+ for (const plan of plans) {
53
+ const path = plan.document === undefined ? undefined : wideIn(plan.document);
54
+ // Into the request `{ document, paging }`, as the Kernel's refusals point.
55
+ if (path !== undefined)
56
+ throw new QueryError('INPUT_INVALID', this.recipe, {
57
+ limit: 'in',
58
+ path: `/document${path}`,
59
+ });
60
+ }
61
+ for (const plan of plans)
62
+ this.#crossings?.check(this.recipe, plan);
63
+ }
64
+ /** One read's result: its verb's empty answer, or its documents' answers merged and rendered. */
65
+ async read(plan, paging) {
66
+ if (plan.empty)
67
+ return emptyResult(this.recipe, plan);
68
+ let document = plan.document;
69
+ if (plan.verb === 'page' && paging?.size !== undefined)
70
+ document = withLimit(document, paging.size);
71
+ const documents = plan.verb === 'page' || plan.verb === 'document'
72
+ ? [document]
73
+ : split(document, document.list?.orderBy !== undefined);
74
+ const answers = [];
75
+ for (const sent of documents) {
76
+ const answer = await this.#send(sent, paging?.after);
77
+ if ('list' in answer)
78
+ requireAnswerConformance(this.domain, answer);
79
+ answers.push(answer);
80
+ }
81
+ return rootResult(this.recipe, this.model, plan, merge(document, answers));
82
+ }
83
+ async #send(document, after) {
84
+ const paging = after === undefined ? {} : { paging: { after } };
85
+ for (let attempt = 0;; attempt += 1) {
86
+ try {
87
+ return await this.client.query(document, paging);
88
+ }
89
+ catch (error) {
90
+ if (isSnapshotStale(error) && attempt < STALE_RESENDS)
91
+ continue;
92
+ throw this.#refusal(error);
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * A Kernel refusal as a `QueryError`. A network failure, or a refusal that prescribes when to
98
+ * send again (capacity, draining), is not the read's: it passes as it came, for the caller to
99
+ * retry.
100
+ */
101
+ #refusal(error) {
102
+ if (!(error instanceof ResponseError) || retryAfterOf(error) !== undefined)
103
+ return error;
104
+ const kernelCode = error.reason?.code ?? CODE_NAMES.get(error.code) ?? String(error.code);
105
+ const details = (error.reason?.details ?? {});
106
+ const cause = { cause: error };
107
+ if (isSnapshotStale(error))
108
+ return new QueryError('REFUSED', this.recipe, { kernelCode, retryable: true }, cause);
109
+ const refusals = refusalsOf(error);
110
+ if (refusals !== undefined) {
111
+ const path = refusals[0]?.path ?? (typeof details.path === 'string' ? details.path : undefined);
112
+ return new QueryError('INPUT_INVALID', this.recipe, {
113
+ kernelCode,
114
+ ...(typeof details.phase === 'string' ? { phase: details.phase } : {}),
115
+ ...(path === undefined ? {} : { path }),
116
+ }, { ...cause, ...(refusals.length === 0 ? {} : { reason: describeRefusals(refusals) }) });
117
+ }
118
+ if (kernelCode === 'QUERY_EXPECTATION_MISMATCH')
119
+ return new QueryError('OUTDATED', this.recipe, { kernelCode: 'QUERY_EXPECTATION_MISMATCH' }, cause);
120
+ // A Kernel that predates v7 refuses its request at the Function's input.
121
+ if (isUnserved(error))
122
+ return new QueryError('UNSUPPORTED', this.recipe, { feature: 'v7' }, cause);
123
+ // Only a trusted backend answers v7 (a maintenance session's refusal says when to retry).
124
+ if (kernelCode === 'QUERY_BACKEND_CONTRACT')
125
+ return new QueryError('UNSUPPORTED', this.recipe, { feature: 'v7' }, cause);
126
+ return new QueryError('REFUSED', this.recipe, { kernelCode, retryable: false }, cause);
127
+ }
128
+ }
129
+ /** A page document of `size` rows. */
130
+ function withLimit(document, size) {
131
+ const list = document.list;
132
+ return { ...document, list: { ...list, limit: size } };
133
+ }
134
+ /** Each task, at most `limit` at once, the results in order; after a failure, no task starts. */
135
+ async function pooled(tasks, limit) {
136
+ const results = [];
137
+ let next = 0;
138
+ let failed = false;
139
+ const worker = async () => {
140
+ while (!failed && next < tasks.length) {
141
+ const index = next++;
142
+ try {
143
+ results[index] = await tasks[index]();
144
+ }
145
+ catch (error) {
146
+ failed = true;
147
+ throw error;
148
+ }
149
+ }
150
+ };
151
+ await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
152
+ return results;
153
+ }
154
+ /**
155
+ * The facets a stage returned, each a read, a list of reads (`items.map`) or nothing. One read is
156
+ * a single stage's alone: each of several returns a record of facets.
157
+ */
158
+ function facetsOf(recipe, built, single) {
159
+ if (typeof built !== 'object' || built === null || Array.isArray(built) || isRead(built)) {
160
+ const expected = single ? 'a stage returns one read or' : 'each of several stages returns';
161
+ throw new TypeError(`Recipe ${recipe}: ${expected} a record of facets; got ${describe(built)}.`);
162
+ }
163
+ return Object.entries(built);
164
+ }
165
+ /** A page is a recipe's single read: one stage that returns it. */
166
+ function refusePage(recipe, plan) {
167
+ if (plan.verb === 'page')
168
+ throw new TypeError(`Recipe ${recipe}: a page is a recipe's single read (D22); read the others with first or all.`);
169
+ }
170
+ /**
171
+ * The reads of one stage, admitted (S11, S12): a single read, or each facet's in order and the
172
+ * shape that puts their results back. A facet takes a name no `earlier` stage read: the merged
173
+ * results keep one value per name.
174
+ */
175
+ export function planStage(run, built, single, earlier = {}) {
176
+ if (single && isRead(built)) {
177
+ const plans = [planOf(built)];
178
+ run.admit(plans);
179
+ return { plans };
180
+ }
181
+ const facets = facetsOf(run.recipe, built, single);
182
+ for (const [name] of facets)
183
+ if (Object.hasOwn(earlier, name))
184
+ throw new TypeError(`Recipe ${run.recipe}: an earlier stage read the facet ${name}; name this stage's facet otherwise.`);
185
+ const plans = [];
186
+ const shape = [];
187
+ for (const [name, facet] of facets) {
188
+ if (facet === undefined)
189
+ shape.push([name, undefined]);
190
+ else if (Array.isArray(facet))
191
+ shape.push([name, facet.map((read) => plans.push(planOf(read)) - 1)]);
192
+ else
193
+ shape.push([name, plans.push(planOf(facet)) - 1]);
194
+ }
195
+ plans.forEach((plan) => refusePage(run.recipe, plan));
196
+ run.admit(plans);
197
+ return { plans, shape };
198
+ }
199
+ /** The results of one stage: a read's result, or a record of each facet's. */
200
+ async function runStage(run, built, single, call, earlier) {
201
+ const { plans, shape } = planStage(run, built, single, earlier);
202
+ if (shape === undefined) {
203
+ const plan = plans[0];
204
+ return run.read(plan, plan.verb === 'page' ? call : undefined);
205
+ }
206
+ const results = await pooled(plans.map((plan) => () => run.read(plan)), IN_FLIGHT);
207
+ const record = {};
208
+ for (const [name, at] of shape)
209
+ record[name] =
210
+ at === undefined
211
+ ? undefined
212
+ : typeof at === 'number'
213
+ ? results[at]
214
+ : Object.freeze(at.map((index) => results[index]));
215
+ return Object.freeze(record);
216
+ }
217
+ /** The size and cursor of a page call, checked against the page's `max`. */
218
+ function pageCall(recipe, call, max) {
219
+ const { size, after } = call;
220
+ if (max === undefined) {
221
+ if (size !== undefined || after !== undefined)
222
+ throw new TypeError(`Recipe ${recipe} is not a page: size and after continue a page recipe.`);
223
+ return {};
224
+ }
225
+ if (size !== undefined && (!Number.isInteger(size) || size < 1 || size > max))
226
+ throw new TypeError(`Recipe ${recipe}: size is a whole number from 1 to the page's max, ${max}; got ${describe(size)}.`);
227
+ if (after !== undefined && (typeof after !== 'string' || after.length === 0))
228
+ throw new TypeError(`Recipe ${recipe}: after is the cursor a page returned as next.`);
229
+ return { size: size ?? max, ...(after === undefined ? {} : { after }) };
230
+ }
231
+ /**
232
+ * Run a recipe for `input` against `domain`, the resolved Domain it was written for: its stages,
233
+ * then its `output`. A page recipe takes its call's `size` and `after`. A read the Kernel refuses
234
+ * raises a `QueryError`; `output` raises the recipe's declared errors. With the Class requirements
235
+ * the Domain's build `declared`, each stage's relations are checked against them first (S12).
236
+ */
237
+ export async function runQueryRecipe(client, domain, recipe, input, call = {}, declared) {
238
+ const spec = recipeSpecOf(recipe);
239
+ if (spec === undefined)
240
+ throw new TypeError(`Not a v7 recipe: define one with defineQuery<Schema>()(id, { build }); got ${describe(recipe)}.`);
241
+ const run = new Run(client, domain, recipe.id, declared);
242
+ const d = handlesOf(domain);
243
+ const single = spec.stages.length === 1;
244
+ let results;
245
+ let merged = {};
246
+ for (const [index, stage] of spec.stages.entries()) {
247
+ const built = index === 0
248
+ ? stage(input, d)
249
+ : stage(Object.freeze({ ...merged }), input, d);
250
+ const page = single && isRead(built) && planOf(built).verb === 'page';
251
+ const options = pageCall(recipe.id, call, page ? planOf(built).bound : undefined);
252
+ results = await runStage(run, built, single, options, merged);
253
+ merged = { ...merged, ...results };
254
+ }
255
+ const answered = single ? results : Object.freeze(merged);
256
+ if (spec.output === undefined)
257
+ return answered;
258
+ return spec.output(answered, input, outputContext(recipe, run.model));
259
+ }
260
+ /**
261
+ * The context a recipe's `output`, or a Mutation recipe's `reject`, receives: its declared errors,
262
+ * and `is`, which narrows a row by Class.
263
+ */
264
+ export function outputContext(recipe, model) {
265
+ return {
266
+ error: declaredErrorFactory(recipe),
267
+ is: (row, cls) => {
268
+ try {
269
+ return model.satisfies(model.classOf(row.$class)).has(cls);
270
+ }
271
+ catch {
272
+ return false;
273
+ }
274
+ },
275
+ };
276
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The Kernel's rejections of a v7 Query, made readable: its refusals (`QUERY_INPUT_INVALID`), and
3
+ * a Host that does not serve v7 at all (`FUNCTION_INPUT_INVALID`).
4
+ */
5
+ import { ResponseError } from '../../../platform/client/index.js';
6
+ /** A decode refusal, as the wire writes it: the fault's JSON pointer, what it takes, a fix. */
7
+ export interface QueryDecodeRefusal {
8
+ readonly phase: 'decode';
9
+ readonly code: string;
10
+ /** RFC 6901, into the request `{document, paging}` the SDK sent. */
11
+ readonly path: string;
12
+ readonly expected: string;
13
+ readonly fix: string;
14
+ readonly tier?: string;
15
+ }
16
+ /** A planning refusal: the issue and its pointer into the request. */
17
+ export interface QueryPlanRefusal {
18
+ readonly phase: 'plan';
19
+ readonly issue: string;
20
+ readonly path?: string;
21
+ }
22
+ /** A caller-visible plan bound the Query exceeds. */
23
+ export interface QueryLimitRefusal {
24
+ readonly phase: 'plan';
25
+ readonly issue: 'QUERY_PLAN_LIMIT_EXCEEDED';
26
+ readonly limit: string;
27
+ readonly maximum: number;
28
+ readonly actual: number;
29
+ readonly path?: string;
30
+ }
31
+ export type QueryRefusal = QueryDecodeRefusal | QueryPlanRefusal | QueryLimitRefusal;
32
+ /** The refusals a `QUERY_INPUT_INVALID` rejection carries, or `undefined` for any other error. */
33
+ export declare function refusalsOf(error: unknown): readonly QueryRefusal[] | undefined;
34
+ /** The refusals in one line: the first three, then how many more. */
35
+ export declare function describeRefusals(refusals: readonly QueryRefusal[]): string;
36
+ /** Whether a rejection of a v7 request says the Host does not serve it. */
37
+ export declare function isUnserved(error: unknown): error is ResponseError;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The Kernel's rejections of a v7 Query, made readable: its refusals (`QUERY_INPUT_INVALID`), and
3
+ * a Host that does not serve v7 at all (`FUNCTION_INPUT_INVALID`).
4
+ */
5
+ import { ResponseError } from '../../../platform/client/index.js';
6
+ const SHOWN = 3;
7
+ const text = (value) => (typeof value === 'string' ? value : undefined);
8
+ const count = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
9
+ /** The refusals a `QUERY_INPUT_INVALID` rejection carries, or `undefined` for any other error. */
10
+ export function refusalsOf(error) {
11
+ if (!(error instanceof ResponseError) || error.reason?.code !== 'QUERY_INPUT_INVALID')
12
+ return undefined;
13
+ const details = error.reason.details;
14
+ if (details.phase === 'decode' && Array.isArray(details.refusals)) {
15
+ return Object.freeze(details.refusals.flatMap((entry) => {
16
+ const refusal = (entry ?? {});
17
+ const code = text(refusal.code);
18
+ const path = text(refusal.path);
19
+ if (code === undefined || path === undefined)
20
+ return [];
21
+ const tier = text(refusal.tier);
22
+ return [
23
+ Object.freeze({
24
+ phase: 'decode',
25
+ code,
26
+ path,
27
+ expected: text(refusal.expected) ?? '',
28
+ fix: text(refusal.fix) ?? '',
29
+ ...(tier === undefined ? {} : { tier }),
30
+ }),
31
+ ];
32
+ }));
33
+ }
34
+ const issue = text(details.issue);
35
+ if (details.phase !== 'plan' || issue === undefined)
36
+ return Object.freeze([]);
37
+ const path = text(details.path);
38
+ const limit = text(details.limit);
39
+ const maximum = count(details.maximum);
40
+ const actual = count(details.actual);
41
+ if (issue === 'QUERY_PLAN_LIMIT_EXCEEDED' &&
42
+ limit !== undefined &&
43
+ maximum !== undefined &&
44
+ actual !== undefined) {
45
+ return Object.freeze([
46
+ Object.freeze({
47
+ phase: 'plan',
48
+ issue,
49
+ limit,
50
+ maximum,
51
+ actual,
52
+ ...(path === undefined ? {} : { path }),
53
+ }),
54
+ ]);
55
+ }
56
+ return Object.freeze([
57
+ Object.freeze({ phase: 'plan', issue, ...(path === undefined ? {} : { path }) }),
58
+ ]);
59
+ }
60
+ function describe(refusal) {
61
+ if (refusal.phase === 'decode') {
62
+ const fix = refusal.fix.length === 0 ? '' : ` (${refusal.fix})`;
63
+ return `${refusal.code} at ${refusal.path}: expected ${refusal.expected}${fix}`;
64
+ }
65
+ const at = refusal.path === undefined ? '' : ` at ${refusal.path}`;
66
+ if ('limit' in refusal) {
67
+ return `${refusal.issue}${at}: its ${refusal.limit} limit is ${String(refusal.maximum)}, it needs ${String(refusal.actual)}`;
68
+ }
69
+ return `${refusal.issue}${at}`;
70
+ }
71
+ /** The refusals in one line: the first three, then how many more. */
72
+ export function describeRefusals(refusals) {
73
+ const listed = refusals.slice(0, SHOWN).map(describe);
74
+ const rest = refusals.length > SHOWN ? `; and ${String(refusals.length - SHOWN)} more` : '';
75
+ return `${listed.join('; ')}${rest}`;
76
+ }
77
+ /** Whether a rejection of a v7 request says the Host does not serve it. */
78
+ export function isUnserved(error) {
79
+ return error instanceof ResponseError && error.reason?.code === 'FUNCTION_INPUT_INVALID';
80
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A read's answer as rows (spec § Semantics): each item rendered by the layout of the Class the
3
+ * plan reads it as, each verb's rule applied (`get` exactly one, `find` at most one, `all` at most
4
+ * `max`), and a fact the reader may not read left out, kept by `.includeWithheld()`, or an error
5
+ * where `get` and `find` meet one. Rows and lists are frozen portable JSON.
6
+ */
7
+ import type { NodeId } from '../../../platform/graph/node/index.js';
8
+ import type { QueryListAnswer } from '../answer/answer.js';
9
+ import type { Model } from '../handles/model.js';
10
+ import type { EdgeRootPlan, NodeRootPlan, Pattern, ReadPlan } from '../handles/plan.js';
11
+ import type { EdgeKey } from '../handles/rows.js';
12
+ type Json = Readonly<Record<string, unknown>>;
13
+ type FactId = NodeId | EdgeKey;
14
+ /** One delivered item; a Kernel with K-C gives each Node item its Class. */
15
+ interface Item {
16
+ readonly fact: FactId;
17
+ readonly class?: string;
18
+ readonly edge?: EdgeKey;
19
+ readonly source?: Item;
20
+ readonly target?: Item;
21
+ readonly branches: Readonly<Record<string, unknown>>;
22
+ }
23
+ /** What renders one answer: the recipe it errs for, the Domain's Classes, the answer's facts. */
24
+ export declare class Renderer {
25
+ #private;
26
+ readonly recipe: string;
27
+ readonly model: Model;
28
+ constructor(recipe: string, model: Model, answer: QueryListAnswer<unknown, unknown>);
29
+ /** A Node row, or `undefined` when the reader may not read it and the read does not keep it. */
30
+ node(item: Item, pattern: Pattern): Json | undefined;
31
+ /** An Edge root row: its identity, its values and both ends by role. */
32
+ edge(item: Item, plan: EdgeRootPlan): Json | undefined;
33
+ /** The rows of a root list, Node or Edge, leaving out what the reader may not read. */
34
+ rootRows(items: readonly Item[], root: NodeRootPlan | EdgeRootPlan): readonly Json[];
35
+ /** The one root row `get` or `find` gives. */
36
+ rootOne(item: Item, root: NodeRootPlan | EdgeRootPlan): Json;
37
+ }
38
+ /** The answer of a root read with nothing to read: its verb's empty answer. */
39
+ export declare function emptyResult(recipe: string, plan: ReadPlan): unknown;
40
+ /** A root read's result from its answer, by its verb. */
41
+ export declare function rootResult(recipe: string, model: Model, plan: ReadPlan, answer: unknown): unknown;
42
+ export {};