@fragno-dev/core 0.2.0 → 0.2.2

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 (146) hide show
  1. package/.turbo/turbo-build.log +72 -62
  2. package/CHANGELOG.md +28 -0
  3. package/dist/api/api.d.ts +3 -2
  4. package/dist/api/api.d.ts.map +1 -1
  5. package/dist/api/api.js +2 -1
  6. package/dist/api/api.js.map +1 -1
  7. package/dist/api/bind-services.d.ts +0 -1
  8. package/dist/api/bind-services.d.ts.map +1 -1
  9. package/dist/api/bind-services.js.map +1 -1
  10. package/dist/api/error.d.ts.map +1 -1
  11. package/dist/api/error.js.map +1 -1
  12. package/dist/api/fragment-definition-builder.d.ts +26 -44
  13. package/dist/api/fragment-definition-builder.d.ts.map +1 -1
  14. package/dist/api/fragment-definition-builder.js +15 -22
  15. package/dist/api/fragment-definition-builder.js.map +1 -1
  16. package/dist/api/fragment-instantiator.d.ts +51 -37
  17. package/dist/api/fragment-instantiator.d.ts.map +1 -1
  18. package/dist/api/fragment-instantiator.js +74 -69
  19. package/dist/api/fragment-instantiator.js.map +1 -1
  20. package/dist/api/request-context-storage.d.ts +4 -0
  21. package/dist/api/request-context-storage.d.ts.map +1 -1
  22. package/dist/api/request-context-storage.js +6 -0
  23. package/dist/api/request-context-storage.js.map +1 -1
  24. package/dist/api/request-input-context.d.ts.map +1 -1
  25. package/dist/api/request-input-context.js.map +1 -1
  26. package/dist/api/request-middleware.d.ts +1 -1
  27. package/dist/api/request-middleware.d.ts.map +1 -1
  28. package/dist/api/request-middleware.js.map +1 -1
  29. package/dist/api/request-output-context.d.ts +1 -1
  30. package/dist/api/request-output-context.d.ts.map +1 -1
  31. package/dist/api/request-output-context.js.map +1 -1
  32. package/dist/api/route-caller.d.ts +30 -0
  33. package/dist/api/route-caller.d.ts.map +1 -0
  34. package/dist/api/route-caller.js +63 -0
  35. package/dist/api/route-caller.js.map +1 -0
  36. package/dist/api/route-handler-input-options.d.ts.map +1 -1
  37. package/dist/api/route.d.ts +1 -1
  38. package/dist/api/route.d.ts.map +1 -1
  39. package/dist/api/route.js.map +1 -1
  40. package/dist/api/shared-types.d.ts.map +1 -1
  41. package/dist/client/client-error.d.ts.map +1 -1
  42. package/dist/client/client-error.js.map +1 -1
  43. package/dist/client/client.d.ts +91 -52
  44. package/dist/client/client.d.ts.map +1 -1
  45. package/dist/client/client.js +25 -9
  46. package/dist/client/client.js.map +1 -1
  47. package/dist/client/client.svelte.d.ts +6 -5
  48. package/dist/client/client.svelte.d.ts.map +1 -1
  49. package/dist/client/client.svelte.js +10 -2
  50. package/dist/client/client.svelte.js.map +1 -1
  51. package/dist/client/internal/ndjson-streaming.js.map +1 -1
  52. package/dist/client/react.d.ts +5 -4
  53. package/dist/client/react.d.ts.map +1 -1
  54. package/dist/client/react.js +104 -12
  55. package/dist/client/react.js.map +1 -1
  56. package/dist/client/solid.d.ts +7 -5
  57. package/dist/client/solid.d.ts.map +1 -1
  58. package/dist/client/solid.js +23 -9
  59. package/dist/client/solid.js.map +1 -1
  60. package/dist/client/vanilla.d.ts +16 -4
  61. package/dist/client/vanilla.d.ts.map +1 -1
  62. package/dist/client/vanilla.js +21 -1
  63. package/dist/client/vanilla.js.map +1 -1
  64. package/dist/client/vue.d.ts +7 -5
  65. package/dist/client/vue.d.ts.map +1 -1
  66. package/dist/client/vue.js +18 -10
  67. package/dist/client/vue.js.map +1 -1
  68. package/dist/id.d.ts +2 -0
  69. package/dist/id.js +3 -0
  70. package/dist/internal/cuid.d.ts +16 -0
  71. package/dist/internal/cuid.d.ts.map +1 -0
  72. package/dist/internal/cuid.js +82 -0
  73. package/dist/internal/cuid.js.map +1 -0
  74. package/dist/mod-client.d.ts +5 -4
  75. package/dist/mod-client.d.ts.map +1 -1
  76. package/dist/mod-client.js +7 -5
  77. package/dist/mod-client.js.map +1 -1
  78. package/dist/mod.d.ts +6 -5
  79. package/dist/mod.js +2 -1
  80. package/dist/runtime.js +1 -1
  81. package/dist/runtime.js.map +1 -1
  82. package/dist/test/test.d.ts +6 -6
  83. package/dist/test/test.d.ts.map +1 -1
  84. package/dist/test/test.js.map +1 -1
  85. package/dist/util/ssr.js.map +1 -1
  86. package/package.json +24 -40
  87. package/src/api/api.test.ts +3 -1
  88. package/src/api/api.ts +6 -0
  89. package/src/api/bind-services.ts +0 -5
  90. package/src/api/error.ts +1 -0
  91. package/src/api/fragment-definition-builder.extend.test.ts +2 -1
  92. package/src/api/fragment-definition-builder.test.ts +2 -1
  93. package/src/api/fragment-definition-builder.ts +49 -124
  94. package/src/api/fragment-instantiator.test.ts +92 -233
  95. package/src/api/fragment-instantiator.ts +228 -196
  96. package/src/api/fragment-services.test.ts +1 -0
  97. package/src/api/internal/path-runtime.test.ts +1 -0
  98. package/src/api/internal/path-type.test.ts +3 -1
  99. package/src/api/internal/route.test.ts +1 -0
  100. package/src/api/request-context-storage.ts +7 -0
  101. package/src/api/request-input-context.test.ts +4 -2
  102. package/src/api/request-input-context.ts +2 -1
  103. package/src/api/request-middleware.test.ts +9 -14
  104. package/src/api/request-middleware.ts +3 -2
  105. package/src/api/request-output-context.test.ts +3 -1
  106. package/src/api/request-output-context.ts +2 -1
  107. package/src/api/route-caller.test.ts +195 -0
  108. package/src/api/route-caller.ts +167 -0
  109. package/src/api/route-handler-input-options.ts +2 -1
  110. package/src/api/route.test.ts +4 -2
  111. package/src/api/route.ts +2 -1
  112. package/src/api/shared-types.ts +2 -1
  113. package/src/client/client-builder.test.ts +4 -2
  114. package/src/client/client-error.test.ts +2 -1
  115. package/src/client/client-error.ts +1 -1
  116. package/src/client/client-types.test.ts +19 -5
  117. package/src/client/client.ssr.test.ts +6 -4
  118. package/src/client/client.svelte.test.ts +18 -9
  119. package/src/client/client.svelte.ts +38 -13
  120. package/src/client/client.test.ts +49 -10
  121. package/src/client/client.ts +291 -141
  122. package/src/client/internal/ndjson-streaming.test.ts +6 -3
  123. package/src/client/internal/ndjson-streaming.ts +1 -0
  124. package/src/client/react.test.ts +176 -6
  125. package/src/client/react.ts +226 -31
  126. package/src/client/solid.test.ts +29 -5
  127. package/src/client/solid.ts +60 -22
  128. package/src/client/vanilla.test.ts +148 -6
  129. package/src/client/vanilla.ts +63 -9
  130. package/src/client/vue.test.ts +223 -84
  131. package/src/client/vue.ts +57 -30
  132. package/src/id.ts +1 -0
  133. package/src/internal/cuid.test.ts +164 -0
  134. package/src/internal/cuid.ts +133 -0
  135. package/src/mod-client.ts +4 -2
  136. package/src/mod.ts +3 -2
  137. package/src/runtime.ts +1 -1
  138. package/src/test/test.test.ts +4 -2
  139. package/src/test/test.ts +7 -9
  140. package/src/util/async.test.ts +1 -0
  141. package/src/util/content-type.test.ts +1 -0
  142. package/src/util/nanostores.test.ts +3 -1
  143. package/src/util/ssr.ts +1 -0
  144. package/tsconfig.json +1 -1
  145. package/tsdown.config.ts +1 -0
  146. package/vitest.config.ts +2 -1
