@fougere/core 0.12.0-alpha.0 → 0.12.2-alpha.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 (236) hide show
  1. package/dist/CallIdentity.d.ts.map +1 -1
  2. package/dist/CallIdentity.js.map +1 -1
  3. package/dist/DomainCluster.d.ts.map +1 -1
  4. package/dist/DomainCluster.js +1 -1
  5. package/dist/DomainCluster.js.map +1 -1
  6. package/dist/EffectiveOperationModel.d.ts.map +1 -1
  7. package/dist/EffectiveOperationModel.js +20 -1
  8. package/dist/EffectiveOperationModel.js.map +1 -1
  9. package/dist/FougereConfig.d.ts.map +1 -1
  10. package/dist/FougereConfig.js.map +1 -1
  11. package/dist/FrondConfig.d.ts.map +1 -1
  12. package/dist/FrondConfig.js.map +1 -1
  13. package/dist/Source.js +2 -2
  14. package/dist/Source.js.map +1 -1
  15. package/dist/boot/AppLifecycle.d.ts.map +1 -1
  16. package/dist/boot/AppLifecycle.js.map +1 -1
  17. package/dist/boot/Emissions.d.ts.map +1 -1
  18. package/dist/boot/Emissions.js.map +1 -1
  19. package/dist/boot/ambient.als.d.ts.map +1 -1
  20. package/dist/boot/ambient.als.js.map +1 -1
  21. package/dist/boot/apply.d.ts.map +1 -1
  22. package/dist/boot/apply.js.map +1 -1
  23. package/dist/boot/arity.d.ts +13 -0
  24. package/dist/boot/arity.d.ts.map +1 -0
  25. package/dist/boot/arity.js +33 -0
  26. package/dist/boot/arity.js.map +1 -0
  27. package/dist/boot/bootstrap.d.ts.map +1 -1
  28. package/dist/boot/bootstrap.js +3 -2
  29. package/dist/boot/bootstrap.js.map +1 -1
  30. package/dist/boot/card.d.ts.map +1 -1
  31. package/dist/boot/card.js.map +1 -1
  32. package/dist/boot/frame.d.ts.map +1 -1
  33. package/dist/boot/frame.js.map +1 -1
  34. package/dist/boot/install.d.ts.map +1 -1
  35. package/dist/boot/install.js +8 -2
  36. package/dist/boot/install.js.map +1 -1
  37. package/dist/boot/ownership.d.ts.map +1 -1
  38. package/dist/boot/ownership.js.map +1 -1
  39. package/dist/boot/relations.d.ts +10 -0
  40. package/dist/boot/relations.d.ts.map +1 -1
  41. package/dist/boot/relations.js +35 -2
  42. package/dist/boot/relations.js.map +1 -1
  43. package/dist/boot/remote.d.ts.map +1 -1
  44. package/dist/boot/remote.js +8 -2
  45. package/dist/boot/remote.js.map +1 -1
  46. package/dist/boot/seed.js +1 -1
  47. package/dist/boot/seed.js.map +1 -1
  48. package/dist/boot/statement-drift.d.ts +6 -0
  49. package/dist/boot/statement-drift.d.ts.map +1 -1
  50. package/dist/boot/statement-drift.js +19 -0
  51. package/dist/boot/statement-drift.js.map +1 -1
  52. package/dist/boot/together.d.ts.map +1 -1
  53. package/dist/boot/together.js.map +1 -1
  54. package/dist/builtin/Carry.d.ts.map +1 -1
  55. package/dist/builtin/Carry.js.map +1 -1
  56. package/dist/builtin/Logger.d.ts.map +1 -1
  57. package/dist/builtin/Logger.js +3 -1
  58. package/dist/builtin/Logger.js.map +1 -1
  59. package/dist/builtin/config.js.map +1 -1
  60. package/dist/contract.d.ts +2 -0
  61. package/dist/contract.d.ts.map +1 -1
  62. package/dist/contract.js +2 -0
  63. package/dist/contract.js.map +1 -1
  64. package/dist/crypto/encoding.d.ts.map +1 -1
  65. package/dist/crypto/encoding.js.map +1 -1
  66. package/dist/crypto/node.d.ts.map +1 -1
  67. package/dist/crypto/node.js.map +1 -1
  68. package/dist/crypto/webcrypto.d.ts.map +1 -1
  69. package/dist/crypto/webcrypto.js.map +1 -1
  70. package/dist/descriptor/Fronds.d.ts.map +1 -1
  71. package/dist/descriptor/Fronds.js.map +1 -1
  72. package/dist/dispatch/ArrayResult.d.ts.map +1 -1
  73. package/dist/dispatch/ArrayResult.js.map +1 -1
  74. package/dist/dispatch/Dispatcher.d.ts.map +1 -1
  75. package/dist/dispatch/Dispatcher.js.map +1 -1
  76. package/dist/dispatch/HandlerFacade.d.ts +6 -1
  77. package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
  78. package/dist/dispatch/HandlerFacade.js +11 -3
  79. package/dist/dispatch/HandlerFacade.js.map +1 -1
  80. package/dist/dispatch/InFlight.d.ts.map +1 -1
  81. package/dist/dispatch/InFlight.js.map +1 -1
  82. package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
  83. package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
  84. package/dist/dispatch/OutputView.d.ts.map +1 -1
  85. package/dist/dispatch/OutputView.js +7 -0
  86. package/dist/dispatch/OutputView.js.map +1 -1
  87. package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
  88. package/dist/dispatch/PresenterExecutor.js +7 -0
  89. package/dist/dispatch/PresenterExecutor.js.map +1 -1
  90. package/dist/dispatch/Received.d.ts +3 -0
  91. package/dist/dispatch/Received.d.ts.map +1 -0
  92. package/dist/dispatch/Received.js +2 -0
  93. package/dist/dispatch/Received.js.map +1 -0
  94. package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
  95. package/dist/dispatch/RouteRegistry.js.map +1 -1
  96. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  97. package/dist/dispatch/StorageGuard.js +4 -1
  98. package/dist/dispatch/StorageGuard.js.map +1 -1
  99. package/dist/dispatch/decoded.d.ts +19 -0
  100. package/dist/dispatch/decoded.d.ts.map +1 -0
  101. package/dist/dispatch/decoded.js +36 -0
  102. package/dist/dispatch/decoded.js.map +1 -0
  103. package/dist/entry/facade.d.ts +9 -2
  104. package/dist/entry/facade.d.ts.map +1 -1
  105. package/dist/entry/facade.js +16 -7
  106. package/dist/entry/facade.js.map +1 -1
  107. package/dist/identity-keys.d.ts.map +1 -1
  108. package/dist/identity-keys.js.map +1 -1
  109. package/dist/index.d.ts +1 -0
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +1 -0
  112. package/dist/index.js.map +1 -1
  113. package/dist/loader.d.ts.map +1 -1
  114. package/dist/loader.js.map +1 -1
  115. package/dist/prefab/CrudConstructor.d.ts.map +1 -1
  116. package/dist/prefab/CrudConstructor.js +3 -2
  117. package/dist/prefab/CrudConstructor.js.map +1 -1
  118. package/dist/prefab/CrudOps.d.ts +2 -2
  119. package/dist/prefab/CrudOps.d.ts.map +1 -1
  120. package/dist/prefab/MirrorConstructor.js +1 -1
  121. package/dist/prefab/MirrorConstructor.js.map +1 -1
  122. package/dist/prefab/Refreshed.d.ts +7 -2
  123. package/dist/prefab/Refreshed.d.ts.map +1 -1
  124. package/dist/prefab/collector.d.ts.map +1 -1
  125. package/dist/prefab/collector.js.map +1 -1
  126. package/dist/prefab/presenter.d.ts.map +1 -1
  127. package/dist/prefab/presenter.js.map +1 -1
  128. package/dist/storage/Storage.d.ts.map +1 -1
  129. package/dist/storage/Storage.js.map +1 -1
  130. package/dist/storage/Store.d.ts.map +1 -1
  131. package/dist/storage/Store.js +7 -11
  132. package/dist/storage/Store.js.map +1 -1
  133. package/dist/verify.d.ts.map +1 -1
  134. package/dist/verify.js.map +1 -1
  135. package/dist/wire/AppMiddleware.d.ts +10 -1
  136. package/dist/wire/AppMiddleware.d.ts.map +1 -1
  137. package/dist/wire/AppMiddleware.js.map +1 -1
  138. package/dist/wire/AppNext.d.ts +2 -1
  139. package/dist/wire/AppNext.d.ts.map +1 -1
  140. package/dist/wire/Facade.d.ts +14 -2
  141. package/dist/wire/Facade.d.ts.map +1 -1
  142. package/dist/wire/Facade.js.map +1 -1
  143. package/dist/wire/FougereError.d.ts.map +1 -1
  144. package/dist/wire/FougereError.js +1 -1
  145. package/dist/wire/FougereError.js.map +1 -1
  146. package/dist/wire/Invocation.d.ts.map +1 -1
  147. package/dist/wire/Invocation.js.map +1 -1
  148. package/dist/wire/OperationContract.d.ts.map +1 -1
  149. package/dist/wire/OperationContract.js +1 -1
  150. package/dist/wire/OperationContract.js.map +1 -1
  151. package/dist/wire/Page.d.ts +32 -0
  152. package/dist/wire/Page.d.ts.map +1 -0
  153. package/dist/wire/Page.js +32 -0
  154. package/dist/wire/Page.js.map +1 -0
  155. package/dist/wire/RouteAddress.d.ts.map +1 -1
  156. package/dist/wire/RouteAddress.js.map +1 -1
  157. package/dist/wire/Signature.d.ts +7 -0
  158. package/dist/wire/Signature.d.ts.map +1 -1
  159. package/dist/wire/binding.d.ts.map +1 -1
  160. package/dist/wire/binding.js.map +1 -1
  161. package/dist/wire/card/IdentityCard.d.ts.map +1 -1
  162. package/dist/wire/card/IdentityCard.js.map +1 -1
  163. package/dist/wire/drift.d.ts.map +1 -1
  164. package/dist/wire/drift.js.map +1 -1
  165. package/dist/wire/http-error.d.ts.map +1 -1
  166. package/dist/wire/http-error.js.map +1 -1
  167. package/dist/wire/loggerMiddleware.d.ts.map +1 -1
  168. package/dist/wire/loggerMiddleware.js.map +1 -1
  169. package/package.json +5 -5
  170. package/src/CallIdentity.ts +6 -0
  171. package/src/DomainCluster.ts +2 -1
  172. package/src/EffectiveOperationModel.ts +38 -1
  173. package/src/FougereConfig.ts +3 -0
  174. package/src/FrondConfig.ts +2 -0
  175. package/src/Source.ts +2 -2
  176. package/src/boot/AppLifecycle.ts +1 -0
  177. package/src/boot/Emissions.ts +4 -0
  178. package/src/boot/ambient.als.ts +2 -0
  179. package/src/boot/apply.ts +1 -0
  180. package/src/boot/arity.ts +42 -0
  181. package/src/boot/bootstrap.ts +9 -2
  182. package/src/boot/card.ts +2 -0
  183. package/src/boot/frame.ts +6 -0
  184. package/src/boot/install.ts +8 -1
  185. package/src/boot/ownership.ts +1 -0
  186. package/src/boot/relations.ts +39 -2
  187. package/src/boot/remote.ts +10 -2
  188. package/src/boot/seed.ts +1 -1
  189. package/src/boot/statement-drift.ts +25 -0
  190. package/src/boot/together.ts +3 -0
  191. package/src/builtin/Carry.ts +1 -0
  192. package/src/builtin/Logger.ts +9 -1
  193. package/src/builtin/config.ts +1 -0
  194. package/src/contract.ts +3 -0
  195. package/src/crypto/encoding.ts +4 -0
  196. package/src/crypto/node.ts +2 -0
  197. package/src/crypto/webcrypto.ts +2 -0
  198. package/src/descriptor/Fronds.ts +2 -0
  199. package/src/dispatch/ArrayResult.ts +1 -0
  200. package/src/dispatch/Dispatcher.ts +1 -0
  201. package/src/dispatch/HandlerFacade.ts +16 -4
  202. package/src/dispatch/InFlight.ts +2 -0
  203. package/src/dispatch/LocalRoutePolicy.ts +1 -0
  204. package/src/dispatch/OutputView.ts +7 -0
  205. package/src/dispatch/PresenterExecutor.ts +7 -0
  206. package/src/dispatch/Received.ts +2 -0
  207. package/src/dispatch/RouteRegistry.ts +4 -0
  208. package/src/dispatch/StorageGuard.ts +9 -1
  209. package/src/dispatch/decoded.ts +37 -0
  210. package/src/entry/facade.ts +21 -9
  211. package/src/identity-keys.ts +2 -0
  212. package/src/index.ts +1 -0
  213. package/src/loader.ts +1 -0
  214. package/src/prefab/CrudConstructor.ts +4 -2
  215. package/src/prefab/CrudOps.ts +2 -2
  216. package/src/prefab/MirrorConstructor.ts +1 -1
  217. package/src/prefab/Refreshed.ts +7 -2
  218. package/src/prefab/collector.ts +1 -0
  219. package/src/prefab/presenter.ts +1 -0
  220. package/src/storage/Storage.ts +2 -0
  221. package/src/storage/Store.ts +15 -10
  222. package/src/verify.ts +2 -0
  223. package/src/wire/AppMiddleware.ts +12 -1
  224. package/src/wire/AppNext.ts +2 -1
  225. package/src/wire/Facade.ts +14 -2
  226. package/src/wire/FougereError.ts +3 -1
  227. package/src/wire/Invocation.ts +2 -0
  228. package/src/wire/OperationContract.ts +2 -1
  229. package/src/wire/Page.ts +50 -0
  230. package/src/wire/RouteAddress.ts +2 -0
  231. package/src/wire/Signature.ts +7 -0
  232. package/src/wire/binding.ts +1 -0
  233. package/src/wire/card/IdentityCard.ts +1 -0
  234. package/src/wire/drift.ts +3 -0
  235. package/src/wire/http-error.ts +2 -0
  236. package/src/wire/loggerMiddleware.ts +1 -0
