@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.
- package/.turbo/turbo-build.log +72 -62
- package/CHANGELOG.md +28 -0
- package/dist/api/api.d.ts +3 -2
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +2 -1
- package/dist/api/api.js.map +1 -1
- package/dist/api/bind-services.d.ts +0 -1
- package/dist/api/bind-services.d.ts.map +1 -1
- package/dist/api/bind-services.js.map +1 -1
- package/dist/api/error.d.ts.map +1 -1
- package/dist/api/error.js.map +1 -1
- package/dist/api/fragment-definition-builder.d.ts +26 -44
- package/dist/api/fragment-definition-builder.d.ts.map +1 -1
- package/dist/api/fragment-definition-builder.js +15 -22
- package/dist/api/fragment-definition-builder.js.map +1 -1
- package/dist/api/fragment-instantiator.d.ts +51 -37
- package/dist/api/fragment-instantiator.d.ts.map +1 -1
- package/dist/api/fragment-instantiator.js +74 -69
- package/dist/api/fragment-instantiator.js.map +1 -1
- package/dist/api/request-context-storage.d.ts +4 -0
- package/dist/api/request-context-storage.d.ts.map +1 -1
- package/dist/api/request-context-storage.js +6 -0
- package/dist/api/request-context-storage.js.map +1 -1
- package/dist/api/request-input-context.d.ts.map +1 -1
- package/dist/api/request-input-context.js.map +1 -1
- package/dist/api/request-middleware.d.ts +1 -1
- package/dist/api/request-middleware.d.ts.map +1 -1
- package/dist/api/request-middleware.js.map +1 -1
- package/dist/api/request-output-context.d.ts +1 -1
- package/dist/api/request-output-context.d.ts.map +1 -1
- package/dist/api/request-output-context.js.map +1 -1
- package/dist/api/route-caller.d.ts +30 -0
- package/dist/api/route-caller.d.ts.map +1 -0
- package/dist/api/route-caller.js +63 -0
- package/dist/api/route-caller.js.map +1 -0
- package/dist/api/route-handler-input-options.d.ts.map +1 -1
- package/dist/api/route.d.ts +1 -1
- package/dist/api/route.d.ts.map +1 -1
- package/dist/api/route.js.map +1 -1
- package/dist/api/shared-types.d.ts.map +1 -1
- package/dist/client/client-error.d.ts.map +1 -1
- package/dist/client/client-error.js.map +1 -1
- package/dist/client/client.d.ts +91 -52
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +25 -9
- package/dist/client/client.js.map +1 -1
- package/dist/client/client.svelte.d.ts +6 -5
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +10 -2
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/internal/ndjson-streaming.js.map +1 -1
- package/dist/client/react.d.ts +5 -4
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +104 -12
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +7 -5
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +23 -9
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +16 -4
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +21 -1
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +7 -5
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +18 -10
- package/dist/client/vue.js.map +1 -1
- package/dist/id.d.ts +2 -0
- package/dist/id.js +3 -0
- package/dist/internal/cuid.d.ts +16 -0
- package/dist/internal/cuid.d.ts.map +1 -0
- package/dist/internal/cuid.js +82 -0
- package/dist/internal/cuid.js.map +1 -0
- package/dist/mod-client.d.ts +5 -4
- package/dist/mod-client.d.ts.map +1 -1
- package/dist/mod-client.js +7 -5
- package/dist/mod-client.js.map +1 -1
- package/dist/mod.d.ts +6 -5
- package/dist/mod.js +2 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/test/test.d.ts +6 -6
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js.map +1 -1
- package/dist/util/ssr.js.map +1 -1
- package/package.json +24 -40
- package/src/api/api.test.ts +3 -1
- package/src/api/api.ts +6 -0
- package/src/api/bind-services.ts +0 -5
- package/src/api/error.ts +1 -0
- package/src/api/fragment-definition-builder.extend.test.ts +2 -1
- package/src/api/fragment-definition-builder.test.ts +2 -1
- package/src/api/fragment-definition-builder.ts +49 -124
- package/src/api/fragment-instantiator.test.ts +92 -233
- package/src/api/fragment-instantiator.ts +228 -196
- package/src/api/fragment-services.test.ts +1 -0
- package/src/api/internal/path-runtime.test.ts +1 -0
- package/src/api/internal/path-type.test.ts +3 -1
- package/src/api/internal/route.test.ts +1 -0
- package/src/api/request-context-storage.ts +7 -0
- package/src/api/request-input-context.test.ts +4 -2
- package/src/api/request-input-context.ts +2 -1
- package/src/api/request-middleware.test.ts +9 -14
- package/src/api/request-middleware.ts +3 -2
- package/src/api/request-output-context.test.ts +3 -1
- package/src/api/request-output-context.ts +2 -1
- package/src/api/route-caller.test.ts +195 -0
- package/src/api/route-caller.ts +167 -0
- package/src/api/route-handler-input-options.ts +2 -1
- package/src/api/route.test.ts +4 -2
- package/src/api/route.ts +2 -1
- package/src/api/shared-types.ts +2 -1
- package/src/client/client-builder.test.ts +4 -2
- package/src/client/client-error.test.ts +2 -1
- package/src/client/client-error.ts +1 -1
- package/src/client/client-types.test.ts +19 -5
- package/src/client/client.ssr.test.ts +6 -4
- package/src/client/client.svelte.test.ts +18 -9
- package/src/client/client.svelte.ts +38 -13
- package/src/client/client.test.ts +49 -10
- package/src/client/client.ts +291 -141
- package/src/client/internal/ndjson-streaming.test.ts +6 -3
- package/src/client/internal/ndjson-streaming.ts +1 -0
- package/src/client/react.test.ts +176 -6
- package/src/client/react.ts +226 -31
- package/src/client/solid.test.ts +29 -5
- package/src/client/solid.ts +60 -22
- package/src/client/vanilla.test.ts +148 -6
- package/src/client/vanilla.ts +63 -9
- package/src/client/vue.test.ts +223 -84
- package/src/client/vue.ts +57 -30
- package/src/id.ts +1 -0
- package/src/internal/cuid.test.ts +164 -0
- package/src/internal/cuid.ts +133 -0
- package/src/mod-client.ts +4 -2
- package/src/mod.ts +3 -2
- package/src/runtime.ts +1 -1
- package/src/test/test.test.ts +4 -2
- package/src/test/test.ts +7 -9
- package/src/util/async.test.ts +1 -0
- package/src/util/content-type.test.ts +1 -0
- package/src/util/nanostores.test.ts +3 -1
- package/src/util/ssr.ts +1 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +1 -0
- 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
|
|
57
|
+
* Type helper that wraps any Store fields of the object into reactive Vue refs.
|
|
54
58
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
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 ?
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
const result: any = {};
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
+
const result: any = {};
|
|
205
213
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
for (const key in value) {
|
|
215
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
210
218
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
package/src/test/test.test.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
220
|
+
TInternalRoutes
|
|
222
221
|
>,
|
|
223
222
|
routesOrFactories: TRoutesOrFactories,
|
|
224
223
|
options: CreateFragmentForTestOptions<TConfig, TOptions, TServiceDependencies>,
|
|
225
224
|
): FragnoInstantiatedFragment<
|
|
226
|
-
RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>,
|
|
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
|
|
package/src/util/async.test.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
|
|
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
package/tsconfig.json
CHANGED
package/tsdown.config.ts
CHANGED
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, {
|