@aventara/build 1.0.0-rc.10 → 1.0.0-rc.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @aventara/build
2
2
 
3
+ ## 1.0.0-rc.12
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [afd5eda]
8
+ - Updated dependencies [fdeee8a]
9
+ - @aventara/client@1.0.0-rc.12
10
+ - @aventara/core@1.0.0-rc.12
11
+
12
+ ## 1.0.0-rc.11
13
+
14
+ ### Patch Changes
15
+
16
+ - 6197c08: `@/aventara/universal`'s calls are the generated client's own calls again. The entry no longer wraps each one in
17
+ `io().then(call)`, so its own `signal`, `avClient.all`'s `signal` and strict abort, and `avLazy` all reach it, in the
18
+ browser and on the server. Next's `io()` is now awaited before the request is sent.
19
+
20
+ In the browser, the calls made in one tick are still grouped into one `/_all`. On the server, none is grouped: each
21
+ call has a client of its own with grouping off, and it is sent from the async context of the code that made it. Before,
22
+ one queue served every request. Calls from two concurrent requests, or a call inside `unstable_cache` and one outside
23
+ it, could share a flush and run with the first call's headers and `state`.
24
+
25
+ `avLazy` no longer keeps one answer for every request on a server:
26
+
27
+ - The generated client's `avLazy` keeps its answer in a page. On a server (no `location`) it keeps none, and each await
28
+ makes the call again.
29
+ - `@aventara/core`'s `avLazy` keeps none: each await makes the call again.
30
+ - `@/aventara/universal` now exports its own `avLazy`, which keeps one answer per request inside a React render
31
+ (React's `cache`, which the entry now imports) and none where React keeps no request.
32
+
33
+ Before, an `avLazy` made at module scope handed the first request's answer to every request after it.
34
+
35
+ `ready()`'s in-process path now splits a cached read's GET URL into its path and its `?q=&v=` query, with an empty
36
+ body, so the read reaches its GET route instead of being answered as absent.
37
+
38
+ - 0b2f077: `aventara-build` reads `server-only` as an empty module, as a Next.js server bundle does. A `src/aventara/config.ts`
39
+ that imports it, or imports a module that does, now builds; before, the package threw outside the `react-server`
40
+ condition and the build refused the configuration ("could not be run: This module cannot be imported from a Client
41
+ Component module").
42
+ - Updated dependencies [6197c08]
43
+ - Updated dependencies [5ac10f7]
44
+ - Updated dependencies [e0e89c1]
45
+ - Updated dependencies [186bc29]
46
+ - Updated dependencies [33b5f83]
47
+ - Updated dependencies [6d8b589]
48
+ - Updated dependencies [11ece49]
49
+ - Updated dependencies [92fd093]
50
+ - Updated dependencies [e5e98e6]
51
+ - Updated dependencies [a05d970]
52
+ - @aventara/client@1.0.0-rc.11
53
+ - @aventara/core@1.0.0-rc.11
54
+
3
55
  ## 1.0.0-rc.10
4
56
 
5
57
  ### Patch Changes
@@ -12,7 +12,7 @@ const ENTRY_BANNER = [
12
12
  ].join("\n");
13
13
  export const SERVER_CALL_REFUSAL = "avClient from @/aventara/client runs in the browser only, and this call ran on the server, where a Client Component renders first: on the server, call avClient from @/aventara/server, or from @/aventara/universal for data the browser then hydrates.";
14
14
  const BROWSER_GUARD_SOURCE = `/** \`call\`, rejected instead of run when \`refusal()\` names why. */
15
- function guarded(call: (...args: unknown[]) => unknown): (...args: unknown[]) => unknown {
15
+ function guarded(call: Call): Call {
16
16
  return (...args) => {
17
17
  const refused = refusal();
18
18
  return refused === undefined ? call(...args) : Promise.reject(new Error(refused));
@@ -33,40 +33,55 @@ function ioFirst(): Promise<void> {
33
33
  }
34
34
  }
35
35
 
36
- /** \`call\`, once \`io()\` lets it run, rejected instead of run when \`refusal()\` names why. */
37
- function guarded(call: (...args: unknown[]) => unknown): (...args: unknown[]) => unknown {
36
+ /**
37
+ * \`call\`, made at once when it may run (its request awaits \`io()\`); when
38
+ * \`refusal()\` names why it may not, made once \`io()\` lets it if it may run
39
+ * then, and rejected instead otherwise.
40
+ */
41
+ function guarded(call: Call): Call {
38
42
  return (...args) =>
39
- ioFirst().then(() => {
40
- const refused = refusal();
41
- return refused === undefined ? call(...args) : Promise.reject(new Error(refused));
42
- });
43
+ refusal() === undefined
44
+ ? call(...args)
45
+ : ioFirst().then(() => {
46
+ const refused = refusal();
47
+ return refused === undefined ? call(...args) : Promise.reject(new Error(refused));
48
+ });
43
49
  }
44
50
  `;
45
- const GUARD_SOURCE = `/** A frozen copy of one Resource's families, or one family's variants, each call guarded. */
46
- function guardedTree(tree: object): object {
51
+ const GUARD_SOURCE = `/**
52
+ * A frozen copy of one Resource's families, or one family's variants: each call
53
+ * guarded, and made on what \`at\` reads of the \`current()\` client.
54
+ */
55
+ function guardedTree(tree: object, at: (client: AvClient) => unknown): object {
47
56
  const copy: Record<string, unknown> = Object.create(Object.getPrototypeOf(tree) as object | null);
48
57
  for (const [key, value] of Object.entries(tree)) {
49
- copy[key] = typeof value === "function" ? guarded(value as (...args: unknown[]) => unknown) : guardedTree(value as object);
58
+ const member = (client: AvClient): unknown => (at(client) as Readonly<Record<string, unknown>>)[key];
59
+ copy[key] =
60
+ typeof value === "function"
61
+ ? guarded((...args) => (member(current()) as Call)(...args))
62
+ : guardedTree(value as object, member);
50
63
  }
51
64
  return Object.freeze(copy);
52
65
  }
53
66
 
54
67
  /**
55
- * The client with every call that sends a request guarded; \`tx\` builds
56
- * deferred operations and sends nothing, so it is kept as it is.
68
+ * A client shaped as \`template\`, every call that sends a request guarded and
69
+ * made on the \`current()\` client; \`tx\` builds deferred operations and sends
70
+ * nothing, so the template's is kept as it is.
57
71
  */
58
- function guardedClient(client: AvClient): AvClient {
72
+ function guardedClient(template: AvClient): AvClient {
59
73
  const copy: AvClient = Object.create(AvClient.prototype) as AvClient;
60
- for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(client))) {
74
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(template))) {
61
75
  const value: unknown = descriptor.value;
76
+ const member = (client: AvClient): unknown => (client as unknown as Readonly<Record<string, unknown>>)[key];
62
77
  Object.defineProperty(copy, key, {
63
78
  ...descriptor,
64
79
  value:
65
80
  key === "tx"
66
81
  ? value
67
82
  : typeof value === "function"
68
- ? guarded(value as (...args: unknown[]) => unknown)
69
- : guardedTree(value as object),
83
+ ? guarded((...args) => (member(current()) as Call)(...args))
84
+ : guardedTree(value as object, member),
70
85
  });
71
86
  }
72
87
  return copy;
@@ -100,6 +115,16 @@ function refusal(): string | undefined {
100
115
  return onServer() ? SERVER_CALL : undefined;
101
116
  }
102
117
 
118
+ type Call = (...args: unknown[]) => unknown;
119
+
120
+ /** The one client every call is made on. */
121
+ const CLIENT = new AvClient();
122
+
123
+ /** The client a call made here and now is made on: the one client. */
124
+ function current(): AvClient {
125
+ return CLIENT;
126
+ }
127
+
103
128
  ${BROWSER_GUARD_SOURCE}
104
129
  ${GUARD_SOURCE}
105
130
  /**
@@ -107,7 +132,7 @@ ${GUARD_SOURCE}
107
132
  * on the server (while a Client Component renders there) rejects instead: on the
108
133
  * server, import \`avClient\` from \`@/aventara/server\` or \`@/aventara/universal\`.
109
134
  */
110
- export const avClient: AvClient = guardedClient(new AvClient());
135
+ export const avClient: AvClient = guardedClient(CLIENT);
111
136
 
112
137
  /** The key a dehydrated value's tag is under: \`{ ${JSON.stringify(tagKey)}: <scalar>, ${JSON.stringify(valueKey)}: <its wire form> }\`. */
113
138
  const TAG = ${JSON.stringify(tagKey)};
@@ -177,9 +202,12 @@ function universalSource(style, accessVerbs) {
177
202
  return `${ENTRY_BANNER}
178
203
 
179
204
  import { io } from ${JSON.stringify(style.importFileExtension === "" ? "next/cache" : "next/cache.js")};
205
+ import { cache } from "react";
180
206
  import { AvClient } from ${from(`./${GENERATED_DIRECTORY}/client`)};
181
207
  import type { AvFetch } from ${from(`./${GENERATED_DIRECTORY}/runtime/transport`)};
182
208
 
209
+ import { avLazy as lazyCall } from ${from(`./${GENERATED_DIRECTORY}/runtime/operation-set`)};
210
+
183
211
  /** The key the server's in-process path is registered under, on globalThis, by ready() from @aventara/next. */
184
212
  const SERVER_PATH = Symbol.for(${JSON.stringify(UNIVERSAL_PATH_KEY)});
185
213
 
@@ -202,8 +230,9 @@ function refusal(): string | undefined {
202
230
  return onServer() && serverPath() === undefined ? UNREGISTERED : undefined;
203
231
  }
204
232
 
205
- /** In the browser, the platform's fetch; on the server, the in-process path. */
206
- function universalFetch(...request: unknown[]): unknown {
233
+ /** Once Next's \`io()\` lets it: in the browser, the platform's fetch; on the server, the in-process path. */
234
+ async function universalFetch(...request: unknown[]): Promise<unknown> {
235
+ await ioFirst();
207
236
  const reach = onServer() ? serverPath() : (globalThis as { readonly fetch?: AvFetch }).fetch;
208
237
  if (reach === undefined) {
209
238
  throw new TypeError(onServer() ? UNREGISTERED : "No fetch is available here.");
@@ -211,6 +240,51 @@ function universalFetch(...request: unknown[]): unknown {
211
240
  return (reach as (...request: unknown[]) => unknown)(...request);
212
241
  }
213
242
 
243
+ type Call = (...args: unknown[]) => unknown;
244
+
245
+ /** The browser's one client, whose calls are grouped, and the shape \`avClient\` copies. */
246
+ const BROWSER = new AvClient({ fetch: universalFetch as AvFetch });
247
+
248
+ /**
249
+ * The client a call made here and now is made on: the browser's one; on the
250
+ * server, a client of the call's own with grouping off, so the call is sent
251
+ * alone, from the async context it was made in (the request's, or a scope
252
+ * nested in it), and shares no queue with any other call.
253
+ */
254
+ function current(): AvClient {
255
+ return onServer() ? new AvClient({ fetch: universalFetch as AvFetch, grouping: false }) : BROWSER;
256
+ }
257
+
258
+ /** On the server, the calls each avLazy made in the request being rendered (React's \`cache\`). */
259
+ const requestCalls = cache((): WeakMap<object, Promise<unknown>> => new WeakMap());
260
+
261
+ /**
262
+ * Describe a call now, make it later: the tree's avLazy, which avClient.all
263
+ * accepts on either side. In the browser it makes the call once and keeps its
264
+ * answer. On the server it keeps one answer per request: inside a React
265
+ * request scope (a render), each request makes the call once, in its own
266
+ * context; where React keeps no request (a route handler, a Server Action),
267
+ * it keeps nothing, and each await makes the call again.
268
+ *
269
+ * @example const posts = avLazy(() => avClient.Post.find.many({}));
270
+ */
271
+ export function avLazy<T>(run: () => PromiseLike<T>): Promise<T> {
272
+ const key = {};
273
+ return lazyCall(() => {
274
+ if (!onServer()) {
275
+ return run();
276
+ }
277
+ const calls = requestCalls();
278
+ const made = calls.get(key);
279
+ if (made !== undefined) {
280
+ return made as Promise<T>;
281
+ }
282
+ const call = Promise.resolve(run());
283
+ calls.set(key, call);
284
+ return call;
285
+ });
286
+ }
287
+
214
288
  ${UNIVERSAL_GUARD_SOURCE}
215
289
  ${GUARD_SOURCE}
216
290
  /**
@@ -221,7 +295,7 @@ ${GUARD_SOURCE}
221
295
  * prefetched on the server hydrates in the browser: pass it through
222
296
  * \`avDehydrate\` (\`@aventara/next\`) and \`avHydrate\` (\`@/aventara/client\`).
223
297
  */
224
- export const avClient: AvClient = ${accessVerbs ? "withServerAccess(" : ""}guardedClient(new AvClient({ fetch: universalFetch as AvFetch }))${accessVerbs ? ")" : ""};
298
+ export const avClient: AvClient = ${accessVerbs ? "withServerAccess(" : ""}guardedClient(BROWSER)${accessVerbs ? ")" : ""};
225
299
  ${accessVerbs ? SERVER_ACCESS_SOURCE : ""}`;
226
300
  }
227
301
  const UNIVERSAL_ACCESS_KEY = "aventara.next.universal.access";
@@ -5,6 +5,7 @@ import { createJiti } from "jiti";
5
5
  import { AvBuildError } from "../build.error.js";
6
6
  const EXTENSIONS = [".js", ".ts", ".mjs", ".cjs", ".mts", ".cts"];
7
7
  const EXTENSIONS_AS_LISTED = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
8
+ const SERVER_MARKERS = { "server-only": {} };
8
9
  const CONFIG_EXPORTS = ["aventaraConfig", "default"];
9
10
  function projectFile(projectDirectory, module, layout) {
10
11
  const present = EXTENSIONS.map((extension) => `${module}${extension}`).filter((file) => statSync(path.join(projectDirectory, file), {
@@ -107,6 +108,7 @@ export async function loadProjectModules(projectDirectory, { configFile, adapter
107
108
  fsCache: false,
108
109
  interopDefault: false,
109
110
  nativeModules: ["@aventara/core"],
111
+ virtualModules: SERVER_MARKERS,
110
112
  });
111
113
  const project = await projectCore(jiti);
112
114
  const config = configOf(configFile, await evaluate(jiti, projectDirectory, configFile));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/build",
3
- "version": "1.0.0-rc.10",
3
+ "version": "1.0.0-rc.12",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Compiles an Aventara application's Contracts at build time, for serverless and edge cold starts.",
6
6
  "type": "module",
@@ -16,18 +16,18 @@
16
16
  "CHANGELOG.md"
17
17
  ],
18
18
  "dependencies": {
19
- "@aventara/client": "1.0.0-rc.10",
19
+ "@aventara/client": "1.0.0-rc.12",
20
20
  "jiti": "2.7.0"
21
21
  },
22
22
  "peerDependencies": {
23
- "@aventara/core": "1.0.0-rc.10",
23
+ "@aventara/core": "1.0.0-rc.12",
24
24
  "typescript": ">=5.7.0 <7"
25
25
  },
26
26
  "publishConfig": {
27
27
  "access": "public"
28
28
  },
29
29
  "devDependencies": {
30
- "@aventara/core": "1.0.0-rc.10",
30
+ "@aventara/core": "1.0.0-rc.12",
31
31
  "@types/node": "24.10.1",
32
32
  "typescript": "^5.9.2"
33
33
  },