@vizejs/data 0.0.0-stage → 0.713.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.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # @vizejs/data
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed SSR data resource foundations for Vize applications.
4
+
5
+ > This package is released on npm as `@vizejs/data`; its resource and adapter contracts remain
6
+ > experimental throughout the Vize alpha line.
7
+
8
+ ```vue
9
+ <script setup lang="ts">
10
+ import { createDataClient, defineDataResource } from "@vizejs/data";
11
+
12
+ const articleResource = defineDataResource({
13
+ key: "article.byId",
14
+ source: "./ArticleView.vue",
15
+ async loader({ input }: { input: { id: string } }) {
16
+ return { id: input.id, title: "Hello Vize" };
17
+ },
18
+ });
19
+
20
+ const data = createDataClient();
21
+ const article = await data.load(
22
+ articleResource,
23
+ { id: "intro" },
24
+ {
25
+ retries: 2,
26
+ retryDelayMs: ({ attempt }) => attempt * 100,
27
+ deadlineMs: 1500,
28
+ },
29
+ );
30
+ </script>
31
+
32
+ <template>
33
+ <article v-if="article.status === 'success'">
34
+ {{ article.data.title }}
35
+ </article>
36
+ </template>
37
+ ```
38
+
39
+ Server renderers can call `serializeDataSnapshot(data)` and hydrate the client with
40
+ `hydrateDataClient(snapshot)`. Hydrated success entries satisfy the first client load without
41
+ issuing duplicate network work; `policy: "replace"` opts back into a fresh request.
42
+
43
+ ## Cache, races, and optimistic work
44
+
45
+ The client supports deduplication, cache-first reads, explicit revalidation, replacement, and
46
+ stale-while-revalidate. Each request key has latest-result custody: an older response may resolve to
47
+ its caller, but cannot overwrite a newer cache entry. `replace` aborts the previous request, while
48
+ `cancel` exposes route- or scope-owned cancellation. Optimistic updates return a guarded rollback;
49
+ the rollback refuses to overwrite data that a later request has already committed.
50
+
51
+ ```ts
52
+ const rollback = data.optimistic(articleResource, { id: "intro" }, (article) => ({
53
+ ...article,
54
+ title: "Draft title",
55
+ }));
56
+
57
+ const stale = await data.load(
58
+ articleResource,
59
+ { id: "intro" },
60
+ {
61
+ policy: "stale-while-revalidate",
62
+ },
63
+ );
64
+ ```
65
+
66
+ ## Runtime adapters
67
+
68
+ `createFetchDataResource` adapts typed HTTP endpoints, `createStreamDataResource` reduces an
69
+ abort-aware async iterable, and `hydrateDataClientFromStream` restores incremental SSR snapshots.
70
+ None of them reads browser globals during module evaluation. `createDataTestAdapter` supplies a
71
+ deterministic response queue and records every load context for race, retry, and cancellation tests.
72
+ Resources can return an explicit unsupported reason from `supports` before their loader runs.
@@ -0,0 +1,296 @@
1
+ //#region src/types.d.ts
2
+ /** Source-owned Vue file that documents where a resource is used. */
3
+ type DataSourceSpecifier = `${string}.vue`;
4
+ /** Serializable metadata that generators and devtools can consume. */
5
+ type DataResourceMeta = Readonly<Record<string, unknown>>;
6
+ /** Stable resource key. Prefer domain-qualified literals such as `article.byId`. */
7
+ type DataResourceKey = string;
8
+ /** Reason a fetch was started or refreshed. */
9
+ type DataLoadReason = "initial" | "hydrate" | "refresh" | "retry" | "optimistic";
10
+ /** Context passed to every resource loader. */
11
+ interface DataLoadContext<Input = unknown> {
12
+ readonly key: DataResourceKey;
13
+ readonly input: Input;
14
+ readonly meta: DataResourceMeta;
15
+ readonly reason: DataLoadReason;
16
+ readonly signal: AbortSignal;
17
+ }
18
+ /** Loader invoked by server renderers, client hydration, or manual refreshes. */
19
+ type DataLoader<Input, Data> = {
20
+ bivarianceHack(context: DataLoadContext<Input>): Data | Promise<Data>;
21
+ }["bivarianceHack"];
22
+ /** Host capability check for a resource input. Return a reason to explain unsupported hosts. */
23
+ type DataSupport<Input> = {
24
+ bivarianceHack(input: Input): boolean | string;
25
+ }["bivarianceHack"];
26
+ /** Typed data endpoint accepted by createDataClient. */
27
+ interface DataResourceDefinition<Key extends DataResourceKey = DataResourceKey, Input = unknown, Data = unknown, ErrorPayload = unknown, Meta extends DataResourceMeta = DataResourceMeta> {
28
+ readonly key: Key;
29
+ readonly source: DataSourceSpecifier;
30
+ readonly meta?: Meta;
31
+ readonly loader: DataLoader<Input, Data>;
32
+ readonly serializeError?: (error: unknown) => ErrorPayload;
33
+ /** Return false or a reason when the active host cannot load this resource. */
34
+ readonly supports?: DataSupport<Input>;
35
+ }
36
+ /** Broad resource definition used by registries, clients, and generated manifests. */
37
+ type AnyDataResourceDefinition = DataResourceDefinition<DataResourceKey, unknown, unknown, unknown, DataResourceMeta>;
38
+ /** Resolve the successful payload type declared by a resource. */
39
+ type InferData<Resource> = Resource extends DataResourceDefinition<DataResourceKey, infer _Input, infer Data> ? Data : never;
40
+ /** Resolve the serialized error payload type declared by a resource. */
41
+ type InferError<Resource> = Resource extends DataResourceDefinition<DataResourceKey, infer _Input, infer _Data, infer ErrorPayload> ? ErrorPayload : never;
42
+ /** Resolve the loader input type declared by a resource. */
43
+ type InferInput<Resource> = Resource extends DataResourceDefinition<infer _Key, infer Input> ? Input : never;
44
+ /** Resolve a resource's literal key, falling back to the public key type. */
45
+ type InferKey<Resource> = Resource extends {
46
+ readonly key: infer Key extends DataResourceKey;
47
+ } ? Key : DataResourceKey;
48
+ /** Complete lifecycle state for one keyed resource request. */
49
+ type DataState<Key extends DataResourceKey = DataResourceKey, Input = unknown, Data = unknown, ErrorPayload = unknown> = DataIdleState<Key, Input> | DataPendingState<Key, Input> | DataSuccessState<Key, Input, Data> | DataStaleState<Key, Input, Data> | DataCancelledState<Key, Input> | DataUnsupportedState<Key, Input> | DataErrorState<Key, Input, ErrorPayload>;
50
+ /** Identity shared by every resource lifecycle state. */
51
+ interface DataStateBase<Key extends DataResourceKey, Input> {
52
+ readonly key: Key;
53
+ readonly input: Input;
54
+ readonly requestKey: string;
55
+ }
56
+ /** Resource request that has not started. */
57
+ interface DataIdleState<Key extends DataResourceKey, Input> extends DataStateBase<Key, Input> {
58
+ readonly status: "idle";
59
+ }
60
+ /** Resource request with an active loader attempt. */
61
+ interface DataPendingState<Key extends DataResourceKey, Input> extends DataStateBase<Key, Input> {
62
+ readonly status: "pending";
63
+ readonly attempt: number;
64
+ readonly startedAt: number;
65
+ }
66
+ /** Resource request whose current data completed successfully. */
67
+ interface DataSuccessState<Key extends DataResourceKey, Input, Data> extends DataStateBase<Key, Input> {
68
+ readonly status: "success";
69
+ readonly data: Data;
70
+ readonly updatedAt: number;
71
+ }
72
+ /** Previously successful data marked for refresh or optimistic reconciliation. */
73
+ interface DataStaleState<Key extends DataResourceKey, Input, Data> extends DataStateBase<Key, Input> {
74
+ readonly status: "stale";
75
+ readonly data: Data;
76
+ readonly staleAt: number;
77
+ readonly updatedAt: number;
78
+ readonly reason?: string;
79
+ }
80
+ /** Resource request cancelled before it could commit data. */
81
+ interface DataCancelledState<Key extends DataResourceKey, Input> extends DataStateBase<Key, Input> {
82
+ readonly status: "cancelled";
83
+ readonly cancelledAt: number;
84
+ readonly reason: string;
85
+ }
86
+ /** Resource request rejected by its host capability check. */
87
+ interface DataUnsupportedState<Key extends DataResourceKey, Input> extends DataStateBase<Key, Input> {
88
+ readonly status: "unsupported";
89
+ readonly reason: string;
90
+ }
91
+ /** Resource request that exhausted its loader and retry policy. */
92
+ interface DataErrorState<Key extends DataResourceKey, Input, ErrorPayload> extends DataStateBase<Key, Input> {
93
+ readonly status: "error";
94
+ readonly error: ErrorPayload;
95
+ readonly updatedAt: number;
96
+ }
97
+ /** Context used to compute deterministic retry backoff. */
98
+ interface DataRetryDelayContext<Key extends DataResourceKey = DataResourceKey, Input = unknown> {
99
+ readonly key: Key;
100
+ readonly input: Input;
101
+ readonly attempt: number;
102
+ readonly error: unknown;
103
+ }
104
+ /** Retry backoff in milliseconds or a deterministic backoff factory. */
105
+ type DataRetryDelay<Key extends DataResourceKey = DataResourceKey, Input = unknown> = number | ((context: DataRetryDelayContext<Key, Input>) => number);
106
+ /** Testable sleep hook used by retry backoff. */
107
+ type DataSleep = (milliseconds: number, signal: AbortSignal) => Promise<void>;
108
+ /** Per-request cache, cancellation, and retry policy. */
109
+ interface DataLoadOptions<Key extends DataResourceKey = DataResourceKey, Input = unknown> {
110
+ /**
111
+ * Reason exposed to the first loader attempt.
112
+ *
113
+ * @default "initial"
114
+ */
115
+ readonly reason?: DataLoadReason;
116
+ /**
117
+ * External abort signal linked to the request-owned controller.
118
+ *
119
+ * @default undefined
120
+ */
121
+ readonly signal?: AbortSignal;
122
+ /**
123
+ * Cache and in-flight request policy.
124
+ *
125
+ * @default "dedupe"
126
+ */
127
+ readonly policy?: "dedupe" | "cache-first" | "revalidate" | "replace" | "stale-while-revalidate";
128
+ /**
129
+ * Additional attempts after the first loader call.
130
+ *
131
+ * @default 0
132
+ */
133
+ readonly retries?: number;
134
+ /**
135
+ * Backoff before retry attempts.
136
+ *
137
+ * @default 0
138
+ */
139
+ readonly retryDelayMs?: DataRetryDelay<Key, Input>;
140
+ /**
141
+ * Abort the load after this many milliseconds.
142
+ *
143
+ * @default undefined
144
+ */
145
+ readonly deadlineMs?: number;
146
+ }
147
+ /** Cache-owning resource client used by SSR and hydrated applications. */
148
+ interface DataClient {
149
+ readonly load: <Resource extends AnyDataResourceDefinition>(resource: Resource, input: InferInput<Resource>, options?: DataLoadOptions<InferKey<Resource>, InferInput<Resource>>) => Promise<DataState<InferKey<Resource>, InferInput<Resource>, InferData<Resource>, InferError<Resource>>>;
150
+ readonly peek: <Resource extends AnyDataResourceDefinition>(resource: Resource, input: InferInput<Resource>) => DataState<InferKey<Resource>, InferInput<Resource>, InferData<Resource>, InferError<Resource>> | undefined;
151
+ readonly invalidate: <Resource extends AnyDataResourceDefinition>(resource: Resource, input?: InferInput<Resource>, reason?: string) => number;
152
+ readonly optimistic: <Resource extends AnyDataResourceDefinition>(resource: Resource, input: InferInput<Resource>, update: (current: InferData<Resource>) => InferData<Resource>) => () => boolean;
153
+ readonly cancel: <Resource extends AnyDataResourceDefinition>(resource: Resource, input: InferInput<Resource>, reason?: unknown) => boolean;
154
+ readonly subscribe: (listener: (state: DataState) => void) => () => void;
155
+ readonly snapshot: () => DataSnapshot;
156
+ }
157
+ /** Deterministic hooks and initial cache state for a data client. */
158
+ interface DataClientOptions {
159
+ /**
160
+ * Clock used for lifecycle timestamps.
161
+ *
162
+ * @default Date.now
163
+ */
164
+ readonly now?: () => number;
165
+ /**
166
+ * Server snapshot used to seed the cache.
167
+ *
168
+ * @default undefined
169
+ */
170
+ readonly snapshot?: DataSnapshot;
171
+ /**
172
+ * Abort-aware wait implementation used by retry backoff.
173
+ *
174
+ * @default built-in setTimeout-based sleep
175
+ */
176
+ readonly sleep?: DataSleep;
177
+ }
178
+ /** Versioned collection of serializable resource states for hydration. */
179
+ interface DataSnapshot {
180
+ readonly schemaVersion: 1;
181
+ readonly resources: readonly SerializedDataState[];
182
+ }
183
+ /** Resource states that may cross the server-to-client boundary. */
184
+ type SerializedDataState = DataSuccessState<DataResourceKey, unknown, unknown> | DataStaleState<DataResourceKey, unknown, unknown> | DataCancelledState<DataResourceKey, unknown> | DataUnsupportedState<DataResourceKey, unknown> | DataErrorState<DataResourceKey, unknown, unknown>;
185
+ /** Versioned resource metadata emitted for tooling and documentation. */
186
+ interface DataManifest {
187
+ readonly schemaVersion: 1;
188
+ readonly resources: readonly DataManifestEntry[];
189
+ }
190
+ /** One resource's stable key, source owner, and serializable metadata. */
191
+ interface DataManifestEntry {
192
+ readonly key: DataResourceKey;
193
+ readonly source: DataSourceSpecifier;
194
+ readonly meta: DataResourceMeta;
195
+ }
196
+ //#endregion
197
+ //#region src/runtime.d.ts
198
+ /** Define a typed SSR data resource and preserve literal endpoint metadata. */
199
+ declare function defineDataResource<const Key extends DataResourceKey, Input, Data, ErrorPayload = unknown, const Meta extends DataResourceMeta = DataResourceMeta>(resource: DataResourceDefinition<Key, Input, Data, ErrorPayload, Meta>): DataResourceDefinition<Key, Input, Data, ErrorPayload, Meta>;
200
+ /** Create an SSR-safe data client with request deduplication and hydration cache. */
201
+ declare function createDataClient(options?: DataClientOptions): DataClient;
202
+ /** Serialize the current cache into an SSR payload. */
203
+ declare function serializeDataSnapshot(client: Pick<DataClient, "snapshot">): DataSnapshot;
204
+ /** Hydrate a data client from a server-rendered payload. */
205
+ declare function hydrateDataClient(snapshot: DataSnapshot, options?: Omit<DataClientOptions, "snapshot">): DataClient;
206
+ /** Emit metadata for generators, documentation, and devtools. */
207
+ declare function createDataManifest<const Resources extends readonly AnyDataResourceDefinition[]>(resources: Resources): DataManifest;
208
+ /** Stream a snapshot as deterministic records for render pipelines. */
209
+ declare function streamDataSnapshot(snapshot: DataSnapshot): AsyncGenerator<SerializedDataState, void, void>;
210
+ //#endregion
211
+ //#region src/adapters.d.ts
212
+ /** HTTP loader failure with the response status and decoded error body. */
213
+ declare class DataHttpError extends Error {
214
+ readonly status: number;
215
+ readonly statusText: string;
216
+ readonly body: unknown;
217
+ constructor(response: Response, body: unknown);
218
+ }
219
+ /** Typed HTTP endpoint and decoding policy for a fetch-backed resource. */
220
+ interface FetchDataResourceOptions<Key extends DataResourceKey, Input, Data, ErrorPayload = unknown, Meta extends DataResourceMeta = DataResourceMeta> {
221
+ readonly key: Key;
222
+ readonly source: DataSourceSpecifier;
223
+ readonly endpoint: (input: Input) => string | URL;
224
+ /**
225
+ * Request initialization derived from the typed input.
226
+ *
227
+ * @default undefined
228
+ */
229
+ readonly init?: (input: Input) => RequestInit;
230
+ /**
231
+ * Fetch implementation used by the loader.
232
+ *
233
+ * @default globalThis.fetch
234
+ */
235
+ readonly fetch?: typeof globalThis.fetch;
236
+ /**
237
+ * Successful response decoder.
238
+ *
239
+ * @default response.json()
240
+ */
241
+ readonly decode?: (response: Response) => Data | Promise<Data>;
242
+ /**
243
+ * Failed response body decoder.
244
+ *
245
+ * @default response.text(), falling back to undefined
246
+ */
247
+ readonly decodeError?: (response: Response) => unknown;
248
+ /**
249
+ * Error payload serializer forwarded to the resource definition.
250
+ *
251
+ * @default undefined
252
+ */
253
+ readonly serializeError?: (error: unknown) => ErrorPayload;
254
+ /**
255
+ * Serializable metadata forwarded to manifests and loaders.
256
+ *
257
+ * @default {}
258
+ */
259
+ readonly meta?: Meta;
260
+ }
261
+ /** Adapt a typed HTTP endpoint without reading `fetch` during module evaluation. */
262
+ declare function createFetchDataResource<const Key extends DataResourceKey, Input, Data, ErrorPayload = unknown, const Meta extends DataResourceMeta = DataResourceMeta>(options: FetchDataResourceOptions<Key, Input, Data, ErrorPayload, Meta>): DataResourceDefinition<Key, Input, Data, ErrorPayload, Meta>;
263
+ /** Typed async stream and reduction policy for a stream-backed resource. */
264
+ interface StreamDataResourceOptions<Key extends DataResourceKey, Input, Chunk, Data, ErrorPayload = unknown, Meta extends DataResourceMeta = DataResourceMeta> {
265
+ readonly key: Key;
266
+ readonly source: DataSourceSpecifier;
267
+ readonly initial: (input: Input) => Data;
268
+ readonly open: (context: DataLoadContext<Input>) => AsyncIterable<Chunk>;
269
+ readonly reduce: (data: Data, chunk: Chunk) => Data;
270
+ /**
271
+ * Error payload serializer forwarded to the resource definition.
272
+ *
273
+ * @default undefined
274
+ */
275
+ readonly serializeError?: (error: unknown) => ErrorPayload;
276
+ /**
277
+ * Serializable metadata forwarded to manifests and loaders.
278
+ *
279
+ * @default {}
280
+ */
281
+ readonly meta?: Meta;
282
+ }
283
+ /** Collect an abort-aware async stream into one typed resource state. */
284
+ declare function createStreamDataResource<const Key extends DataResourceKey, Input, Chunk, Data, ErrorPayload = unknown, const Meta extends DataResourceMeta = DataResourceMeta>(options: StreamDataResourceOptions<Key, Input, Chunk, Data, ErrorPayload, Meta>): DataResourceDefinition<Key, Input, Data, ErrorPayload, Meta>;
285
+ /** Reconstruct an SSR cache from an incremental snapshot stream. */
286
+ declare function hydrateDataClientFromStream(stream: AsyncIterable<SerializedDataState>, options?: Parameters<typeof hydrateDataClient>[1]): Promise<DataClient>;
287
+ /** Queue-backed loader and call history exposed to deterministic tests. */
288
+ interface DataTestAdapter<Input, Data> {
289
+ readonly calls: readonly DataLoadContext<Input>[];
290
+ readonly loader: (context: DataLoadContext<Input>) => Promise<Data>;
291
+ readonly enqueue: (result: Data | Error | ((context: DataLoadContext<Input>) => Data | Promise<Data>)) => void;
292
+ }
293
+ /** Deterministic queue-backed loader for resource and hydration tests. */
294
+ declare function createDataTestAdapter<Input, Data>(): DataTestAdapter<Input, Data>;
295
+ //#endregion
296
+ export { AnyDataResourceDefinition, DataCancelledState, DataClient, DataClientOptions, DataErrorState, DataHttpError, DataIdleState, DataLoadContext, DataLoadOptions, DataLoadReason, DataLoader, DataManifest, DataManifestEntry, DataPendingState, DataResourceDefinition, DataResourceKey, DataResourceMeta, DataRetryDelay, DataRetryDelayContext, DataSleep, DataSnapshot, DataSourceSpecifier, DataStaleState, DataState, DataStateBase, DataSuccessState, DataSupport, DataTestAdapter, DataUnsupportedState, FetchDataResourceOptions, InferData, InferError, InferInput, InferKey, SerializedDataState, StreamDataResourceOptions, createDataClient, createDataManifest, createDataTestAdapter, createFetchDataResource, createStreamDataResource, defineDataResource, hydrateDataClient, hydrateDataClientFromStream, serializeDataSnapshot, streamDataSnapshot };
package/dist/index.mjs ADDED
@@ -0,0 +1,397 @@
1
+ //#region src/runtime.ts
2
+ const DATA_ERROR = {
3
+ invalidSource: "VIZE_DATA_SOURCE_NOT_VUE",
4
+ invalidKey: "VIZE_DATA_INVALID_KEY",
5
+ invalidDeadline: "VIZE_DATA_INVALID_DEADLINE",
6
+ invalidRetry: "VIZE_DATA_INVALID_RETRY"
7
+ };
8
+ /** Define a typed SSR data resource and preserve literal endpoint metadata. */
9
+ function defineDataResource(resource) {
10
+ validateResource(resource);
11
+ return resource;
12
+ }
13
+ /** Create an SSR-safe data client with request deduplication and hydration cache. */
14
+ function createDataClient(options = {}) {
15
+ const now = options.now ?? Date.now;
16
+ const sleep = options.sleep ?? defaultSleep;
17
+ const cache = /* @__PURE__ */ new Map();
18
+ const inflight = /* @__PURE__ */ new Map();
19
+ const versions = /* @__PURE__ */ new Map();
20
+ const listeners = /* @__PURE__ */ new Set();
21
+ for (const state of options.snapshot?.resources ?? []) cache.set(state.requestKey, state);
22
+ return {
23
+ load(resource, input, loadOptions) {
24
+ validateResource(resource);
25
+ const requestKey = createRequestKey(resource.key, input);
26
+ const cached = cache.get(requestKey);
27
+ const policy = loadOptions?.policy ?? "dedupe";
28
+ if (policy !== "revalidate" && policy !== "replace" && policy !== "stale-while-revalidate" && cached?.status === "success") return Promise.resolve(cached);
29
+ if (policy === "cache-first" && cached != null && cached.status !== "pending") return Promise.resolve(cached);
30
+ const current = inflight.get(requestKey);
31
+ if (policy === "dedupe" && current) return current.promise;
32
+ const staleWhileRevalidate = policy === "stale-while-revalidate" && cached !== void 0 && (cached.status === "success" || cached.status === "stale");
33
+ if (staleWhileRevalidate && current) return Promise.resolve(cached);
34
+ if (policy === "replace") current?.controller.abort("replaced");
35
+ const version = (versions.get(requestKey) ?? 0) + 1;
36
+ versions.set(requestKey, version);
37
+ const controller = createLinkedAbortController(loadOptions);
38
+ const commit = (state) => {
39
+ if (versions.get(requestKey) !== version) return false;
40
+ cache.set(requestKey, state);
41
+ for (const listener of listeners) listener(state);
42
+ return true;
43
+ };
44
+ let immediate = cached;
45
+ if (staleWhileRevalidate && cached.status === "success") {
46
+ const stale = {
47
+ ...cached,
48
+ status: "stale",
49
+ staleAt: now(),
50
+ reason: "stale-while-revalidate"
51
+ };
52
+ commit(stale);
53
+ immediate = stale;
54
+ }
55
+ const task = runLoadAttempt({
56
+ commit,
57
+ input,
58
+ loadOptions,
59
+ now,
60
+ requestKey,
61
+ resource,
62
+ signal: controller.signal,
63
+ sleep
64
+ }).finally(() => {
65
+ if (inflight.get(requestKey)?.version === version) inflight.delete(requestKey);
66
+ controller.dispose();
67
+ });
68
+ inflight.set(requestKey, {
69
+ controller,
70
+ promise: task,
71
+ version
72
+ });
73
+ return staleWhileRevalidate ? Promise.resolve(immediate) : task;
74
+ },
75
+ peek(resource, input) {
76
+ validateResource(resource);
77
+ return cache.get(createRequestKey(resource.key, input));
78
+ },
79
+ invalidate(resource, input, reason = "manual") {
80
+ validateResource(resource);
81
+ const prefix = `${resource.key}:`;
82
+ let changed = 0;
83
+ for (const [requestKey, state] of cache) {
84
+ if (input !== void 0 && requestKey !== createRequestKey(resource.key, input)) continue;
85
+ if (input === void 0 && !requestKey.startsWith(prefix)) continue;
86
+ if (state.status !== "success") continue;
87
+ const stale = {
88
+ ...state,
89
+ status: "stale",
90
+ staleAt: now(),
91
+ reason
92
+ };
93
+ cache.set(requestKey, stale);
94
+ for (const listener of listeners) listener(stale);
95
+ changed += 1;
96
+ }
97
+ return changed;
98
+ },
99
+ optimistic(resource, input, update) {
100
+ validateResource(resource);
101
+ const requestKey = createRequestKey(resource.key, input);
102
+ const previous = cache.get(requestKey);
103
+ if (previous?.status !== "success" && previous?.status !== "stale") return () => false;
104
+ const optimistic = {
105
+ ...previous,
106
+ status: "stale",
107
+ data: update(previous.data),
108
+ staleAt: now(),
109
+ reason: "optimistic"
110
+ };
111
+ cache.set(requestKey, optimistic);
112
+ for (const listener of listeners) listener(optimistic);
113
+ return () => {
114
+ if (cache.get(requestKey) !== optimistic) return false;
115
+ cache.set(requestKey, previous);
116
+ for (const listener of listeners) listener(previous);
117
+ return true;
118
+ };
119
+ },
120
+ cancel(resource, input, reason = "cancelled") {
121
+ validateResource(resource);
122
+ const requestKey = createRequestKey(resource.key, input);
123
+ const current = inflight.get(requestKey);
124
+ if (!current) return false;
125
+ current.controller.abort(reason);
126
+ return true;
127
+ },
128
+ subscribe(listener) {
129
+ listeners.add(listener);
130
+ return () => listeners.delete(listener);
131
+ },
132
+ snapshot: () => ({
133
+ schemaVersion: 1,
134
+ resources: [...cache.values()].filter(isSerializableState)
135
+ })
136
+ };
137
+ }
138
+ /** Serialize the current cache into an SSR payload. */
139
+ function serializeDataSnapshot(client) {
140
+ return client.snapshot();
141
+ }
142
+ /** Hydrate a data client from a server-rendered payload. */
143
+ function hydrateDataClient(snapshot, options = {}) {
144
+ return createDataClient({
145
+ ...options,
146
+ snapshot
147
+ });
148
+ }
149
+ /** Emit metadata for generators, documentation, and devtools. */
150
+ function createDataManifest(resources) {
151
+ for (const resource of resources) validateResource(resource);
152
+ return {
153
+ schemaVersion: 1,
154
+ resources: resources.map((resource) => ({
155
+ key: resource.key,
156
+ source: resource.source,
157
+ meta: resource.meta ?? {}
158
+ }))
159
+ };
160
+ }
161
+ /** Stream a snapshot as deterministic records for render pipelines. */
162
+ async function* streamDataSnapshot(snapshot) {
163
+ for (const state of snapshot.resources) yield state;
164
+ }
165
+ async function runLoadAttempt(options) {
166
+ const support = options.resource.supports?.(options.input);
167
+ if (support === false || typeof support === "string") {
168
+ const unsupported = {
169
+ status: "unsupported",
170
+ key: options.resource.key,
171
+ input: options.input,
172
+ requestKey: options.requestKey,
173
+ reason: typeof support === "string" ? support : "unsupported"
174
+ };
175
+ options.commit(unsupported);
176
+ return unsupported;
177
+ }
178
+ const retries = normalizeRetries(options.loadOptions?.retries);
179
+ for (let attempt = 1;; attempt += 1) {
180
+ options.commit({
181
+ status: "pending",
182
+ key: options.resource.key,
183
+ input: options.input,
184
+ requestKey: options.requestKey,
185
+ attempt,
186
+ startedAt: options.now()
187
+ });
188
+ try {
189
+ if (options.signal.aborted) return cacheCancelled(options);
190
+ const data = await options.resource.loader({
191
+ key: options.resource.key,
192
+ input: options.input,
193
+ meta: options.resource.meta ?? {},
194
+ reason: attempt === 1 ? options.loadOptions?.reason ?? "initial" : "retry",
195
+ signal: options.signal
196
+ });
197
+ if (options.signal.aborted) return cacheCancelled(options);
198
+ const success = {
199
+ status: "success",
200
+ key: options.resource.key,
201
+ input: options.input,
202
+ requestKey: options.requestKey,
203
+ data,
204
+ updatedAt: options.now()
205
+ };
206
+ options.commit(success);
207
+ return success;
208
+ } catch (error) {
209
+ if (options.signal.aborted) return cacheCancelled(options);
210
+ if (attempt > retries) {
211
+ const failed = {
212
+ status: "error",
213
+ key: options.resource.key,
214
+ input: options.input,
215
+ requestKey: options.requestKey,
216
+ error: options.resource.serializeError?.(error) ?? error,
217
+ updatedAt: options.now()
218
+ };
219
+ options.commit(failed);
220
+ return failed;
221
+ }
222
+ const delay = retryDelay(options.loadOptions?.retryDelayMs, {
223
+ attempt,
224
+ error,
225
+ input: options.input,
226
+ key: options.resource.key
227
+ });
228
+ try {
229
+ await options.sleep(delay, options.signal);
230
+ } catch (sleepError) {
231
+ if (options.signal.aborted) return cacheCancelled(options);
232
+ const failed = {
233
+ status: "error",
234
+ key: options.resource.key,
235
+ input: options.input,
236
+ requestKey: options.requestKey,
237
+ error: options.resource.serializeError?.(sleepError) ?? sleepError,
238
+ updatedAt: options.now()
239
+ };
240
+ options.commit(failed);
241
+ return failed;
242
+ }
243
+ }
244
+ }
245
+ }
246
+ function cacheCancelled(options) {
247
+ const cancelled = {
248
+ status: "cancelled",
249
+ key: options.resource.key,
250
+ input: options.input,
251
+ requestKey: options.requestKey,
252
+ reason: String(options.signal.reason ?? "aborted"),
253
+ cancelledAt: options.now()
254
+ };
255
+ options.commit(cancelled);
256
+ return cancelled;
257
+ }
258
+ function normalizeRetries(retries) {
259
+ if (retries == null) return 0;
260
+ if (!Number.isInteger(retries) || retries < 0) throw new Error(`[${DATA_ERROR.invalidRetry}] retries must be a non-negative integer`);
261
+ return retries;
262
+ }
263
+ function retryDelay(retryDelayMs, context) {
264
+ const value = typeof retryDelayMs === "function" ? retryDelayMs(context) : retryDelayMs ?? 0;
265
+ if (!Number.isFinite(value) || value < 0) throw new Error(`[${DATA_ERROR.invalidRetry}] retryDelayMs must be a non-negative number`);
266
+ return value;
267
+ }
268
+ function createLinkedAbortController(loadOptions) {
269
+ const controller = new AbortController();
270
+ const abort = () => controller.abort(loadOptions?.signal?.reason ?? "aborted");
271
+ if (loadOptions?.signal?.aborted) abort();
272
+ else loadOptions?.signal?.addEventListener("abort", abort, { once: true });
273
+ let deadline;
274
+ if (loadOptions?.deadlineMs != null) {
275
+ if (!Number.isFinite(loadOptions.deadlineMs) || loadOptions.deadlineMs < 0) throw new Error(`[${DATA_ERROR.invalidDeadline}] deadlineMs must be a non-negative number`);
276
+ if (loadOptions.deadlineMs === 0) controller.abort("deadline");
277
+ else deadline = setTimeout(() => controller.abort("deadline"), loadOptions.deadlineMs);
278
+ }
279
+ return {
280
+ signal: controller.signal,
281
+ abort(reason) {
282
+ controller.abort(reason);
283
+ },
284
+ dispose() {
285
+ if (deadline) clearTimeout(deadline);
286
+ loadOptions?.signal?.removeEventListener("abort", abort);
287
+ }
288
+ };
289
+ }
290
+ function defaultSleep(milliseconds, signal) {
291
+ if (signal.aborted) return Promise.reject(signal.reason);
292
+ if (milliseconds <= 0) return Promise.resolve();
293
+ return new Promise((resolve, reject) => {
294
+ const done = () => {
295
+ signal.removeEventListener("abort", abort);
296
+ resolve();
297
+ };
298
+ const timeout = setTimeout(done, milliseconds);
299
+ const abort = () => {
300
+ clearTimeout(timeout);
301
+ reject(signal.reason);
302
+ };
303
+ signal.addEventListener("abort", abort, { once: true });
304
+ });
305
+ }
306
+ function createRequestKey(key, input) {
307
+ return `${key}:${stableStringify(input)}`;
308
+ }
309
+ function stableStringify(value) {
310
+ if (value == null || typeof value !== "object") return JSON.stringify(value);
311
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
312
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
313
+ }
314
+ function isSerializableState(state) {
315
+ return state.status !== "idle" && state.status !== "pending";
316
+ }
317
+ function validateResource(resource) {
318
+ if (!resource.key) throw new Error(`[${DATA_ERROR.invalidKey}] Data resource key is required`);
319
+ if (!resource.source.endsWith(".vue")) throw new Error(`[${DATA_ERROR.invalidSource}] Data resource ${resource.key} must use .vue`);
320
+ }
321
+ //#endregion
322
+ //#region src/adapters.ts
323
+ /** HTTP loader failure with the response status and decoded error body. */
324
+ var DataHttpError = class extends Error {
325
+ status;
326
+ statusText;
327
+ body;
328
+ constructor(response, body) {
329
+ super(`HTTP ${response.status} ${response.statusText}`.trim());
330
+ this.name = "DataHttpError";
331
+ this.status = response.status;
332
+ this.statusText = response.statusText;
333
+ this.body = body;
334
+ }
335
+ };
336
+ /** Adapt a typed HTTP endpoint without reading `fetch` during module evaluation. */
337
+ function createFetchDataResource(options) {
338
+ return defineDataResource({
339
+ key: options.key,
340
+ source: options.source,
341
+ ...options.meta === void 0 ? {} : { meta: options.meta },
342
+ ...options.serializeError === void 0 ? {} : { serializeError: options.serializeError },
343
+ supports: () => options.fetch !== void 0 || typeof globalThis.fetch === "function" || "fetch unavailable",
344
+ async loader({ input, signal }) {
345
+ const response = await (options.fetch ?? globalThis.fetch)(options.endpoint(input), {
346
+ ...options.init?.(input),
347
+ signal
348
+ });
349
+ if (!response.ok) throw new DataHttpError(response, options.decodeError ? await options.decodeError(response) : await response.text().catch(() => void 0));
350
+ return options.decode ? await options.decode(response) : await response.json();
351
+ }
352
+ });
353
+ }
354
+ /** Collect an abort-aware async stream into one typed resource state. */
355
+ function createStreamDataResource(options) {
356
+ return defineDataResource({
357
+ key: options.key,
358
+ source: options.source,
359
+ ...options.meta === void 0 ? {} : { meta: options.meta },
360
+ ...options.serializeError === void 0 ? {} : { serializeError: options.serializeError },
361
+ async loader(context) {
362
+ let data = options.initial(context.input);
363
+ for await (const chunk of options.open(context)) {
364
+ context.signal.throwIfAborted();
365
+ data = options.reduce(data, chunk);
366
+ }
367
+ return data;
368
+ }
369
+ });
370
+ }
371
+ /** Reconstruct an SSR cache from an incremental snapshot stream. */
372
+ async function hydrateDataClientFromStream(stream, options = {}) {
373
+ const resources = [];
374
+ for await (const state of stream) resources.push(state);
375
+ return hydrateDataClient({
376
+ schemaVersion: 1,
377
+ resources
378
+ }, options);
379
+ }
380
+ /** Deterministic queue-backed loader for resource and hydration tests. */
381
+ function createDataTestAdapter() {
382
+ const calls = [];
383
+ const results = [];
384
+ return {
385
+ calls,
386
+ enqueue: (result) => results.push(result),
387
+ async loader(context) {
388
+ calls.push(context);
389
+ const result = results.shift();
390
+ if (result === void 0) throw new Error("No queued data test result");
391
+ if (result instanceof Error) throw result;
392
+ return typeof result === "function" ? await result(context) : result;
393
+ }
394
+ };
395
+ }
396
+ //#endregion
397
+ export { DataHttpError, createDataClient, createDataManifest, createDataTestAdapter, createFetchDataResource, createStreamDataResource, defineDataResource, hydrateDataClient, hydrateDataClientFromStream, serializeDataSnapshot, streamDataSnapshot };
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@vizejs/data",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.713.0",
4
+ "description": "Typed SSR data resource foundations for Vize applications",
5
+ "keywords": [
6
+ "data-fetching",
7
+ "resource",
8
+ "ssr",
9
+ "type-safe",
10
+ "vize",
11
+ "vue"
12
+ ],
13
+ "homepage": "https://github.com/ubugeeei-prod/vize",
14
+ "bugs": {
15
+ "url": "https://github.com/ubugeeei-prod/vize/issues"
16
+ },
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/ubugeeei-prod/vize.git",
21
+ "directory": "npm/compose/data"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.mjs",
30
+ "types": "./dist/index.d.mts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.mts",
34
+ "import": "./dist/index.mjs",
35
+ "default": "./dist/index.mjs"
36
+ }
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "vp pack",
43
+ "dev": "vp pack --watch",
44
+ "test": "vp exec node --test 'src/**/*.test.ts'",
45
+ "check": "vp check src vite.config.ts && vp exec tsc --noEmit -p tsconfig.json",
46
+ "check:fix": "vp check --fix src vite.config.ts",
47
+ "check:types": "vp exec tsc --noEmit -p tsconfig.json",
48
+ "fmt": "vp fmt --write src vite.config.ts"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "catalog:typescript",
52
+ "tsdown": "catalog:vite-stack",
53
+ "typescript": "catalog:typescript",
54
+ "vite-plus": "catalog:vite-stack"
55
+ },
56
+ "engines": {
57
+ "node": ">=22"
58
+ },
59
+ "metadata": {
60
+ "vize": {
61
+ "stability": "experimental",
62
+ "stabilityDocs": "https://github.com/ubugeeei-prod/vize/blob/main/docs/content/stability.md#package-support-tiers"
63
+ }
64
+ }
65
+ }