@@ -1,4 +1,4 @@
1
- import { applyCreate, applyUpdate, Lifecycle, Role, type SchemaView } from '@fougere/schema';
1
+ import { applyCreate, applyUpdate, Role, type SchemaView } from '@fougere/schema';
2
2
  import { comparisonOf, comparisonsIn, type Comparison } from './Comparison.js';
3
3
  import type { Storage } from './Storage.js';
4
4
  import type { StorageFactory } from './StorageFactory.js';
@@ -21,7 +21,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
21
21
  return (entity: SchemaView, name: string): Storage => {
22
22
  const fields = entity.getFields();
23
23
  const store = open(entity, name);
24
- const pk = Object.entries(fields).find(([, field]) => Role.of(field).isPrimary)?.[0] ?? 'id';
24
+ const pk = Object.entries(fields).find(([, field]) => Role.of(field).isPrimary())?.[0] ?? 'id';
25
25
  // `Storage.findById(id: string)` — but a key can hold a number, and a Map keyed on
26
26
  // `1` does not answer `'1'`. SQL never had the question; here the divergence was
27
27
  // silent and only on this storage.
@@ -46,20 +46,18 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
46
46
  ? Object.fromEntries(Object.entries(values).filter(([key]) => selected.has(key)))
47
47
  : values);
