@aventara/core 1.0.0-rc.10 → 1.0.0-rc.11

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 (110) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/ERRORS.md +82 -82
  3. package/README.md +57 -1
  4. package/dist/compiler/contract-compiler-diagnostic.guidance.js +1 -1
  5. package/dist/compiler/contract.compiler.js +56 -9
  6. package/dist/compiler/create-requirements.projector.js +1 -3
  7. package/dist/compiler/field-validator.compiler.js +1 -3
  8. package/dist/compiler/managed-elsewhere.projector.js +5 -5
  9. package/dist/config/cache-stage.factory.d.ts +24 -0
  10. package/dist/config/cache-stage.factory.js +3 -0
  11. package/dist/config/config.resolver.js +1 -7
  12. package/dist/config/config.validator.js +61 -13
  13. package/dist/config/exact-framework-config.d.ts +6 -0
  14. package/dist/config/field-compute.resolver.js +1 -3
  15. package/dist/config/pipeline-config.d.ts +16 -0
  16. package/dist/config/pipeline-tree.resolver.js +2 -3
  17. package/dist/contracts/canonical-json.writer.d.ts +7 -0
  18. package/dist/contracts/canonical-json.writer.js +55 -0
  19. package/dist/contracts/client-contract-structure.validator.d.ts +64 -0
  20. package/dist/contracts/client-contract-structure.validator.js +12 -0
  21. package/dist/contracts/contract-canonicalizer.js +5 -0
  22. package/dist/contracts/contract-limits.d.ts +6 -0
  23. package/dist/contracts/contract-limits.js +2 -0
  24. package/dist/contracts/contract.d.ts +15 -0
  25. package/dist/contracts/json-schema.projector.js +1 -3
  26. package/dist/framework/access-facade.js +1 -3
  27. package/dist/framework/framework.factory.js +42 -13
  28. package/dist/framework/operation-facade.d.ts +67 -6
  29. package/dist/framework/operation-facade.js +7 -0
  30. package/dist/framework/operation-set.runner.d.ts +47 -0
  31. package/dist/framework/operation-set.runner.js +168 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +2 -0
  34. package/dist/internal.d.ts +2 -0
  35. package/dist/internal.js +2 -0
  36. package/dist/operations/operation-validator-core.js +2 -4
  37. package/dist/operations/structural-limits.walker.js +1 -3
  38. package/dist/policy/access-question.runtime.js +1 -3
  39. package/dist/policy/policy-admission.js +1 -3
  40. package/dist/policy/policy-after-update.runtime.js +1 -3
  41. package/dist/policy/policy-nested-write.runtime.js +3 -12
  42. package/dist/policy/policy-read.runtime.js +1 -3
  43. package/dist/policy/policy-reference-write.runtime.js +2 -11
  44. package/dist/policy/policy-request.d.ts +31 -0
  45. package/dist/policy/policy-request.js +69 -17
  46. package/dist/policy/policy-session.js +1 -3
  47. package/dist/policy/policy-write.runtime.js +1 -3
  48. package/dist/prebuilt/core-version.generated.d.ts +1 -1
  49. package/dist/prebuilt/core-version.generated.js +1 -1
  50. package/dist/prebuilt/prebuilt-changes.js +1 -3
  51. package/dist/prebuilt/prebuilt-contracts.loader.js +1 -3
  52. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +1 -3
  53. package/dist/protocol/absent-route.responder.d.ts +69 -16
  54. package/dist/protocol/absent-route.responder.js +75 -31
  55. package/dist/protocol/contract-document.responder.d.ts +7 -0
  56. package/dist/protocol/contract-document.responder.js +3 -2
  57. package/dist/protocol/http-request.decoder.d.ts +33 -0
  58. package/dist/protocol/http-request.decoder.js +23 -7
  59. package/dist/protocol/http-request.reader.d.ts +4 -0
  60. package/dist/protocol/http-request.reader.js +11 -2
  61. package/dist/protocol/http-response.encoder.d.ts +12 -0
  62. package/dist/protocol/http-response.encoder.js +15 -1
  63. package/dist/protocol/operation-response.guard.js +1 -3
  64. package/dist/protocol/operation-set.decoder.d.ts +48 -0
  65. package/dist/protocol/operation-set.decoder.js +185 -0
  66. package/dist/protocol/operation-set.handler.d.ts +27 -0
  67. package/dist/protocol/operation-set.handler.js +179 -0
  68. package/dist/protocol/protocol-headers.d.ts +9 -0
  69. package/dist/protocol/protocol-headers.js +12 -5
  70. package/dist/protocol/protocol-surface.d.ts +12 -4
  71. package/dist/protocol/protocol-surface.js +34 -1
  72. package/dist/protocol/protocol.binder.d.ts +37 -1
  73. package/dist/protocol/protocol.binder.js +2 -0
  74. package/dist/protocol/protocol.handler.d.ts +35 -2
  75. package/dist/protocol/protocol.handler.js +84 -8
  76. package/dist/protocol/read-cache.responder.d.ts +26 -0
  77. package/dist/protocol/read-cache.responder.js +47 -0
  78. package/dist/protocol/read-query.codec.d.ts +34 -0
  79. package/dist/protocol/read-query.codec.js +125 -0
  80. package/dist/runtime/computed-read-runtime.js +2 -8
  81. package/dist/runtime/create-requirement.js +1 -3
  82. package/dist/runtime/diagnostics-reporter.d.ts +6 -1
  83. package/dist/runtime/diagnostics-reporter.js +3 -8
  84. package/dist/runtime/error.catalog.d.ts +2 -2
  85. package/dist/runtime/error.catalog.js +1 -1
  86. package/dist/runtime/field-stage.runtime.d.ts +0 -2
  87. package/dist/runtime/field-stage.runtime.js +1 -7
  88. package/dist/runtime/field-validator.runtime.js +2 -1
  89. package/dist/runtime/mutation-planner.js +1 -3
  90. package/dist/runtime/nested-write.walker.js +2 -1
  91. package/dist/runtime/output-validator.js +1 -3
  92. package/dist/runtime/owned-execution.registry.d.ts +22 -2
  93. package/dist/runtime/pipeline.d.ts +5 -1
  94. package/dist/runtime/record.guard.d.ts +14 -0
  95. package/dist/runtime/record.guard.js +10 -0
  96. package/dist/runtime/request-envelope.validator.js +13 -10
  97. package/dist/runtime/scalar-runtime.d.ts +0 -10
  98. package/dist/runtime/scalar-runtime.js +1 -10
  99. package/dist/runtime/thenable.guard.d.ts +7 -0
  100. package/dist/runtime/thenable.guard.js +5 -0
  101. package/dist/runtime/unsent-value.normalizer.js +2 -1
  102. package/dist/transactions/operation-arguments-encoder.js +1 -7
  103. package/dist/transactions/sql-statement.validator.js +2 -1
  104. package/dist/transactions/transaction-plan-assembler.js +1 -7
  105. package/dist/transactions/transaction-plan-validator.js +4 -6
  106. package/dist/transactions/transaction-reference-collector.js +8 -10
  107. package/dist/transactions/transaction-reference-resolver.js +1 -3
  108. package/dist/transactions/transaction-runtime.js +31 -2
  109. package/dist/transactions/transaction-stage.runtime.js +1 -3
  110. package/package.json +1 -1