package/src/client/vue.ts CHANGED
@@ -1,8 +1,13 @@
1
- import type { StandardSchemaV1 } from "@standard-schema/spec";
2
1
  import { atom, type ReadableAtom, type Store, type StoreValue } from "nanostores";
3
2
  import type { DeepReadonly, Ref, ShallowRef, UnwrapNestedRefs } from "vue";
4
3
  import { computed, getCurrentScope, isRef, onScopeDispose, ref, shallowRef, watch } from "vue";
4
+
5
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
6
+
5
7
  import type { NonGetHTTPMethod } from "../api/api";
8
+ import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
9
+ import { isReadableAtom } from "../util/nanostores";
10
+ import type { InferOr } from "../util/types-util";
6
11
  import {
7
12
  isGetHook,
8
13
  isMutatorHook,
@@ -10,11 +15,10 @@ import {
10
15
  type FragnoClientMutatorData,
11
16
  type FragnoClientHookData,
12
17
  type FragnoStoreData,
18
+ type FragnoStoreFactoryData,
19
+ type FragnoStoreObjectData,
13
20
  } from "./client";
14
- import { isReadableAtom } from "../util/nanostores";
15
21
  import type { FragnoClientError } from "./client-error";
16
- import type { MaybeExtractPathParamsOrWiden, QueryParamsHint } from "../api/internal/path";
17
- import type { InferOr } from "../util/types-util";
18
22
 
19
23
  export type FragnoVueHook<
20
24
  _TMethod extends "GET",
@@ -50,14 +54,20 @@ export type FragnoVueMutator<
50
54
  };