48
48
 
49
- // Same contract as SQL: the key and the creation stamps survive an overwrite.
49
+ // Same contract as SQL: a row that is already there is UPDATED, so what the write leaves
50
+ // out stays where it was.
50
51
  // Named, and not reached through `this`: a caller may have wrapped these gestures,
51
52
  // and a derived one that goes back through the front facade is judged twice.
52
53
  const upsert = async (input: Partial<Record<string, unknown>>): Promise<Values> => {
53
- const values = applyCreate(fields, applyUpdate(fields, input));
54
- const id = values[pk] as string | undefined;
54
+ const created = applyCreate(fields, applyUpdate(fields, input));
55
+ const id = created[pk] as string | undefined;
55
56
  if (id === undefined) throw new Error(`${name}.upsert(): no \`${pk}\` — an upsert needs the key it writes at.`);
56
57
  const previous = await store.get(keyOf(id));
57
- if (previous) {
58
- for (const [key, field] of Object.entries(fields)) {
59
- if (key === pk || Lifecycle.of(field).stampedOnce) values[key] = previous[key];
60
- }
61
- }
58
+ const values = previous ? { ...previous, ...applyUpdate(fields, input) } : created;
62
59
  await store.set(keyOf(id), values);
60
+
63
61
  return pick(values);
64
62
  };