@@ -7,6 +7,7 @@ import { DEFAULT_CONTRACT_LIMITS, } from "../contracts/contract-limits.js";
7
7
  import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
8
8
  import { advertisedDirectives, advertisedReducers, holdsInAnyScope, } from "../contracts/field.js";
9
9
  import { isOperationVariantAvailable, } from "../contracts/resource-operations.js";
10
+ import { isRecord } from "../runtime/record.guard.js";
10
11
  import { checkCreateRequirements, checkHiddenRequirements, projectCreateRequirements, } from "./create-requirements.projector.js";
11
12
  import { applyValidators, bindFieldValidators, checkLayerValidators, compileValidatorTable, } from "./field-validator.compiler.js";
12
13
  import { explainManagedRefusals, hideManagedElsewhere, managedElsewhereOf, managedWriteReason, markCoreResources, pruneManagedEnums, withholdManagedWrites, } from "./managed-elsewhere.projector.js";
@@ -24,9 +25,6 @@ const COMPUTED_TRAITS = new Set([
24
25
  function diagnostic(code, path, message) {
25
26
  return { code, path, message };
26
27
  }
27
- function isRecord(value) {
28
- return value !== null && typeof value === "object" && !Array.isArray(value);
29
- }
30
28
  function cloneValue(value) {
31
29
  if (Array.isArray(value)) {
32
30
  return value.map((entry) => cloneValue(entry));
@@ -438,7 +436,8 @@ function checkResourceTree(model, baseline, scope, tree, definitions, managed, d
438
436
  diagnostics.push(diagnostic("COMPILER_UNKNOWN_FIELD", `${resourcePath}.fields.${fieldName}`, `field "${fieldName}" is neither discovered by the adapter nor defined at the root`));
439
437
  continue;
440
438
  }
441
- if (!isRecord(fieldEntry) || !isRecord(fieldEntry.pipelines)) {
439
+ if (!isRecord(fieldEntry) ||
440
+ !isRecord(fieldEntry.pipelines)) {
442
441
  continue;
443
442
  }
444
443
  const pipelinesPath = `${resourcePath}.fields.${fieldName}.pipelines`;
@@ -764,7 +763,9 @@ function exemptOperations(walk, resourceName, universe, parent, entry, path) {
764
763
  if (value === false || value === undefined) {
765
764
  continue;
766
765
  }
767
- if (value !== true && !Array.isArray(value) && !isRecord(value)) {
766
+ if (value !== true &&
767
+ !Array.isArray(value) &&
768
+ !isRecord(value)) {
768
769
  walk.diagnostics.push(diagnostic("COMPILER_UNAVAILABLE_OPERATION", familyPath, "an operation family takes true, false, a list of its variants or an object of them"));
769
770
  continue;
770
771
  }
@@ -811,7 +812,8 @@ function exemptResource(walk, resourceName, universe, parent, entry, path) {
811
812
  }
812
813
  function projectExposure(walk, universe, parent, exemptions) {
813
814
  const path = `${walk.layer}.exemptions.resources`;
814
- const entries = isRecord(exemptions) && isRecord(exemptions.resources)
815
+ const entries = isRecord(exemptions) &&
816
+ isRecord(exemptions.resources)
815
817
  ? exemptions.resources
816
818
  : {};
817
819
  const resources = {};
@@ -855,7 +857,8 @@ function projectExposure(walk, universe, parent, exemptions) {
855
857
  }
856
858
  function pruneEmptyOperationFamilies(operations) {
857
859
  for (const [family, variants] of Object.entries(operations)) {
858
- if (isRecord(variants) && Object.keys(variants).length === 0) {
860
+ if (isRecord(variants) &&
861
+ Object.keys(variants).length === 0) {
859
862
  delete operations[family];
860
863
  }
861
864
  }
@@ -1151,9 +1154,10 @@ export async function compileContracts(model, config) {
1151
1154
  }
1152
1155
  const core = semanticObject(coreCompilation.projection);
1153
1156
  const application = semanticObject(applicationProjection.projection);
1157
+ const clientMarked = withReadCache(clientProjection.projection, compilerConfig.client.pipelines);
1154
1158
  const client = await identifiedContract(compilerConfig.access === undefined
1155
- ? clientProjection.projection
1156
- : { ...clientProjection.projection, access: compilerConfig.access });
1159
+ ? clientMarked
1160
+ : { ...clientMarked, access: compilerConfig.access });
1157
1161
  const agent = await identifiedContract(agentProjection.projection);
1158
1162
  for (const [layer, contract] of [
1159
1163
  ["client", client],
@@ -1180,6 +1184,49 @@ export async function compileContracts(model, config) {
1180
1184
  });
1181
1185
  return { ok: true, contracts };
1182
1186
  }
1187
+ function lastCacheOf(levels) {
1188
+ for (let index = (levels?.length ?? 0) - 1; index >= 0; index -= 1) {
1189
+ const stage = levels?.[index]?.cache;
1190
+ if (stage !== undefined) {
1191
+ return { for: stage.for ?? "user", maxAge: stage.maxAge };
1192
+ }
1193
+ }
1194
+ return undefined;
1195
+ }
1196
+ function withReadCache(projection, tree) {
1197
+ const layer = lastCacheOf(tree.layer);
1198
+ const resources = {};
1199
+ for (const [name, resource] of Object.entries(projection.resources)) {
1200
+ const pipelines = Object.hasOwn(tree.resources, name)
1201
+ ? tree.resources[name]
1202
+ : undefined;
1203
+ const find = Object.hasOwn(pipelines?.families ?? {}, "find")
1204
+ ? pipelines?.families.find
1205
+ : undefined;
1206
+ const fallback = lastCacheOf(find?.pipelines) ??
1207
+ lastCacheOf(pipelines?.pipelines) ??
1208
+ layer;
1209
+ const advertised = (resource.operations.find ?? {});
1210
+ const marks = {};
1211
+ for (const variant of Object.keys(advertised)) {
1212
+ if (!isOperationVariantAvailable(advertised[variant])) {
1213
+ continue;
1214
+ }
1215
+ const own = Object.hasOwn(find?.variants ?? {}, variant)
1216
+ ? find?.variants[variant]?.pipelines
1217
+ : undefined;
1218
+ const mark = lastCacheOf(own) ?? fallback;
1219
+ if (mark !== undefined) {
1220
+ marks[variant] = mark;
1221
+ }
1222
+ }
1223
+ resources[name] =
1224
+ Object.keys(marks).length === 0
1225
+ ? resource
1226
+ : { ...resource, cache: marks };
1227
+ }
1228
+ return { ...projection, resources };
1229
+ }
1183
1230
  function bindRuntimePieces(contracts, definitions, validators) {
1184
1231
  for (const origin of ["core", "application", "client", "agent"]) {
1185
1232
  bindShapedFields(contracts[origin], definitions);
@@ -1,8 +1,6 @@
1
1
  import { advertisedDirectives, advertisedDirectivesAt, } from "../contracts/field.js";
2
2
  import { isOperationVariantAvailable, writeScopeOf, } from "../contracts/resource-operations.js";
3
- function isRecord(value) {
4
- return value !== null && typeof value === "object" && !Array.isArray(value);
5
- }
3
+ import { isRecord } from "../runtime/record.guard.js";
6
4
  function own(record, key) {
7
5
  return Object.hasOwn(record, key) ? record[key] : undefined;
8
6
  }
@@ -1,8 +1,6 @@
1
1
  import { avzCheckOf, } from "../avz/avz-check.registry.js";
2
2
  import { FIELD_CHECK_SUBJECTS, } from "../runtime/field-check.evaluator.js";
3
- function isRecord(value) {
4
- return value !== null && typeof value === "object" && !Array.isArray(value);
5
- }
3
+ import { isRecord } from "../runtime/record.guard.js";
6
4
  function own(record, key) {
7
5
  return Object.hasOwn(record, key) ? record[key] : undefined;
8
6
  }
@@ -1,3 +1,4 @@
1
+ import { isRecord } from "../runtime/record.guard.js";
1
2
  const WRITE_FAMILIES = [
2
3
  "create",
3
4
  "update",
@@ -8,16 +9,14 @@ const SOURCE_ROW_DIRECTIVES = [
8
9
  "$connect",
9
10
  "$disconnect",
10
11
  ];
11
- function isRecord(value) {
12
- return typeof value === "object" && value !== null && !Array.isArray(value);
13
- }
14
12
  export function managedElsewhereOf(rootTree) {
15
13
  const managed = new Set();
16
14
  if (!isRecord(rootTree)) {
17
15
  return managed;
18
16
  }
19
17
  for (const [name, entry] of Object.entries(rootTree)) {
20
- if (isRecord(entry) && entry.managedElsewhere === true) {
18
+ if (isRecord(entry) &&
19
+ entry.managedElsewhere === true) {
21
20
  managed.add(name);
22
21
  }
23
22
  }
@@ -28,7 +27,8 @@ function keptDirectives(capability, kept) {
28
27
  const left = capability.filter((directive) => kept.includes(directive));
29
28
  return left.length > 0 ? left : undefined;
30
29
  }
31
- if (isRecord(capability) && Array.isArray(capability.directives)) {
30
+ if (isRecord(capability) &&
31
+ Array.isArray(capability.directives)) {
32
32
  const left = capability.directives.filter((directive) => kept.includes(directive));
33
33
  return left.length > 0 ? { ...capability, directives: left } : undefined;
34
34
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A read's cache stage, for `pipelines.cache` at the root or client layer, a
3
+ * Resource, the `find` family or one of its variants:
4
+ * `pipelines: { cache: cache({ for: "user", maxAge: 60 }) }`. The read is then
5
+ * also served as a GET a browser or a CDN may keep for `maxAge` seconds.
6
+ *
7
+ * Always write `for`, even for the default `"user"`: the audience is the
8
+ * promise this read makes, and it should be visible in the code.
9
+ */
10
+ export declare function cache(stage: {
11
+ /**
12
+ * Who may share one answer: `"user"` (the default; each caller's own
13
+ * browser keeps its own), `"anonymous"` (anonymous callers share one answer a
14
+ * CDN may keep; a signed-in caller's stays private) or `"everyone"` (one
15
+ * answer for every caller, which a CDN may hand to anyone). Always write it,
16
+ * even for `"user"`: the audience is the promise this read makes.
17
+ */
18
+ readonly for?: "user" | "anonymous" | "everyone";
19
+ /** How long an answer may be kept, in whole seconds (`max-age`). */
20
+ readonly maxAge: number;
21
+ }): {
22
+ readonly for: "user" | "anonymous" | "everyone";
23
+ readonly maxAge: number;
24
+ };
@@ -0,0 +1,3 @@
1
+ export function cache(stage) {
2
+ return Object.freeze({ for: stage.for ?? "user", maxAge: stage.maxAge });
3
+ }
@@ -1,17 +1,11 @@
1
1
  import { AvConstructionError } from "../framework/framework-construction.error.js";
2
2
  import { ACCESS_VERB_NAMES, providerVerb, } from "../policy/access-verb.table.js";
3
+ import { isPlainRecord } from "../runtime/record.guard.js";
3
4
  import { mountPathRefusalDiagnostic } from "./config.validator.js";
4
5
  import { normalizeEntrypoint } from "./entrypoint.normalizer.js";
5
6
  import { chainCompute, computeOfLayer, } from "./field-compute.resolver.js";
6
7
  import { resolvePipelineTree, } from "./pipeline-tree.resolver.js";
7
8
  const hasOwn = (value, key) => Object.hasOwn(value, key);
8
- function isPlainRecord(value) {
9
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
10
- return false;
11
- }
12
- const prototype = Object.getPrototypeOf(value);
13
- return prototype === Object.prototype || prototype === null;
14
- }
15
9
  function cloneConfigValue(value) {
16
10
  if (Array.isArray(value)) {
17
11
  return [...value];
@@ -1,6 +1,7 @@
1
1
  import { isAccessPath, isAccessUrl } from "../contracts/access-route.rules.js";
2
2
  import { TRANSACTION_CAPABILITIES } from "../contracts/transaction-capability.js";
3
3
  import { ACCESS_VERB_NAMES, isAccessVerbName, providerVerb, } from "../policy/access-verb.table.js";
4
+ import { isPlainRecord } from "../runtime/record.guard.js";
4
5
  import { normalizeEntrypoint } from "./entrypoint.normalizer.js";
5
6
  import { AVZ_FORMATS } from "./framework-field-definition.js";
6
7
  const ROOT_ONLY_PROPERTIES = new Set([
@@ -77,6 +78,8 @@ const LIMIT_PROPERTIES = new Set([
77
78
  "maxTransactionOperations",
78
79
  "maxBooleanNodes",
79
80
  "maxJsonDepth",
81
+ "maxAll",
82
+ "maxAllConcurrency",
80
83
  ]);
81
84
  const PIPELINE_PROPERTIES = new Set([
82
85
  "guards",
@@ -86,13 +89,6 @@ const PIPELINE_PROPERTIES = new Set([
86
89
  "filters",
87
90
  ]);
88
91
  const PIPELINE_HOOK_PROPERTIES = new Set(["before", "after"]);
89
- function isPlainRecord(value) {
90
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
91
- return false;
92
- }
93
- const prototype = Object.getPrototypeOf(value);
94
- return prototype === Object.prototype || prototype === null;
95
- }
96
92
  function diagnostic(code, path, message) {
97
93
  return { code, path, message };
98
94
  }
@@ -119,6 +115,14 @@ function validateLimits(value, path, diagnostics) {
119
115
  diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", limitPath, `unknown limit property: ${key}`));
120
116
  continue;
121
117
  }
118
+ if (key === "maxAllConcurrency") {
119
+ if (typeof limit !== "number" ||
120
+ !Number.isSafeInteger(limit) ||
121
+ limit < 1) {
122
+ diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", limitPath, `${limitPath} must be a whole number of at least 1`));
123
+ }
124
+ continue;
125
+ }
122
126
  if (typeof limit !== "number" ||
123
127
  !Number.isFinite(limit) ||
124
128
  !Number.isInteger(limit) ||
@@ -127,6 +131,33 @@ function validateLimits(value, path, diagnostics) {
127
131
  }
128
132
  }
129
133
  }
134
+ const CACHE_PROPERTIES = new Set(["for", "maxAge"]);
135
+ const CACHE_AUDIENCES = new Set(["user", "anonymous", "everyone"]);
136
+ function validateCache(value, path, diagnostics) {
137
+ if (!requireRecord(value, path, diagnostics)) {
138
+ return;
139
+ }
140
+ for (const key of Object.keys(value)) {
141
+ if (!CACHE_PROPERTIES.has(key)) {
142
+ diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", `${path}.${key}`, `unknown cache property: ${key}; cache takes for and maxAge`));
143
+ }
144
+ }
145
+ const maxAgePath = `${path}.maxAge`;
146
+ if (!Object.hasOwn(value, "maxAge")) {
147
+ diagnostics.push(diagnostic("CONFIG_REQUIRED", maxAgePath, `${maxAgePath} is required`));
148
+ }
149
+ else if (typeof value.maxAge !== "number" ||
150
+ !Number.isInteger(value.maxAge) ||
151
+ value.maxAge < 0 ||
152
+ value.maxAge > MAX_CACHE_AGE) {
153
+ diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", maxAgePath, `${maxAgePath} must be a whole number of seconds from 0 to ${MAX_CACHE_AGE}`));
154
+ }
155
+ if (Object.hasOwn(value, "for") && !CACHE_AUDIENCES.has(value.for)) {
156
+ const forPath = `${path}.for`;
157
+ diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", forPath, `${forPath} must be "user", "anonymous" or "everyone"`));
158
+ }
159
+ }
160
+ const MAX_CACHE_AGE = 2_147_483_647;
130
161
  function validateArray(value, key, path, diagnostics) {
131
162
  if (!Object.hasOwn(value, key)) {
132
163
  return;
@@ -148,7 +179,7 @@ function validatePipelineHooks(value, path, diagnostics) {
148
179
  validateArray(value, "before", path, diagnostics);
149
180
  validateArray(value, "after", path, diagnostics);
150
181
  }
151
- function validatePipelines(value, path, diagnostics) {
182
+ function validatePipelines(value, path, diagnostics, cacheRefusal) {
152
183
  if (!requireRecord(value, path, diagnostics)) {
153
184
  return;
154
185
  }
@@ -157,6 +188,16 @@ function validatePipelines(value, path, diagnostics) {
157
188
  diagnostics.push(diagnostic("CONFIG_MOVED_PROPERTY", `${path}.${key}`, `${path}.${key} moved in rc.1: hooks are ${path}.hooks.${key}`));
158
189
  continue;
159
190
  }
191
+ if (key === "cache") {
192
+ const refusal = cacheRefusal ?? layerCacheRefusal(path);
193
+ if (refusal !== undefined) {
194
+ diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", `${path}.cache`, `${path}.cache: ${refusal}`));
195
+ }
196
+ else if (value.cache !== undefined) {
197
+ validateCache(value.cache, `${path}.cache`, diagnostics);
198
+ }
199
+ continue;
200
+ }
160
201
  if (!PIPELINE_PROPERTIES.has(key)) {
161
202
  diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", `${path}.${key}`, `unknown pipeline property: ${key}`));
162
203
  }
@@ -168,6 +209,13 @@ function validatePipelines(value, path, diagnostics) {
168
209
  validatePipelineHooks(value.hooks, `${path}.hooks`, diagnostics);
169
210
  }
170
211
  }
212
+ function layerCacheRefusal(path) {
213
+ const layer = path.split(".")[0];
214
+ return layer === "application" || layer === "agent"
215
+ ? `cache applies to a client's reads; the ${layer} layer is never served a GET (state it at the root or in client)`
216
+ : undefined;
217
+ }
218
+ const WRITE_CACHE_REFUSAL = "cache applies to reads (the find family) only; a write is never a GET";
171
219
  function validateForceTransaction(value, path, diagnostics) {
172
220
  if (!Object.hasOwn(value, "forceTransaction")) {
173
221
  return;
@@ -177,21 +225,21 @@ function validateForceTransaction(value, path, diagnostics) {
177
225
  diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", propertyPath, `${propertyPath} must be a boolean`));
178
226
  }
179
227
  }
180
- function validateOperationLevel(value, path, isFamily, diagnostics) {
228
+ function validateOperationLevel(value, path, isFamily, diagnostics, isRead) {
181
229
  if (!requireRecord(value, path, diagnostics)) {
182
230
  return;
183
231
  }
184
232
  for (const [key, child] of Object.entries(value)) {
185
233
  const keyPath = `${path}.${key}`;
186
234
  if (key === "pipelines") {
187
- validatePipelines(child, keyPath, diagnostics);
235
+ validatePipelines(child, keyPath, diagnostics, isRead ? undefined : WRITE_CACHE_REFUSAL);
188
236
  continue;
189
237
  }
190
238
  if (key === "forceTransaction") {
191
239
  continue;
192
240
  }
193
241
  if (isFamily && OPERATION_VARIANTS.has(key)) {
194
- validateOperationLevel(child, keyPath, false, diagnostics);
242
+ validateOperationLevel(child, keyPath, false, diagnostics, isRead);
195
243
  continue;
196
244
  }
197
245
  diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", keyPath, isFamily
@@ -210,7 +258,7 @@ function validateOperations(value, path, diagnostics) {
210
258
  diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", familyPath, `unknown operation family: ${family}`));
211
259
  continue;
212
260
  }
213
- validateOperationLevel(level, familyPath, true, diagnostics);
261
+ validateOperationLevel(level, familyPath, true, diagnostics, family === "find");
214
262
  }
215
263
  }
216
264
  function validateResourceTransactions(value, path, diagnostics) {
@@ -223,7 +271,7 @@ function validateResourceTransactions(value, path, diagnostics) {
223
271
  diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", keyPath, `unknown Resource transactions property: ${key}`));
224
272
  continue;
225
273
  }
226
- validatePipelines(child, keyPath, diagnostics);
274
+ validatePipelines(child, keyPath, diagnostics, "cache applies to reads; a transaction is never a GET");
227
275
  }
228
276
  }
229
277
  function validateDefinition(value, path, diagnostics) {
@@ -16,6 +16,10 @@ type PipelinesSchema = {
16
16
  };
17
17
  readonly interceptors: Leaf;
18
18
  readonly filters: Leaf;
19
+ readonly cache: {
20
+ readonly for: Leaf;
21
+ readonly maxAge: Leaf;
22
+ };
19
23
  };
20
24
  type LimitsSchema = {
21
25
  readonly maxRequestBytes: Leaf;
@@ -24,6 +28,8 @@ type LimitsSchema = {
24
28
  readonly maxTransactionOperations: Leaf;
25
29
  readonly maxBooleanNodes: Leaf;
26
30
  readonly maxJsonDepth: Leaf;
31
+ readonly maxAll: Leaf;
32
+ readonly maxAllConcurrency: Leaf;
27
33
  };
28
34
  type LevelSchema = {
29
35
  readonly pipelines: PipelinesSchema;
@@ -1,3 +1,4 @@
1
+ import { isRecord } from "../runtime/record.guard.js";
1
2
  function chainWrite(root, overlay) {
2
3
  if (root === undefined || overlay === undefined) {
3
4
  return root ?? overlay;
@@ -46,9 +47,6 @@ function chainRead(root, overlay) {
46
47
  ? compute
47
48
  : { dependsOn, compute };
48
49
  }
49
- function isRecord(value) {
50
- return value !== null && typeof value === "object" && !Array.isArray(value);
51
- }
52
50
  export function computeOfLayer(resources) {
53
51
  const result = {};
54
52
  if (!isRecord(resources)) {
@@ -23,6 +23,17 @@ export type AvPipeline<M extends AvAdapterModel = AvAdapterModel, R extends stri
23
23
  readonly interceptors?: readonly AvInterceptor<M, R, F, V, A>[];
24
24
  /** Turn a thrown error into a {@link AvError} outcome. */
25
25
  readonly filters?: readonly AvFilter<M, R, F, V, PreCompute>[];
26
+ /**
27
+ * A read's cache stage, `cache({ for: "user", maxAge: 60 })`: the reads this
28
+ * level covers (its `find` operations) are also served as a GET a browser or
29
+ * a CDN may keep. The most specific level's stage wins. Always write `for`,
30
+ * even for the default `"user"`: the audience is the promise this read
31
+ * makes, and it should be visible in the code.
32
+ */
33
+ readonly cache?: {
34
+ readonly for?: "user" | "anonymous" | "everyone";
35
+ readonly maxAge: number;
36
+ };
26
37
  };
27
38
  /**
28
39
  * A Resource's transaction stages: they run once per transaction with a
@@ -51,6 +62,11 @@ export type ResolvedPipelineConfig = {
51
62
  };
52
63
  readonly interceptors: readonly AvInterceptor[];
53
64
  readonly filters: readonly AvFilter[];
65
+ /** The level's read cache stage, as authored (validated); rc.11. */
66
+ readonly cache?: {
67
+ readonly for?: "user" | "anonymous" | "everyone";
68
+ readonly maxAge: number;
69
+ };
54
70
  };
55
71
  /** One layer's registrations on one Resource's transactions. */
56
72
  export type ResolvedTransactionPipelineConfig = {
@@ -1,6 +1,4 @@
1
- function isRecord(value) {
2
- return value !== null && typeof value === "object" && !Array.isArray(value);
3
- }
1
+ import { isRecord } from "../runtime/record.guard.js";
4
2
  function own(record, key) {
5
3
  return record !== undefined && Object.hasOwn(record, key)
6
4
  ? record[key]
@@ -23,6 +21,7 @@ function levelOf(value) {
23
21
  },
24
22
  interceptors: list(pipeline.interceptors),
25
23
  filters: list(pipeline.filters),
24
+ ...(isRecord(pipeline.cache) ? { cache: pipeline.cache } : {}),
26
25
  };
27
26
  }
28
27
  function transactionLevelOf(value) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The canonical JSON text (RFC 8785) of a JSON value: an object's keys in
3
+ * UTF-16 code-unit order, a member holding `undefined` left out, nothing but
4
+ * `null`, booleans, finite numbers, strings, arrays and plain objects. Throws a
5
+ * `TypeError` for anything else, a lone surrogate in a string or key included.
6
+ */
7
+ export declare function canonicalJson(value: unknown): string;
@@ -0,0 +1,55 @@
1
+ export function canonicalJson(value) {
2
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
3
+ const text = (string) => {
4
+ if (LONE_SURROGATE.test(string)) {
5
+ throw new TypeError("Canonical JSON cannot write a lone surrogate.");
6
+ }
7
+ return JSON.stringify(string);
8
+ };
9
+ const write = (current, ancestors) => {
10
+ if (current === null) {
11
+ return "null";
12
+ }
13
+ switch (typeof current) {
14
+ case "boolean":
15
+ return current ? "true" : "false";
16
+ case "number":
17
+ if (!Number.isFinite(current)) {
18
+ throw new TypeError("Canonical JSON cannot write a non-finite number.");
19
+ }
20
+ return JSON.stringify(current);
21
+ case "string":
22
+ return text(current);
23
+ case "object":
24
+ break;
25
+ default:
26
+ throw new TypeError(`Canonical JSON cannot write a ${typeof current}.`);
27
+ }
28
+ const container = current;
29
+ if (ancestors.includes(container)) {
30
+ throw new TypeError("Canonical JSON cannot write a cycle.");
31
+ }
32
+ const path = [...ancestors, container];
33
+ if (Array.isArray(container)) {
34
+ const items = [];
35
+ for (let index = 0; index < container.length; index += 1) {
36
+ items.push(write(container[index] ?? null, path));
37
+ }
38
+ return `[${items.join(",")}]`;
39
+ }
40
+ const prototype = Object.getPrototypeOf(container);
41
+ if (prototype !== Object.prototype && prototype !== null) {
42
+ throw new TypeError("Canonical JSON writes plain objects only.");
43
+ }
44
+ const record = container;
45
+ const members = [];
46
+ for (const key of Object.keys(record).sort((left, right) => left < right ? -1 : left > right ? 1 : 0)) {
47
+ const member = record[key];
48
+ if (member !== undefined) {
49
+ members.push(`${text(key)}:${write(member, path)}`);
50
+ }
51
+ }
52
+ return `{${members.join(",")}}`;
53
+ };
54
+ return write(value, []);
55
+ }
@@ -1307,6 +1307,64 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
1307
1307
  };
1308
1308
  };
1309
1309
  };
1310
+ readonly cache: {
1311
+ readonly is: "object";
1312
+ readonly required: {};
1313
+ readonly optional: {
1314
+ readonly first: {
1315
+ readonly is: "object";
1316
+ readonly required: {
1317
+ readonly for: {
1318
+ readonly is: "literal";
1319
+ readonly values: readonly ["user", "anonymous", "everyone"];
1320
+ };
1321
+ readonly maxAge: {
1322
+ readonly is: "number";
1323
+ };
1324
+ };
1325
+ readonly optional: {};
1326
+ };
1327
+ readonly unique: {
1328
+ readonly is: "object";
1329
+ readonly required: {
1330
+ readonly for: {
1331
+ readonly is: "literal";
1332
+ readonly values: readonly ["user", "anonymous", "everyone"];
1333
+ };
1334
+ readonly maxAge: {
1335
+ readonly is: "number";
1336
+ };
1337
+ };
1338
+ readonly optional: {};
1339
+ };
1340
+ readonly many: {
1341
+ readonly is: "object";
1342
+ readonly required: {
1343
+ readonly for: {
1344
+ readonly is: "literal";
1345
+ readonly values: readonly ["user", "anonymous", "everyone"];
1346
+ };
1347
+ readonly maxAge: {
1348
+ readonly is: "number";
1349
+ };
1350
+ };
1351
+ readonly optional: {};
1352
+ };
1353
+ readonly count: {
1354
+ readonly is: "object";
1355
+ readonly required: {
1356
+ readonly for: {
1357
+ readonly is: "literal";
1358
+ readonly values: readonly ["user", "anonymous", "everyone"];
1359
+ };
1360
+ readonly maxAge: {
1361
+ readonly is: "number";
1362
+ };
1363
+ };
1364
+ readonly optional: {};
1365
+ };
1366
+ };
1367
+ };
1310
1368
  };
1311
1369
  };
1312
1370
  };
@@ -1331,6 +1389,12 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
1331
1389
  readonly maxJsonDepth: {
1332
1390
  readonly is: "number";
1333
1391
  };
1392
+ readonly maxAll: {
1393
+ readonly is: "number";
1394
+ };
1395
+ readonly maxAllConcurrency: {
1396
+ readonly is: "number";
1397
+ };
1334
1398
  };
1335
1399
  readonly optional: Record<never, never>;
1336
1400
  };
@@ -265,6 +265,10 @@ const RELATION_FIELD = object({
265
265
  governed: literal([true]),
266
266
  });
267
267
  const OPERATION_AVAILABILITY = union(literal([true]), object({ projection: literal(PROJECTION_SCOPES) }, { write: literal(WRITE_SCOPES) }), object({ write: literal(WRITE_SCOPES) }, { projection: literal(PROJECTION_SCOPES) }));
268
+ const READ_CACHE_MARK = object({
269
+ for: literal(["user", "anonymous", "everyone"]),
270
+ maxAge: NUMBER,
271
+ }, {});
268
272
  const RESOURCE = object({
269
273
  fields: registry({
270
274
  is: "variants",
@@ -304,6 +308,12 @@ const RESOURCE = object({
304
308
  oneOf: nonEmptyList(pairOrMoreList(STRING)),
305
309
  parentSupplies: registry(registry(nonEmptyList(STRING))),
306
310
  }),
311
+ cache: object({}, {
312
+ first: READ_CACHE_MARK,
313
+ unique: READ_CACHE_MARK,
314
+ many: READ_CACHE_MARK,
315
+ count: READ_CACHE_MARK,
316
+ }),
307
317
  });
308
318
  const ACCESS_VERB = object({
309
319
  method: literal(["GET", "POST"]),
@@ -336,6 +346,8 @@ export const CLIENT_CONTRACT_STRUCTURE = object({
336
346
  maxTransactionOperations: NUMBER,
337
347
  maxBooleanNodes: NUMBER,
338
348
  maxJsonDepth: NUMBER,
349
+ maxAll: NUMBER,
350
+ maxAllConcurrency: NUMBER,
339
351
  }),
340
352
  transactions: literal(TRANSACTION_CAPABILITIES),
341
353
  protocol: object({
@@ -187,8 +187,13 @@ function normalizeResource(resource) {
187
187
  createRequirements: normalizeCreateRequirements(resource.createRequirements),
188
188
  }),
189
189
  ...coreResourceFacts(resource),
190
+ ...clientResourceFacts(resource),
190
191
  };
191
192
  }
193
+ function clientResourceFacts(resource) {
194
+ const { cache } = resource;
195
+ return cache === undefined ? {} : { cache };
196
+ }
192
197
  function coreResourceFacts(resource) {
193
198
  const facts = resource;
194
199
  return {