51
55
 
52
56
  /**
53
- * Type helper that unwraps any Store fields of the object into StoreValues
57
+ * Type helper that wraps any Store fields of the object into reactive Vue refs.
54
58
  */
55
- export type FragnoVueStore<T extends object> = () => T extends Store<infer TStore>
56
- ? StoreValue<TStore>
59
+ type FragnoVueStoreRef<T extends Store> = DeepReadonly<UnwrapNestedRefs<ShallowRef<StoreValue<T>>>>;
60
+
61
+ type FragnoVueStoreValue<T extends object> = T extends Store
62
+ ? FragnoVueStoreRef<T>
57
63
  : {
58
- [K in keyof T]: T[K] extends Store ? StoreValue<T[K]> : T[K];
64
+ [K in keyof T]: T[K] extends Store ? FragnoVueStoreRef<T[K]> : T[K];
59
65
  };
60
66
 
67
+ export type FragnoVueStore<T extends object, TArgs extends unknown[] = []> = (
68
+ ...args: TArgs
69
+ ) => FragnoVueStoreValue<T>;
70
+
61
71
  /**
62
72
  * Converts a Vue Ref to a NanoStore Atom.
63
73
  *
@@ -193,31 +203,46 @@ function createVueMutator<
193
203
  };
194
204
  }
195
205
 
196
- // Helper function to create a Vue composable from a store
197
- function createVueStore<const T extends object>(hook: FragnoStoreData<T>): FragnoVueStore<T> {
198
- if (isReadableAtom(hook.obj)) {
199
- return (() => useStore(hook.obj as Store).value) as FragnoVueStore<T>;
206
+ function unwrapVueStoreValue<T extends object>(value: T): FragnoVueStoreValue<T> {
207
+ if (isReadableAtom(value)) {
208
+ return useStore(value as Store) as FragnoVueStoreValue<T>;
200
209
  }
201
210
 
202
- return (() => {
203
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
204
- const result: any = {};
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
+ const result: any = {};
205
213
 
206
- for (const key in hook.obj) {
207
- if (!Object.prototype.hasOwnProperty.call(hook.obj, key)) {
208
- continue;
209
- }
214
+ for (const key in value) {
215
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
216
+ continue;
217
+ }
210
218
 
211
- const value = hook.obj[key];
212
- if (isReadableAtom(value)) {
213
- result[key] = useStore(value).value;
214
- } else {
215
- result[key] = value;
216
- }
219
+ const fieldValue = value[key];
220
+ if (isReadableAtom(fieldValue)) {
221
+ result[key] = useStore(fieldValue);
222
+ } else {
223
+ result[key] = fieldValue;
224
+ }
225
+ }
226
+
227
+ return result as FragnoVueStoreValue<T>;
228
+ }
229
+
230
+ // Helper function to create a Vue composable from a store
231
+ function createVueStore<const T extends object, const TArgs extends unknown[]>(
232
+ hook: FragnoStoreData<T, TArgs>,
233
+ ): FragnoVueStore<T, TArgs> {
234
+ return ((...args: TArgs) => {
235
+ const value = "factory" in hook ? hook.factory(...args) : hook.obj;
236
+ const disposer = value[Symbol.dispose as keyof typeof value];
237
+
238
+ if (typeof disposer === "function" && getCurrentScope()) {
239
+ onScopeDispose(() => {
240
+ disposer.call(value);
241
+ });
217
242
  }
218
243
 
219
- return result;
220
- }) as FragnoVueStore<T>;
244
+ return unwrapVueStoreValue(value);
245
+ }) as FragnoVueStore<T, TArgs>;
221
246
  }
222
247
 
223
248
  export function useFragno<T extends Record<string, unknown>>(
@@ -240,9 +265,11 @@ export function useFragno<T extends Record<string, unknown>>(
240
265
  infer TQueryParameters
241
266
  >
242
267
  ? FragnoVueMutator<M, TPath, TInputSchema, TOutputSchema, TErrorCode, TQueryParameters>
243
- : T[K] extends FragnoStoreData<infer TStoreObj>
244
- ? FragnoVueStore<TStoreObj>
245
- : T[K];
268
+ : T[K] extends FragnoStoreObjectData<infer TStoreObj>
269
+ ? FragnoVueStore<TStoreObj, []>
270
+ : T[K] extends FragnoStoreFactoryData<infer TStoreObj, infer TStoreArgs>
271
+ ? FragnoVueStore<TStoreObj, TStoreArgs>
272
+ : T[K];
246
273
  } {
247
274
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
248
275
  const result = {} as any;
package/src/id.ts ADDED
@@ -0,0 +1 @@
1
+ export { createId, init } from "./internal/cuid";
@@ -0,0 +1,164 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { createId, init } from "./cuid";
4
+
5
+ const idPattern = /^[a-z][0-9a-z]*$/;
6
+
7
+ const createSeededRandom = (seed: number) => {
8
+ let state = seed >>> 0;
9
+
10
+ return () => {
11
+ state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
12
+ return state / 4294967296;
13
+ };
14
+ };
15
+
16
+ const createCounter = (start = 0) => {
17
+ let value = start;
18
+ return () => value++;
19
+ };
20
+
21
+ describe("cuid", () => {
22
+ afterEach(() => {
23
+ vi.useRealTimers();
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ it("creates ids with the default format", () => {
28
+ const id = createId();
29
+
30
+ expect(id).toHaveLength(24);
31
+ expect(id).toMatch(idPattern);
32
+ });
33
+
34
+ it("defers default state initialization until the first generated id", () => {
35
+ vi.useFakeTimers();
36
+ vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
37
+
38
+ const random = vi.fn(() => 0.5);
39
+
40
+ const generator = init({ random });
41
+
42
+ expect(random).not.toHaveBeenCalled();
43
+
44
+ generator();
45
+
46
+ expect(random).toHaveBeenCalled();
47
+ });
48
+
49
+ it("uses web-standard crypto randomness by default", () => {
50
+ vi.useFakeTimers();
51
+ vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
52
+
53
+ const mathRandomSpy = vi.spyOn(Math, "random");
54
+ const getRandomValuesSpy = vi.spyOn(globalThis.crypto, "getRandomValues");
55
+
56
+ const generator = init();
57
+ const ids = Array.from({ length: 10 }, () => generator());
58
+
59
+ expect(ids[0]).toHaveLength(24);
60
+ expect(ids[0]).toMatch(idPattern);
61
+ expect(mathRandomSpy).not.toHaveBeenCalled();
62
+ expect(getRandomValuesSpy).toHaveBeenCalled();
63
+ });
64
+
65
+ it("supports exact custom lengths including very short ids", () => {
66
+ vi.useFakeTimers();
67
+ vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
68
+
69
+ for (const length of [1, 2, 8, 24, 32]) {
70
+ const id = init({
71
+ length,
72
+ random: createSeededRandom(123),
73
+ counter: createCounter(7),
74
+ fingerprint: "fingerprint",
75
+ })();
76
+
77
+ expect(id).toHaveLength(length);
78
+ expect(id).toMatch(idPattern);
79
+ }
80
+ });
81
+
82
+ it("creates deterministic sequences for the same configuration", () => {
83
+ vi.useFakeTimers();
84
+ vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
85
+
86
+ const first = init({
87
+ random: createSeededRandom(123),
88
+ counter: createCounter(10),
89
+ fingerprint: "fingerprint",
90
+ });
91
+ const second = init({
92
+ random: createSeededRandom(123),
93
+ counter: createCounter(10),
94
+ fingerprint: "fingerprint",
95
+ });
96
+
97
+ const firstIds = [first(), first(), first()];
98
+ const secondIds = [second(), second(), second()];
99
+
100
+ expect(firstIds).toEqual(secondIds);
101
+ expect(firstIds).toMatchInlineSnapshot(`
102
+ [
103
+ "h1bh5y4k2htp1bh5y4k2htp1",
104
+ "p1zxk77n8dqx1zxk77n8dqx1",
105
+ "u576un0v3is576un0v3is576",
106
+ ]
107
+ `);
108
+ });
109
+
110
+ it("changes the output when the fingerprint changes", () => {
111
+ vi.useFakeTimers();
112
+ vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
113
+
114
+ const withFirstFingerprint = init({
115
+ random: createSeededRandom(123),
116
+ counter: createCounter(10),
117
+ fingerprint: "fingerprint-a",
118
+ });
119
+ const withSecondFingerprint = init({
120
+ random: createSeededRandom(123),
121
+ counter: createCounter(10),
122
+ fingerprint: "fingerprint-b",
123
+ });
124
+
125
+ expect(withFirstFingerprint()).not.toBe(withSecondFingerprint());
126
+ });
127
+
128
+ it("remains collision-free across many ids even when time and random are fixed", () => {
129
+ vi.useFakeTimers();
130
+ vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
131
+
132
+ const generator = init({
133
+ random: () => 0,
134
+ counter: createCounter(0),
135
+ fingerprint: "fingerprint",
136
+ });
137
+
138
+ const ids = Array.from({ length: 1000 }, () => generator());
139
+
140
+ expect(new Set(ids).size).toBe(ids.length);
141
+ expect(ids.every((id) => id.length === 24 && idPattern.test(id))).toBe(true);
142
+ });
143
+
144
+ it("uses the configured random source for the leading character", () => {
145
+ vi.useFakeTimers();
146
+ vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
147
+
148
+ const leadingA = init({
149
+ length: 8,
150
+ random: () => 0,
151
+ counter: createCounter(0),
152
+ fingerprint: "fingerprint",
153
+ })();
154
+ const leadingZ = init({
155
+ length: 8,
156
+ random: () => 0.999999,
157
+ counter: createCounter(0),
158
+ fingerprint: "fingerprint",
159
+ })();
160
+
161
+ expect(leadingA[0]).toBe("a");
162
+ expect(leadingZ[0]).toBe("z");
163
+ });
164
+ });
@@ -0,0 +1,133 @@
1
+ const defaultLength = 24;
2
+ const bigLength = 32;
3
+ const initialCountMax = 476782367;
4
+ const randomPoolSize = 128;
5
+ const uint32ToFloatScale = 1 / 4294967296;
6
+
7
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
8
+ const entropyAlphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
9
+
10
+ const createCryptoRandom = (cryptoApi = globalThis.crypto) => {
11
+ if (!cryptoApi?.getRandomValues) {
12
+ throw new Error("Fragno cuid requires globalThis.crypto.getRandomValues()");
13
+ }
14
+
15
+ const values = new Uint32Array(randomPoolSize);
16
+ let offset = values.length;
17
+
18
+ return () => {
19
+ if (offset >= values.length) {
20
+ cryptoApi.getRandomValues(values);
21
+ offset = 0;
22
+ }
23
+
24
+ const value = values[offset]!;
25
+ offset += 1;
26
+ return value * uint32ToFloatScale;
27
+ };
28
+ };
29
+
30
+ const cryptoRandom = (() => {
31
+ let random: (() => number) | undefined;
32
+
33
+ return () => {
34
+ random ??= createCryptoRandom();
35
+ return random();
36
+ };
37
+ })();
38
+
39
+ const pickIndex = (random: () => number, length: number) => {
40
+ const value = random();
41
+
42
+ if (value <= 0) {
43
+ return 0;
44
+ }
45
+
46
+ if (value >= 1) {
47
+ return length - 1;
48
+ }
49
+
50
+ return Math.floor(value * length);
51
+ };
52
+
53
+ const createEntropy = (length = 4, random = cryptoRandom) => {
54
+ let entropy = "";
55
+
56
+ while (entropy.length < length) {
57
+ entropy += entropyAlphabet[pickIndex(random, entropyAlphabet.length)] ?? "0";
58
+ }
59
+
60
+ return entropy;
61
+ };
62
+
63
+ const hash53 = (input: string, seed = 0) => {
64
+ let h1 = 0xdeadbeef ^ seed;
65
+ let h2 = 0x41c6ce57 ^ seed;
66
+
67
+ for (let i = 0; i < input.length; i += 1) {
68
+ const code = input.charCodeAt(i);
69
+ h1 = Math.imul(h1 ^ code, 2654435761);
70
+ h2 = Math.imul(h2 ^ code, 1597334677);
71
+ }
72
+
73
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
74
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
75
+
76
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
77
+ };
78
+
79
+ const hash = (input: string, length = bigLength) => {
80
+ let output = "";
81
+ let seed = 0;
82
+
83
+ while (output.length < length) {
84
+ output += hash53(`${seed}:${input}`, seed).toString(36);
85
+ seed += 1;
86
+ }
87
+
88
+ return output.slice(0, length);
89
+ };
90
+
91
+ const createFingerprint = ({
92
+ random = cryptoRandom,
93
+ }: {
94
+ random?: () => number;
95
+ } = {}) => createEntropy(bigLength, random);
96
+
97
+ const createCounter = (count: number) => () => count++;
98
+
99
+ export const init = ({
100
+ random = cryptoRandom,
101
+ counter,
102
+ length = defaultLength,
103
+ fingerprint,
104
+ }: {
105
+ random?: () => number;
106
+ counter?: () => number;
107
+ length?: number;
108
+ fingerprint?: string;
109
+ } = {}) => {
110
+ const normalizedLength = Math.max(1, Math.floor(length));
111
+ let resolvedCounter = counter;
112
+ let resolvedFingerprint = fingerprint;
113
+
114
+ return () => {
115
+ resolvedCounter ??= createCounter(pickIndex(random, initialCountMax));
116
+ resolvedFingerprint ??= createFingerprint({ random });
117
+
118
+ const firstLetter = alphabet[pickIndex(random, alphabet.length)] ?? "a";
119
+
120
+ if (normalizedLength === 1) {
121
+ return firstLetter;
122
+ }
123
+
124
+ const time = Date.now().toString(36);
125
+ const count = resolvedCounter().toString(36);
126
+ const salt = createEntropy(normalizedLength, random);
127
+ const body = hash(`${time}:${salt}:${count}:${resolvedFingerprint}`, normalizedLength - 1);
128
+ return `${firstLetter}${body}`;
129
+ };
130
+ };
131
+
132
+ // Safe to keep at module scope: init() defers all randomness until the first ID request.
133
+ export const createId = init();
package/src/mod-client.ts CHANGED
@@ -45,11 +45,12 @@ export function defineFragment(_name: string) {
45
45
  withRequestStorage: () => stub,
46
46
  withExternalRequestStorage: () => stub,
47
47
  withThisContext: () => stub,
48
- withLinkedFragment: () => stub,
48
+ withInternalRoutes: () => stub,
49
49
  extend: () => stub,
50
50
  build: () => definitionStub,
51
51
  // From fragno-db
52
52
  provideHooks: () => stub,
53
+ withSyncCommands: () => stub,
53
54
  };
54
55
 
55
56
  // Wrap with Proxy to handle any additional methods (e.g. from extend())
@@ -82,11 +83,11 @@ export function instantiate(_definition: unknown) {
82
83
  return {
83
84
  deps: {},
84
85
  options: {},
85
- linkedFragments: {},
86
86
  };
87
87
  },
88
88
  withMiddleware: () => fragmentStub,
89
89
  inContext: <T>(callback: () => T) => callback(),
90
+ callServices: async (_serviceCalls: () => unknown) => [],
90
91
  handlersFor: () => ({}),
91
92
  handler: async () => new Response(),
92
93
  callRoute: async () => ({ ok: true, data: undefined, error: undefined }),
@@ -145,6 +146,7 @@ export type { FragnoPublicConfig } from "./api/shared-types";
145
146
  // Runtime
146
147
  // ============================================================================
147
148
  export { defaultFragnoRuntime, type FragnoRuntime } from "./runtime";
149
+ export { createId, init } from "./id";
148
150
 
149
151
  // ============================================================================
150
152
  // Route Definition
package/src/mod.ts CHANGED
@@ -7,8 +7,6 @@ export {
7
7
  type FragmentDefinition,
8
8
  type ServiceContext,
9
9
  type ServiceConstructorFn,
10
- type LinkedFragmentCallback,
11
- type ExtractLinkedServices,
12
10
  } from "./api/fragment-definition-builder";
13
11
 
14
12
  export {
@@ -18,6 +16,7 @@ export {
18
16
  type AnyFragnoInstantiatedFragment,
19
17
  type BoundServices,
20
18
  type InstantiatedFragmentFromDefinition,
19
+ type FragnoRequestLifecycleContext,
21
20
  } from "./api/fragment-instantiator";
22
21
 
23
22
  // ============================================================================
@@ -29,6 +28,7 @@ export type { FragnoPublicConfig } from "./api/shared-types";
29
28
  // Runtime
30
29
  // ============================================================================
31
30
  export { defaultFragnoRuntime, type FragnoRuntime } from "./runtime";
31
+ export { createId, init } from "./id";
32
32
 
33
33
  // ============================================================================
34
34
  // Route Definition
@@ -36,6 +36,7 @@ export { defaultFragnoRuntime, type FragnoRuntime } from "./runtime";
36
36
  export {
37
37
  defineRoute,
38
38
  defineRoutes,
39
+ type AnyRouteOrFactory,
39
40
  type RouteFactory,
40
41
  type RouteFactoryContext,
41
42
  } from "./api/route";
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createId } from "@paralleldrive/cuid2";
1
+ import { createId } from "./id";
2
2
 
3
3
  export type FragnoRuntime = {
4
4
  time: {
@@ -1,8 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { createFragmentForTest, withTestUtils } from "./test";
2
+
3
+ import { z } from "zod";
4
+
3
5
  import { defineFragment } from "../api/fragment-definition-builder";
4
6
  import { defineRoutes } from "../api/route";
5
- import { z } from "zod";
7
+ import { createFragmentForTest, withTestUtils } from "./test";
6
8
 
7
9
  describe("withTestUtils extension", () => {
8
10
  it("should expose deps via services.deps", () => {
package/src/test/test.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { RequestThisContext } from "../api/api";
2
- import type { AnyRouteOrFactory, FlattenRouteFactories } from "../api/route";
3
- import type { FragnoPublicConfig } from "../api/shared-types";
2
+ import type { BoundServices } from "../api/bind-services";
4
3
  import {
5
4
  FragmentDefinitionBuilder,
6
5
  type FragmentDefinition,
@@ -8,11 +7,11 @@ import {
8
7
  } from "../api/fragment-definition-builder";
9
8
  import {
10
9
  instantiateFragment,
11
- type AnyFragnoInstantiatedFragment,
12
10
  type FragnoInstantiatedFragment,
13
11
  type RoutesWithInternal,
14
12
  } from "../api/fragment-instantiator";
15
- import type { BoundServices } from "../api/bind-services";
13
+ import type { AnyRouteOrFactory, FlattenRouteFactories } from "../api/route";
14
+ import type { FragnoPublicConfig } from "../api/shared-types";
16
15
 
17
16
  // Re-export for convenience
18
17
  export type { RouteHandlerInputOptions } from "../api/route-handler-input-options";
@@ -205,7 +204,7 @@ export function createFragmentForTest<
205
204
  THandlerThisContext extends RequestThisContext,
206
205
  TRequestStorage,
207
206
  const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
208
- TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment> = {},
207
+ TInternalRoutes extends readonly AnyRouteOrFactory[] = readonly [],
209
208
  >(
210
209
  definition: FragmentDefinition<
211
210
  TConfig,
@@ -218,19 +217,18 @@ export function createFragmentForTest<
218
217
  TServiceThisContext,
219
218
  THandlerThisContext,
220
219
  TRequestStorage,
221
- TLinkedFragments
220
+ TInternalRoutes
222
221
  >,
223
222
  routesOrFactories: TRoutesOrFactories,
224
223
  options: CreateFragmentForTestOptions<TConfig, TOptions, TServiceDependencies>,
225
224
  ): FragnoInstantiatedFragment<
226
- RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TLinkedFragments>,
225
+ RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TInternalRoutes>,
227
226
  TDeps,
228
227
  BoundServices<TBaseServices & TServices>,
229
228
  TServiceThisContext,
230
229
  THandlerThisContext,
231
230
  TRequestStorage,
232
- TOptions,
233
- TLinkedFragments
231
+ TOptions
234
232
  > {
235
233
  const { config, options: fragmentOptions = {} as TOptions, serviceImplementations } = options;
236
234
 
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "vitest";
2
+
2
3
  import { createAsyncIteratorFromCallback, waitForAsyncIterator } from "./async";
3
4
 
4
5
  describe("createAsyncIteratorFromCallback", () => {
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
+
2
3
  import { parseContentType } from "./content-type";
3
4
 
4
5
  describe("parseContentType", () => {
@@ -1,7 +1,9 @@
1
1
  import { describe, test, expect } from "vitest";
2
- import { isReadableAtom } from "./nanostores";
2
+
3
3
  import { atom, map } from "nanostores";
4
4
 
5
+ import { isReadableAtom } from "./nanostores";
6
+
5
7
  describe("nanostores", () => {
6
8
  test("isReadableAtom should return true for a readable atom", () => {
7
9
  const store = atom(0);
package/src/util/ssr.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { allTasks } from "nanostores";
2
+
2
3
  import type { FetcherStore } from "@nanostores/query";
3
4
 
4
5
  let stores: FetcherStore[] = [];
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "@fragno-private/typescript-config/tsconfig.base.json",
2
+ "extends": "@fragno-private/typescript-config/tsconfig.node.json",
3
3
  "compilerOptions": {
4
4
  "outDir": "./dist",
5
5
  "rootDir": ".",
package/tsdown.config.ts CHANGED
@@ -12,6 +12,7 @@ export default defineConfig({
12
12
  "./src/api/request-context-storage.ts",
13
13
  "./src/request/request.ts",
14
14
  "./src/client/client.ts",
15
+ "./src/id.ts",
15
16
  "./src/client/vanilla.ts",
16
17
  "./src/client/client.svelte.ts",
17
18
  "./src/client/react.ts",
package/vitest.config.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { svelteTesting } from "@testing-library/svelte/vite";
1
2
  import { defineConfig, mergeConfig } from "vitest/config";
3
+
2
4
  import { baseConfig } from "@fragno-private/vitest-config";
3
5
  import { svelte } from "@sveltejs/vite-plugin-svelte";
4
- import { svelteTesting } from "@testing-library/svelte/vite";
5
6
 
6
7
  export default defineConfig(
7
8
  mergeConfig(baseConfig, {