65
63
 
@@ -74,10 +72,12 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
74
72
  },
75
73
  async findById(id: string) {
76
74
  const values = await store.get(keyOf(id));
75
+
77
76
  return values && pick(values);
78
77
  },
79
78
  async findBy(criteria: Record<string, unknown>) {
80
79
  const values = (await store.all()).find((held) => matches(held, criteria));
80
+
81
81
  return values && pick(values);
82
82
  },
83
83
  async findAllBy(criteria: Record<string, unknown>) {
@@ -90,6 +90,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
90
90
  const values = await store.get(keyOf(id));
91
91
  if (values) found.set(String(id), pick(values));
92
92
  }
93
+
93
94
  return found;
94
95
  },
95
96
  // The dual, same contract as SQL: grouped by the value read off the instance.
@@ -103,11 +104,13 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
103
104
  const bucket = grouped.get(key);
104
105
  if (bucket) bucket.push(pick(values)); else grouped.set(key, [pick(values)]);
105
106
  }
107
+
106
108
  return grouped;
107
109
  },
108
110
  upsert,
109
111
  async upsertAll(inputs: readonly Partial<Record<string, unknown>>[]) {
110
112
  for (const input of inputs) await upsert(input);
113
+
111
114
  return inputs.length;
112
115
  },
113
116
  async create(input: Partial<Record<string, unknown>>) {
@@ -128,6 +131,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
128
131
  throw new Error(`${name}.create: '${pk}' ${JSON.stringify(id)} already exists.`);
129
132
  }
130
133
  await store.set(keyOf(id), values);
134
+
131
135
  return pick(values);
132
136
  },
133
137
  async update(id: string, input: Partial<Record<string, unknown>>) {
@@ -135,6 +139,7 @@ export function storageOver(open: (entity: SchemaView, name: string) => Store):
135
139
  if (!existing) throw new Error(`Not found: ${id}`);
136
140
  const updated = { ...existing, ...applyUpdate(fields, input), [pk]: existing[pk] };
137
141
  await store.set(keyOf(id), updated);
142
+
138
143
  return pick(updated);
139
144
  },
140
145
  async delete(id: string) { return await store.delete(keyOf(id)); },
package/src/verify.ts CHANGED
@@ -42,6 +42,7 @@ function registrationsOf(frond: FrondDescriptor): Map<string, Registration> {
42
42
  put(storageKeyOf(e.name), 'storage');
43
43
  put(repositoryKeyOf(e.name), 'repository');
44
44
  }
45
+
45
46
  return out;
46
47
  }
47
48
 
@@ -143,6 +144,7 @@ export function verify(app: { fronds: readonly FrondDescriptor[] }): Misplaced[]
143
144
  }
144
145
  }
145
146
  }
147
+
146
148
  return violations;
147
149
  }
148
150
 
@@ -1,7 +1,16 @@
1
1
  import type { OperationContext } from './OperationContext.js';
2
2
  import type { AppNext } from './AppNext.js';
3
3
 
4
- export type AppMiddleware = (ctx: OperationContext, next: AppNext) => Promise<unknown>;
4
+ /**
5
+ * A middleware answers what the chain answered — the same TYPE, whatever it does with the value.
6
+ *
7
+ * `T` is the whole rule and nothing enforces it at runtime: a middleware is handed a type it
8
+ * cannot name, so the only value of that type it can produce is the one `next()` gave it. It may
9
+ * observe it, log it, replace it with another of the same shape, or refuse by throwing; it may
10
+ * not wrap it in `{ data, meta }` nor invent one, because a caller reads the handler's signature
11
+ * and nothing tells that signature a middleware stood in the way.
12
+ */
13
+ export type AppMiddleware = <T>(ctx: OperationContext, next: AppNext<T>) => Promise<T>;
5
14
 
6
15
  // ── Runner ──────────────────────────────────────
7
16
 
@@ -18,7 +27,9 @@ export function runMiddlewares(
18
27
  if (index < middlewares.length) {
19
28
  return middlewares[index++](ctx, next);
20
29
  }
30
+
21
31
  return handler();
22
32
  };
33
+
23
34
  return next();
24
35
  }
@@ -1 +1,2 @@
1
- export type AppNext = () => Promise<unknown>;
1
+ /** What a middleware holds: the rest of the chain, and the answer it will hand back. */
2
+ export type AppNext<T = unknown> = () => Promise<T>;
@@ -10,10 +10,22 @@ type Served = keyof FougereOperations & string;
10
10
  */
11
11
  type AddressIn<Key> = Key extends `${infer Address}.${string}` ? Address : never;
12
12
 
13
- /** The facade built in front of a handler — the framework's second port, after `Storage`. */
13
+ /**
14
+ * The facade built in front of a handler — the framework's second port, after `Storage`.
15
+ *
16
+ * What the handler knows is WHICH operations exist and what each one answers. The two ends are
17
+ * the port's own: an invocation goes in where the handler takes positional arguments, and a
18
+ * promise comes back where the handler may answer a bare value. A facade is a crossing, and a
19
+ * crossing is awaited before any transport — the dispatch resolves a route, runs the middlewares
20
+ * and awaits the collectors, so `readLocation(): string` was typed as answering now and never did.
21
+ *
22
+ * `Awaited` because a promise does not stack: `Promise.resolve(p)` IS `p`, so writing
23
+ * `Promise<R>` over an async handler would describe a `Promise<Promise<Post>>` that no value can
24
+ * have — `.then` would hand its callback a promise the runtime never delivers.
25
+ */
14
26
  export type Facade<T> = {
15
27
  [K in keyof T]: T[K] extends (...args: never[]) => infer R
16
- ? (invocation?: InvocationContext) => R
28
+ ? (invocation?: InvocationContext) => Promise<Awaited<R>>
17
29
  : never;
18
30
  };
19
31
 
@@ -20,7 +20,7 @@ export class FougereError<Code extends ErrorCode = ErrorCode> extends Error {
20
20
 
21
21
  constructor(options: FougereErrorOptions<Code>) {
22
22
  super(options.message, { cause: options.cause });
23
- this.name = new.target.name;
23
+ this.name = 'FougereError';
24
24
  this.code = options.code;
25
25
  this.entity = options.entity;
26
26
  this.operation = options.operation;
@@ -41,6 +41,7 @@ export class FougereError<Code extends ErrorCode = ErrorCode> extends Error {
41
41
  static fromJSON(json: unknown): FougereError {
42
42
  const raw = (typeof json === 'object' && json !== null ? json : {}) as Record<string, unknown>;
43
43
  const known = Object.values(ErrorCode).includes(raw.code as ErrorCode);
44
+
44
45
  return new FougereError({
45
46
  code: known ? (raw.code as ErrorCode) : ErrorCode.INTERNAL_ERROR,
46
47
  message: typeof raw.message === 'string' ? raw.message : 'Unknown error',
@@ -65,5 +66,6 @@ export function validationErrorsOf(error: unknown): ValidationError[] | undefine
65
66
  && Array.isArray((entry as ValidationError).path)
66
67
  && typeof (entry as ValidationError).message === 'string',
67
68
  );
69
+
68
70
  return refusals.length === details.length ? refusals : undefined;
69
71
  }
@@ -8,6 +8,7 @@ function canonicalValue(value: unknown): unknown {
8
8
 
9
9
  const prototype = Object.getPrototypeOf(value);
10
10
  if (prototype !== Object.prototype && prototype !== null) return value;
11
+
11
12
  return canonicalRecord(value);
12
13
  }
13
14
 
@@ -19,6 +20,7 @@ function canonicalRecord(value: unknown): Record<string, unknown> {
19
20
  for (const [key, member] of Object.entries(value)) {
20
21
  if (member !== undefined) result[key] = canonicalValue(member);
21
22
  }
23
+
22
24
  return Object.freeze(result);
23
25
  }
24
26
 
@@ -36,9 +36,10 @@ export function cardinalityOf(type: TypeRef | undefined): OperationContract['car
36
36
  if (!type) return undefined;
37
37
  const inner = type.name === 'Promise' ? type.generics?.[0] : type;
38
38
  if (!inner) return 'none';
39
- if (inner.name === 'ListResult') return 'page';
39
+ if (inner.name === 'Page' || inner.name === 'ListResult') return 'page';
40
40
  if (inner.array) return 'many';
41
41
  if (PRIMITIVE_RETURNS.has(inner.name)) return 'none';
42
+
42
43
  return inner.nullable || inner.undefined ? 'maybe' : 'one';
43
44
  }
44
45
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What a paged operation answers — the rows, and what the storage said about the rest of them.
3
+ *
4
+ * An ENVELOPE rather than the array `Storage.list` hands back: that one carries `total` and
5
+ * `hasMore` as properties OF the array, and `JSON.stringify` keeps only the indices. Measured
6
+ * 2026-09-18 — `page.total` is 42 in this process and `undefined` behind `fronds:`, off the same
7
+ * code. What crosses has to be named, so it is a field.
8
+ *
9
+ * `Storage.list` keeps its array: it answers inside a frond and never crosses.
10
+ */
11
+ export interface Page<Row> {
12
+ items: Row[];
13
+ /** Only when the caller asked to count. */
14
+ total?: number;
15
+ hasMore?: boolean;
16
+ endCursor?: string;
17
+ }
18
+
19
+ /**
20
+ * The array `Storage.list` answers, read as the page an operation hands over.
21
+ *
22
+ * A storage handed in by a host may answer the envelope already — `storageFactory` is its door,
23
+ * and nothing there is obliged to subclass an array. Taken as it comes in that case.
24
+ */
25
+ export function pageOf<Row>(rows: Page<Row> | readonly Row[]): Page<Row> {
26
+ if (!Array.isArray(rows)) return rows as Page<Row>;
27
+
28
+ const { total, hasMore, endCursor } = rows as unknown as Omit<Page<Row>, 'items'>;
29
+
30
+ return {
31
+ items: [...rows],
32
+ ...(total !== undefined && { total }),
33
+ ...(hasMore !== undefined && { hasMore }),
34
+ ...(endCursor !== undefined && { endCursor }),
35
+ };
36
+ }
37
+
38
+ /**
39
+ * The value read as a page, or nothing.
40
+ *
41
+ * Read from the FORM, and arbitrated by the schema: an entity legally declaring a field named
42
+ * `items` answers a row, not a page, and it is the only thing that could tell the two apart.
43
+ */
44
+ export function asPage(value: unknown, fields: Record<string, unknown>): Page<unknown> | undefined {
45
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
46
+ if ('items' in fields) return undefined;
47
+ const page = value as Page<unknown>;
48
+
49
+ return Array.isArray(page.items) ? page : undefined;
50
+ }
@@ -3,6 +3,7 @@ import type { RouteAddressInput } from './RouteAddressInput.js';
3
3
  /** Refuses an empty segment, so no address can be built that nothing can serve. */
4
4
  function required(value: string, name: string): string {
5
5
  if (value.trim().length === 0) throw new TypeError(`Route ${name} cannot be empty`);
6
+
6
7
  return value;
7
8
  }
8
9
 
@@ -38,6 +39,7 @@ export class RouteAddress {
38
39
  /** Writes the address the way a log and an error message name it. */
39
40
  toString(): string {
40
41
  const audience = this.surface ? `${this.surface}/` : '';
42
+
41
43
  return `${audience}${this.entity}.${this.operation}`;
42
44
  }
43
45
 
@@ -10,4 +10,11 @@ export interface Signature {
10
10
  inherited?: boolean;
11
11
  /** The operation in words — the first sentence of the method's doc comment. */
12
12
  description?: string;
13
+ /**
14
+ * The first thing in the return type that is not data — its path, and the type sitting there.
15
+ *
16
+ * Written whenever a checker read the signature, and judged only where nothing converts the
17
+ * output: what a declared entity answers is encoded by its fields.
18
+ */
19
+ notData?: string;
13
20
  }
@@ -38,6 +38,7 @@ const PRIMITIVES = new Set(['string', 'number', 'boolean']);
38
38
  function coercionFor(typeName: string): 'number' | 'boolean' | undefined {
39
39
  if (typeName === 'number') return 'number';
40
40
  if (typeName === 'boolean') return 'boolean';
41
+
41
42
  return undefined;
42
43
  }
43
44
 
@@ -27,6 +27,7 @@ export function assertIdentityCard(value: unknown, source: string): IdentityCard
27
27
  if (!frond || typeof frond.name !== 'string') throw cardRefusal(source, 'a frond with no name');
28
28
  if (!Array.isArray(frond.facades)) throw cardRefusal(source, `frond '${frond.name}' has no valid facades array`);
29
29
  }
30
+
30
31
  return card as IdentityCard;
31
32
  }
32
33
 
package/src/wire/drift.ts CHANGED
@@ -29,6 +29,7 @@ function facadesOf(card: IdentityCard, frond: string): Map<string, Answering> {
29
29
  found.set(facade.name, { ops: new Set(facade.ops.map((op) => op.name)), schema: facade.schema });
30
30
  }
31
31
  }
32
+
32
33
  return found;
33
34
  }
34
35
 
@@ -38,6 +39,7 @@ function factsOf(card: IdentityCard, frond: string): Map<string, SchemaDescripto
38
39
  if (one.name !== frond) continue;
39
40
  for (const fact of one.facts ?? []) found.set(fact.name, fact.schema as SchemaDescriptor | undefined);
40
41
  }
42
+
41
43
  return found;
42
44
  }
43
45
 
@@ -117,6 +119,7 @@ export function explain(drift: CardDrift): string[] {
117
119
  if (changes === 'gone') { lines.push(`${fact} — you subscribe to it, it is no longer announced`); continue; }
118
120
  for (const change of changes) lines.push(`${fact} — ${describe(change)} → re-sync and deploy the readers, THEN the sender`);
119
121
  }
122
+
120
123
  return lines;
121
124
  }
122
125
 
@@ -40,6 +40,7 @@ export function toPublicError(err: FougereError): ReturnType<FougereError['toJSO
40
40
  if (err.code !== ErrorCode.INTERNAL_ERROR) return err.toJSON();
41
41
  const where = [err.entity, err.operation].filter(Boolean).join('.');
42
42
  log.error(`${where || 'internal'}: ${err.message}`, err.cause ?? err);
43
+
43
44
  return {
44
45
  code: ErrorCode.INTERNAL_ERROR,
45
46
  message: 'Internal error',
@@ -60,5 +61,6 @@ export function toHttpError(err: unknown): { status: number; body: ReturnType<Fo
60
61
  message: (err as { message?: string })?.message ?? 'Internal error',
61
62
  cause: err,
62
63
  });
64
+
63
65
  return { status: 500, body: toPublicError(framed) };
64
66
  }
@@ -19,6 +19,7 @@ export function loggerMiddleware(logger: Logger): AppMiddleware {
19
19
  const result = await next();
20
20
  const ms = (performance.now() - start).toFixed(1);
21
21
  logger.info(`${ctx.entity}.${ctx.operation} (${ms}ms)`);
22
+
22
23
  return result;
23
24
  } catch (err) {
24
25
  const ms = (performance.now() - start).toFixed(1);