@firsthandjs/data 0.5.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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/codegen.d.ts +77 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.dev.js +67 -0
- package/dist/codegen.js +67 -0
- package/dist/dev.d.ts +10 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.prod.d.ts +3 -0
- package/dist/dev.prod.d.ts.map +1 -0
- package/dist/document.d.ts +98 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/http.d.ts +64 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.dev.js +562 -0
- package/dist/index.js +2 -0
- package/dist/resource.d.ts +82 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/store.d.ts +121 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/tags.d.ts +42 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.dev.js +223 -0
- package/dist/vite.js +223 -0
- package/package.json +59 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
// packages/data/src/resource.ts
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
effect,
|
|
5
|
+
onCleanup,
|
|
6
|
+
untrack,
|
|
7
|
+
useContext
|
|
8
|
+
} from "@firsthandjs/core";
|
|
9
|
+
|
|
10
|
+
// packages/data/src/store.ts
|
|
11
|
+
import { batch, signal } from "@firsthandjs/core";
|
|
12
|
+
|
|
13
|
+
// packages/data/src/tags.ts
|
|
14
|
+
var NO_VARS = Object.freeze({});
|
|
15
|
+
function tag(name, vars = NO_VARS) {
|
|
16
|
+
return { name, vars };
|
|
17
|
+
}
|
|
18
|
+
function tagMatches(pattern, candidate) {
|
|
19
|
+
if (pattern.name !== candidate.name) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
for (const name of Object.keys(pattern.vars)) {
|
|
23
|
+
if (!Object.is(pattern.vars[name], candidate.vars[name])) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
function anyTagMatches(patterns, tags) {
|
|
30
|
+
for (const pattern of patterns) {
|
|
31
|
+
for (const candidate of tags) {
|
|
32
|
+
if (tagMatches(pattern, candidate)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// packages/data/src/dev.ts
|
|
41
|
+
function hook() {
|
|
42
|
+
const installed = globalThis.__FIRSTHAND_DEVTOOLS__;
|
|
43
|
+
return installed !== void 0 && installed.attached ? installed : void 0;
|
|
44
|
+
}
|
|
45
|
+
function devQuery(event, key, tags) {
|
|
46
|
+
hook()?.query(event, key, tags.map(readable));
|
|
47
|
+
}
|
|
48
|
+
function readable(value) {
|
|
49
|
+
const names = Object.keys(value.vars).sort();
|
|
50
|
+
if (names.length === 0) {
|
|
51
|
+
return value.name;
|
|
52
|
+
}
|
|
53
|
+
const vars = names.map((name) => `${name}: ${String(value.vars[name])}`).join(", ");
|
|
54
|
+
return `${value.name}(${vars})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// packages/data/src/store.ts
|
|
58
|
+
function createData(options = {}) {
|
|
59
|
+
const held = /* @__PURE__ */ new Set();
|
|
60
|
+
return {
|
|
61
|
+
storage: options.storage,
|
|
62
|
+
hold: (entry) => {
|
|
63
|
+
held.add(entry);
|
|
64
|
+
return () => held.delete(entry);
|
|
65
|
+
},
|
|
66
|
+
invalidate: async (...patterns) => {
|
|
67
|
+
const waiting = [];
|
|
68
|
+
for (const entry of [...held]) {
|
|
69
|
+
if (entry.controller !== null) {
|
|
70
|
+
entry.pending.push(...patterns);
|
|
71
|
+
}
|
|
72
|
+
if (!anyTagMatches(patterns, entry.tags)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
devQuery("invalidated", entry.name ?? "(call site)", entry.tags);
|
|
76
|
+
waiting.push(entry.run(true));
|
|
77
|
+
}
|
|
78
|
+
await Promise.all(waiting);
|
|
79
|
+
},
|
|
80
|
+
clear: () => {
|
|
81
|
+
for (const entry of [...held]) {
|
|
82
|
+
entry.controller?.abort();
|
|
83
|
+
held.delete(entry);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
options.storage?.clear?.();
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
get size() {
|
|
91
|
+
return held.size;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function createHeld(store, name) {
|
|
96
|
+
const entry = {
|
|
97
|
+
data: signal(void 0),
|
|
98
|
+
error: signal(void 0),
|
|
99
|
+
status: signal("idle"),
|
|
100
|
+
loading: signal(false),
|
|
101
|
+
tags: [],
|
|
102
|
+
controller: null,
|
|
103
|
+
pending: [],
|
|
104
|
+
superseded: false,
|
|
105
|
+
disposed: false,
|
|
106
|
+
name
|
|
107
|
+
};
|
|
108
|
+
return entry;
|
|
109
|
+
}
|
|
110
|
+
function succeed(entry, value) {
|
|
111
|
+
batch(() => {
|
|
112
|
+
entry.data.value = value;
|
|
113
|
+
entry.error.value = void 0;
|
|
114
|
+
entry.status.value = "success";
|
|
115
|
+
entry.loading.value = false;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function fail(entry, error) {
|
|
119
|
+
batch(() => {
|
|
120
|
+
entry.error.value = error;
|
|
121
|
+
entry.status.value = "error";
|
|
122
|
+
entry.loading.value = false;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function expose(entry, release) {
|
|
126
|
+
return {
|
|
127
|
+
data: entry.data,
|
|
128
|
+
error: entry.error,
|
|
129
|
+
status: entry.status,
|
|
130
|
+
loading: entry.loading,
|
|
131
|
+
reload: () => entry.run(true),
|
|
132
|
+
dispose: () => {
|
|
133
|
+
entry.disposed = true;
|
|
134
|
+
entry.controller?.abort();
|
|
135
|
+
release();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// packages/data/src/resource.ts
|
|
141
|
+
var DataContext = createContext();
|
|
142
|
+
function useData() {
|
|
143
|
+
return useContext(DataContext).value;
|
|
144
|
+
}
|
|
145
|
+
function useInvalidate() {
|
|
146
|
+
const store = useData();
|
|
147
|
+
return (...patterns) => store.invalidate(...patterns);
|
|
148
|
+
}
|
|
149
|
+
function useResource(load, options = {}) {
|
|
150
|
+
const store = useData();
|
|
151
|
+
const entry = createHeld(store, options.persist);
|
|
152
|
+
const release = store.hold(entry);
|
|
153
|
+
devQuery("created", options.persist ?? "(call site)", []);
|
|
154
|
+
entry.run = (force) => {
|
|
155
|
+
if (entry.disposed) {
|
|
156
|
+
return Promise.resolve(void 0);
|
|
157
|
+
}
|
|
158
|
+
entry.controller?.abort();
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
entry.controller = controller;
|
|
161
|
+
entry.pending = [];
|
|
162
|
+
entry.superseded = false;
|
|
163
|
+
entry.loading.value = true;
|
|
164
|
+
if (entry.data.peek() === void 0) {
|
|
165
|
+
entry.status.value = "loading";
|
|
166
|
+
}
|
|
167
|
+
const context = {
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
force,
|
|
170
|
+
tags: (...next) => {
|
|
171
|
+
entry.tags = next;
|
|
172
|
+
if (entry.pending.length > 0 && anyTagMatches(entry.pending, next)) {
|
|
173
|
+
entry.superseded = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return load(context).then(async (value) => {
|
|
178
|
+
if (controller.signal.aborted || entry.disposed) {
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
entry.controller = null;
|
|
182
|
+
succeed(entry, value);
|
|
183
|
+
if (options.persist !== void 0) {
|
|
184
|
+
try {
|
|
185
|
+
store.storage?.write?.(options.persist, value);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (entry.superseded) {
|
|
190
|
+
entry.superseded = false;
|
|
191
|
+
return entry.run(true);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}).catch((error) => {
|
|
195
|
+
if (controller.signal.aborted || entry.disposed) {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
entry.controller = null;
|
|
199
|
+
fail(entry, error);
|
|
200
|
+
return void 0;
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const persist = options.persist;
|
|
204
|
+
const storage = store.storage;
|
|
205
|
+
if (persist !== void 0 && storage?.read !== void 0) {
|
|
206
|
+
void (async () => {
|
|
207
|
+
try {
|
|
208
|
+
const stored = await storage.read?.(persist);
|
|
209
|
+
if (stored !== void 0 && entry.data.peek() === void 0 && !entry.disposed) {
|
|
210
|
+
succeed(entry, stored);
|
|
211
|
+
entry.loading.value = true;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
}
|
|
217
|
+
effect(() => {
|
|
218
|
+
void entry.run(false);
|
|
219
|
+
});
|
|
220
|
+
onCleanup(() => {
|
|
221
|
+
entry.disposed = true;
|
|
222
|
+
entry.controller?.abort();
|
|
223
|
+
devQuery("dropped", options.persist ?? "(call site)", entry.tags);
|
|
224
|
+
release();
|
|
225
|
+
});
|
|
226
|
+
return expose(entry, release);
|
|
227
|
+
}
|
|
228
|
+
function useAction(run) {
|
|
229
|
+
const store = useData();
|
|
230
|
+
const entry = createHeld(store, void 0);
|
|
231
|
+
let controller = null;
|
|
232
|
+
onCleanup(() => {
|
|
233
|
+
entry.disposed = true;
|
|
234
|
+
controller?.abort();
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
data: entry.data,
|
|
238
|
+
error: entry.error,
|
|
239
|
+
status: entry.status,
|
|
240
|
+
running: entry.loading,
|
|
241
|
+
run: async (input) => {
|
|
242
|
+
controller?.abort();
|
|
243
|
+
controller = new AbortController();
|
|
244
|
+
const current = controller;
|
|
245
|
+
let invalidating = [];
|
|
246
|
+
entry.loading.value = true;
|
|
247
|
+
entry.status.value = "loading";
|
|
248
|
+
try {
|
|
249
|
+
const result = await untrack(
|
|
250
|
+
() => run(input, {
|
|
251
|
+
signal: current.signal,
|
|
252
|
+
invalidates: (...tags) => {
|
|
253
|
+
invalidating = tags;
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
if (current.signal.aborted || entry.disposed) {
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
succeed(entry, result);
|
|
261
|
+
if (invalidating.length > 0) {
|
|
262
|
+
await store.invalidate(...invalidating);
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (current.signal.aborted || entry.disposed) {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
fail(entry, error);
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function fromObservable(source, options = {}) {
|
|
276
|
+
const store = useData();
|
|
277
|
+
const entry = createHeld(store, void 0);
|
|
278
|
+
const release = store.hold(entry);
|
|
279
|
+
entry.run = () => options.reload === void 0 ? Promise.resolve(void 0) : void_(options.reload());
|
|
280
|
+
entry.loading.value = true;
|
|
281
|
+
entry.status.value = "loading";
|
|
282
|
+
const subscription = source.subscribe({
|
|
283
|
+
// A source that pushes after the scope has gone is pushing into nothing:
|
|
284
|
+
// the cells are still there, but nobody is reading them.
|
|
285
|
+
next: (value) => {
|
|
286
|
+
succeed(entry, value);
|
|
287
|
+
},
|
|
288
|
+
error: (error) => {
|
|
289
|
+
fail(entry, error);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
const stop = typeof subscription === "function" ? subscription : () => {
|
|
293
|
+
subscription.unsubscribe();
|
|
294
|
+
};
|
|
295
|
+
onCleanup(() => {
|
|
296
|
+
entry.disposed = true;
|
|
297
|
+
stop();
|
|
298
|
+
release();
|
|
299
|
+
});
|
|
300
|
+
return expose(entry, () => {
|
|
301
|
+
stop();
|
|
302
|
+
release();
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function fromPromise(factory) {
|
|
306
|
+
return useResource(() => factory());
|
|
307
|
+
}
|
|
308
|
+
async function void_(promise) {
|
|
309
|
+
await promise;
|
|
310
|
+
return void 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// packages/data/src/document.ts
|
|
314
|
+
var DIRECTIVE = /^@(tag|invalidates)\b/;
|
|
315
|
+
var OPERATION = /\b(query|mutation|subscription)\b[^\S\n]*([A-Za-z_]\w*)?/;
|
|
316
|
+
var NAME = /^[_A-Za-z][_0-9A-Za-z]*/;
|
|
317
|
+
var FirsthandDirectiveError = class extends Error {
|
|
318
|
+
constructor(message) {
|
|
319
|
+
super(message);
|
|
320
|
+
this.name = "FirsthandDirectiveError";
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
function endOfString(source, start) {
|
|
324
|
+
if (source.startsWith('"""', start)) {
|
|
325
|
+
const close = source.indexOf('"""', start + 3);
|
|
326
|
+
return close === -1 ? source.length : close + 3;
|
|
327
|
+
}
|
|
328
|
+
let at = start + 1;
|
|
329
|
+
while (at < source.length && source[at] !== '"') {
|
|
330
|
+
at += source[at] === "\\" ? 2 : 1;
|
|
331
|
+
}
|
|
332
|
+
return at + 1;
|
|
333
|
+
}
|
|
334
|
+
function endOfArguments(source, start) {
|
|
335
|
+
let depth = 0;
|
|
336
|
+
let at = start;
|
|
337
|
+
while (at < source.length) {
|
|
338
|
+
const character = source[at];
|
|
339
|
+
if (character === '"') {
|
|
340
|
+
at = endOfString(source, at);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (character === "(" || character === "[" || character === "{") {
|
|
344
|
+
depth++;
|
|
345
|
+
} else if (character === ")" || character === "]" || character === "}") {
|
|
346
|
+
depth--;
|
|
347
|
+
if (depth === 0) {
|
|
348
|
+
return at + 1;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
at++;
|
|
352
|
+
}
|
|
353
|
+
throw new FirsthandDirectiveError(`unclosed arguments in ${source.slice(start, start + 40)}`);
|
|
354
|
+
}
|
|
355
|
+
function unquote(quoted, directive) {
|
|
356
|
+
try {
|
|
357
|
+
return JSON.parse(quoted);
|
|
358
|
+
} catch {
|
|
359
|
+
throw new FirsthandDirectiveError(`@${directive}: ${quoted} is not a valid string`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function parseArguments(raw, directive) {
|
|
363
|
+
const vars = {};
|
|
364
|
+
let at = 0;
|
|
365
|
+
const skipIgnored = () => {
|
|
366
|
+
while (at < raw.length && /[\s,]/.test(raw[at])) {
|
|
367
|
+
at++;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
skipIgnored();
|
|
371
|
+
while (at < raw.length) {
|
|
372
|
+
const name = NAME.exec(raw.slice(at));
|
|
373
|
+
if (name === null) {
|
|
374
|
+
throw new FirsthandDirectiveError(
|
|
375
|
+
`@${directive}: expected an argument name at "${raw.slice(at)}"`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
at += name[0].length;
|
|
379
|
+
skipIgnored();
|
|
380
|
+
if (raw[at] !== ":") {
|
|
381
|
+
throw new FirsthandDirectiveError(`@${directive}: argument ${name[0]} has no value`);
|
|
382
|
+
}
|
|
383
|
+
at++;
|
|
384
|
+
skipIgnored();
|
|
385
|
+
const read = readValue(raw, directive, at);
|
|
386
|
+
vars[name[0]] = read.value;
|
|
387
|
+
at = read.next;
|
|
388
|
+
skipIgnored();
|
|
389
|
+
}
|
|
390
|
+
return vars;
|
|
391
|
+
}
|
|
392
|
+
function readValue(raw, directive, at) {
|
|
393
|
+
const character = raw[at];
|
|
394
|
+
if (character === "$") {
|
|
395
|
+
const name = NAME.exec(raw.slice(at + 1));
|
|
396
|
+
if (name === null) {
|
|
397
|
+
throw new FirsthandDirectiveError(`@${directive}: expected a variable name after $`);
|
|
398
|
+
}
|
|
399
|
+
return { value: { variable: name[0] }, next: at + 1 + name[0].length };
|
|
400
|
+
}
|
|
401
|
+
if (character === '"') {
|
|
402
|
+
const end = endOfString(raw, at);
|
|
403
|
+
const quoted = raw.slice(at, end);
|
|
404
|
+
const literal = quoted.startsWith('"""') ? quoted.slice(3, -3).trim() : unquote(quoted, directive);
|
|
405
|
+
return { value: { literal }, next: end };
|
|
406
|
+
}
|
|
407
|
+
if (character === "[" || character === "{") {
|
|
408
|
+
throw new FirsthandDirectiveError(
|
|
409
|
+
`@${directive}: a tag variable must be a scalar, not a list or object`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const word = /^[^\s,)]+/.exec(raw.slice(at));
|
|
413
|
+
if (word === null) {
|
|
414
|
+
throw new FirsthandDirectiveError(`@${directive}: expected a value`);
|
|
415
|
+
}
|
|
416
|
+
const text = word[0];
|
|
417
|
+
const next = at + text.length;
|
|
418
|
+
if (text === "true" || text === "false") {
|
|
419
|
+
return { value: { literal: text === "true" }, next };
|
|
420
|
+
}
|
|
421
|
+
if (text === "null") {
|
|
422
|
+
return { value: { literal: null }, next };
|
|
423
|
+
}
|
|
424
|
+
const asNumber = Number(text);
|
|
425
|
+
return { value: { literal: Number.isNaN(asNumber) ? text : asNumber }, next };
|
|
426
|
+
}
|
|
427
|
+
function scan(source) {
|
|
428
|
+
const tags = [];
|
|
429
|
+
const invalidates = [];
|
|
430
|
+
let stripped = "";
|
|
431
|
+
let at = 0;
|
|
432
|
+
let kept = 0;
|
|
433
|
+
while (at < source.length) {
|
|
434
|
+
const character = source[at];
|
|
435
|
+
if (character === '"') {
|
|
436
|
+
at = endOfString(source, at);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (character === "#") {
|
|
440
|
+
const newline = source.indexOf("\n", at);
|
|
441
|
+
at = newline === -1 ? source.length : newline;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (character !== "@") {
|
|
445
|
+
at++;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const directive = DIRECTIVE.exec(source.slice(at));
|
|
449
|
+
if (directive === null) {
|
|
450
|
+
at++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
let end = at + directive[0].length;
|
|
454
|
+
let args = "";
|
|
455
|
+
let probe = end;
|
|
456
|
+
while (probe < source.length && /\s/.test(source[probe])) {
|
|
457
|
+
probe++;
|
|
458
|
+
}
|
|
459
|
+
if (source[probe] === "(") {
|
|
460
|
+
const close = endOfArguments(source, probe);
|
|
461
|
+
args = source.slice(probe + 1, close - 1);
|
|
462
|
+
end = close;
|
|
463
|
+
}
|
|
464
|
+
const vars = parseArguments(args, directive[1]);
|
|
465
|
+
const named = vars["name"];
|
|
466
|
+
if (named === void 0 || !("literal" in named) || typeof named.literal !== "string") {
|
|
467
|
+
throw new FirsthandDirectiveError(
|
|
468
|
+
`@${directive[1]} needs a literal name, as in @${directive[1]}(name: "user", id: $id)`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
delete vars["name"];
|
|
472
|
+
(directive[1] === "tag" ? tags : invalidates).push({ name: named.literal, vars });
|
|
473
|
+
let from = at;
|
|
474
|
+
while (from > kept && /\s/.test(source[from - 1])) {
|
|
475
|
+
from--;
|
|
476
|
+
}
|
|
477
|
+
stripped += source.slice(kept, from);
|
|
478
|
+
kept = end;
|
|
479
|
+
at = end;
|
|
480
|
+
}
|
|
481
|
+
return { tags, invalidates, stripped: stripped + source.slice(kept) };
|
|
482
|
+
}
|
|
483
|
+
function parseGraphQL(source) {
|
|
484
|
+
const { tags, invalidates, stripped } = scan(source);
|
|
485
|
+
const operation = OPERATION.exec(stripped.replace(/#[^\n]*/g, ""));
|
|
486
|
+
return {
|
|
487
|
+
source: stripped,
|
|
488
|
+
operation: operation?.[2] ?? "",
|
|
489
|
+
kind: operation?.[1] ?? "query",
|
|
490
|
+
tags,
|
|
491
|
+
invalidates
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function resolveTags(templates, variables) {
|
|
495
|
+
return templates.map((template) => {
|
|
496
|
+
const vars = {};
|
|
497
|
+
for (const [name, value] of Object.entries(template.vars)) {
|
|
498
|
+
if ("literal" in value) {
|
|
499
|
+
vars[name] = value.literal;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const bound = variables[value.variable];
|
|
503
|
+
if (bound !== void 0 && bound !== null) {
|
|
504
|
+
vars[name] = typeof bound === "object" ? JSON.stringify(bound) : bound;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return tag(template.name, vars);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// packages/data/src/http.ts
|
|
512
|
+
var FirsthandHttpError = class extends Error {
|
|
513
|
+
constructor(status, url, body) {
|
|
514
|
+
super(`HTTP ${String(status)} for ${url}`);
|
|
515
|
+
this.status = status;
|
|
516
|
+
this.url = url;
|
|
517
|
+
this.body = body;
|
|
518
|
+
this.name = "FirsthandHttpError";
|
|
519
|
+
}
|
|
520
|
+
status;
|
|
521
|
+
url;
|
|
522
|
+
body;
|
|
523
|
+
};
|
|
524
|
+
function json(input, init = {}) {
|
|
525
|
+
return async ({ signal: signal2 }) => {
|
|
526
|
+
const { json: payload, ...rest } = init;
|
|
527
|
+
const options = { ...rest, signal: signal2 };
|
|
528
|
+
if (payload !== void 0) {
|
|
529
|
+
options.body = JSON.stringify(payload);
|
|
530
|
+
const headers = new Headers(init.headers);
|
|
531
|
+
if (!headers.has("content-type")) {
|
|
532
|
+
headers.set("content-type", "application/json");
|
|
533
|
+
}
|
|
534
|
+
options.headers = headers;
|
|
535
|
+
}
|
|
536
|
+
const response = await fetch(input, options);
|
|
537
|
+
const text = await response.text();
|
|
538
|
+
const parsed = text === "" ? void 0 : JSON.parse(text);
|
|
539
|
+
if (!response.ok) {
|
|
540
|
+
throw new FirsthandHttpError(response.status, input, parsed);
|
|
541
|
+
}
|
|
542
|
+
return parsed;
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
export {
|
|
546
|
+
DataContext,
|
|
547
|
+
FirsthandDirectiveError,
|
|
548
|
+
FirsthandHttpError,
|
|
549
|
+
anyTagMatches,
|
|
550
|
+
createData,
|
|
551
|
+
fromObservable,
|
|
552
|
+
fromPromise,
|
|
553
|
+
json,
|
|
554
|
+
parseGraphQL,
|
|
555
|
+
resolveTags,
|
|
556
|
+
tag,
|
|
557
|
+
tagMatches,
|
|
558
|
+
useAction,
|
|
559
|
+
useData,
|
|
560
|
+
useInvalidate,
|
|
561
|
+
useResource
|
|
562
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createContext as I,effect as H,onCleanup as S,untrack as N,useContext as q}from"@firsthandjs/core";import{batch as O,signal as x}from"@firsthandjs/core";var j=Object.freeze({});function R(e,t=j){return{name:e,vars:t}}function D(e,t){if(e.name!==t.name)return!1;for(let r of Object.keys(e.vars))if(!Object.is(e.vars[r],t.vars[r]))return!1;return!0}function p(e,t){for(let r of e)for(let n of t)if(D(r,n))return!0;return!1}function C(e={}){let t=new Set;return{storage:e.storage,hold:r=>(t.add(r),()=>t.delete(r)),invalidate:async(...r)=>{let n=[];for(let a of[...t])a.controller!==null&&a.pending.push(...r),p(r,a.tags)&&(a.name,a.tags,n.push(a.run(!0)));await Promise.all(n)},clear:()=>{for(let r of[...t])r.controller?.abort(),t.delete(r);try{e.storage?.clear?.()}catch{}},get size(){return t.size}}}function b(e,t){return{data:x(void 0),error:x(void 0),status:x("idle"),loading:x(!1),tags:[],controller:null,pending:[],superseded:!1,disposed:!1,name:t}}function f(e,t){O(()=>{e.data.value=t,e.error.value=void 0,e.status.value="success",e.loading.value=!1})}function v(e,t){O(()=>{e.error.value=t,e.status.value="error",e.loading.value=!1})}function k(e,t){return{data:e.data,error:e.error,status:e.status,loading:e.loading,reload:()=>e.run(!0),dispose:()=>{e.disposed=!0,e.controller?.abort(),t()}}}var P=I();function m(){return q(P).value}function Q(){let e=m();return(...t)=>e.invalidate(...t)}function A(e,t={}){let r=m(),n=b(r,t.persist),a=r.hold(n);t.persist,n.run=i=>{if(n.disposed)return Promise.resolve(void 0);n.controller?.abort();let l=new AbortController;n.controller=l,n.pending=[],n.superseded=!1,n.loading.value=!0,n.data.peek()===void 0&&(n.status.value="loading");let u={signal:l.signal,force:i,tags:(...d)=>{n.tags=d,n.pending.length>0&&p(n.pending,d)&&(n.superseded=!0)}};return e(u).then(async d=>{if(!(l.signal.aborted||n.disposed)){if(n.controller=null,f(n,d),t.persist!==void 0)try{r.storage?.write?.(t.persist,d)}catch{}return n.superseded?(n.superseded=!1,n.run(!0)):d}}).catch(d=>{l.signal.aborted||n.disposed||(n.controller=null,v(n,d))})};let o=t.persist,s=r.storage;return o!==void 0&&s?.read!==void 0&&(async()=>{try{let i=await s.read?.(o);i!==void 0&&n.data.peek()===void 0&&!n.disposed&&(f(n,i),n.loading.value=!0)}catch{}})(),H(()=>{n.run(!1)}),S(()=>{n.disposed=!0,n.controller?.abort(),t.persist,n.tags,a()}),k(n,a)}function E(e){let t=m(),r=b(t,void 0),n=null;return S(()=>{r.disposed=!0,n?.abort()}),{data:r.data,error:r.error,status:r.status,running:r.loading,run:async a=>{n?.abort(),n=new AbortController;let o=n,s=[];r.loading.value=!0,r.status.value="loading";try{let i=await N(()=>e(a,{signal:o.signal,invalidates:(...l)=>{s=l}}));return o.signal.aborted||r.disposed?void 0:(f(r,i),s.length>0&&await t.invalidate(...s),i)}catch(i){if(o.signal.aborted||r.disposed)return;v(r,i);return}}}}function _(e,t={}){let r=m(),n=b(r,void 0),a=r.hold(n);n.run=()=>t.reload===void 0?Promise.resolve(void 0):J(t.reload()),n.loading.value=!0,n.status.value="loading";let o=e.subscribe({next:i=>{f(n,i)},error:i=>{v(n,i)}}),s=typeof o=="function"?o:()=>{o.unsubscribe()};return S(()=>{n.disposed=!0,s(),a()}),k(n,()=>{s(),a()})}function z(e){return A(()=>e())}async function J(e){await e}var M=/^@(tag|invalidates)\b/,G=/\b(query|mutation|subscription)\b[^\S\n]*([A-Za-z_]\w*)?/,L=/^[_A-Za-z][_0-9A-Za-z]*/,c=class extends Error{constructor(t){super(t),this.name="FirsthandDirectiveError"}};function V(e,t){if(e.startsWith('"""',t)){let n=e.indexOf('"""',t+3);return n===-1?e.length:n+3}let r=t+1;for(;r<e.length&&e[r]!=='"';)r+=e[r]==="\\"?2:1;return r+1}function B(e,t){let r=0,n=t;for(;n<e.length;){let a=e[n];if(a==='"'){n=V(e,n);continue}if(a==="("||a==="["||a==="{")r++;else if((a===")"||a==="]"||a==="}")&&(r--,r===0))return n+1;n++}throw new c(`unclosed arguments in ${e.slice(t,t+40)}`)}function F(e,t){try{return JSON.parse(e)}catch{throw new c(`@${t}: ${e} is not a valid string`)}}function Z(e,t){let r={},n=0,a=()=>{for(;n<e.length&&/[\s,]/.test(e[n]);)n++};for(a();n<e.length;){let o=L.exec(e.slice(n));if(o===null)throw new c(`@${t}: expected an argument name at "${e.slice(n)}"`);if(n+=o[0].length,a(),e[n]!==":")throw new c(`@${t}: argument ${o[0]} has no value`);n++,a();let s=W(e,t,n);r[o[0]]=s.value,n=s.next,a()}return r}function W(e,t,r){let n=e[r];if(n==="$"){let l=L.exec(e.slice(r+1));if(l===null)throw new c(`@${t}: expected a variable name after $`);return{value:{variable:l[0]},next:r+1+l[0].length}}if(n==='"'){let l=V(e,r),u=e.slice(r,l);return{value:{literal:u.startsWith('"""')?u.slice(3,-3).trim():F(u,t)},next:l}}if(n==="["||n==="{")throw new c(`@${t}: a tag variable must be a scalar, not a list or object`);let a=/^[^\s,)]+/.exec(e.slice(r));if(a===null)throw new c(`@${t}: expected a value`);let o=a[0],s=r+o.length;if(o==="true"||o==="false")return{value:{literal:o==="true"},next:s};if(o==="null")return{value:{literal:null},next:s};let i=Number(o);return{value:{literal:Number.isNaN(i)?o:i},next:s}}function K(e){let t=[],r=[],n="",a=0,o=0;for(;a<e.length;){let s=e[a];if(s==='"'){a=V(e,a);continue}if(s==="#"){let g=e.indexOf(`
|
|
2
|
+
`,a);a=g===-1?e.length:g;continue}if(s!=="@"){a++;continue}let i=M.exec(e.slice(a));if(i===null){a++;continue}let l=a+i[0].length,u="",d=l;for(;d<e.length&&/\s/.test(e[d]);)d++;if(e[d]==="("){let g=B(e,d);u=e.slice(d+1,g-1),l=g}let w=Z(u,i[1]),T=w.name;if(T===void 0||!("literal"in T)||typeof T.literal!="string")throw new c(`@${i[1]} needs a literal name, as in @${i[1]}(name: "user", id: $id)`);delete w.name,(i[1]==="tag"?t:r).push({name:T.literal,vars:w});let y=a;for(;y>o&&/\s/.test(e[y-1]);)y--;n+=e.slice(o,y),o=l,a=l}return{tags:t,invalidates:r,stripped:n+e.slice(o)}}function U(e){let{tags:t,invalidates:r,stripped:n}=K(e),a=G.exec(n.replace(/#[^\n]*/g,""));return{source:n,operation:a?.[2]??"",kind:a?.[1]??"query",tags:t,invalidates:r}}function X(e,t){return e.map(r=>{let n={};for(let[a,o]of Object.entries(r.vars)){if("literal"in o){n[a]=o.literal;continue}let s=t[o.variable];s!=null&&(n[a]=typeof s=="object"?JSON.stringify(s):s)}return R(r.name,n)})}var h=class extends Error{constructor(r,n,a){super(`HTTP ${String(r)} for ${n}`);this.status=r;this.url=n;this.body=a;this.name="FirsthandHttpError"}status;url;body};function Y(e,t={}){return async({signal:r})=>{let{json:n,...a}=t,o={...a,signal:r};if(n!==void 0){o.body=JSON.stringify(n);let u=new Headers(t.headers);u.has("content-type")||u.set("content-type","application/json"),o.headers=u}let s=await fetch(e,o),i=await s.text(),l=i===""?void 0:JSON.parse(i);if(!s.ok)throw new h(s.status,e,l);return l}}export{P as DataContext,c as FirsthandDirectiveError,h as FirsthandHttpError,p as anyTagMatches,C as createData,_ as fromObservable,z as fromPromise,Y as json,U as parseGraphQL,X as resolveTags,R as tag,D as tagMatches,E as useAction,m as useData,Q as useInvalidate,A as useResource};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export { createData } from './store.js';
|
|
2
|
+
import { type ActionContext, type DataStore, type LoadContext, type Resource, type Status } from './store.js';
|
|
3
|
+
import { type Tag } from './tags.js';
|
|
4
|
+
export declare const DataContext: import("@firsthandjs/core").Context<DataStore>;
|
|
5
|
+
export declare function useData(): DataStore;
|
|
6
|
+
/** Invalidates through the store this component is under. */
|
|
7
|
+
export declare function useInvalidate(): (...patterns: Tag[]) => Promise<void>;
|
|
8
|
+
export interface ResourceOptions {
|
|
9
|
+
/**
|
|
10
|
+
* A name to keep this resource's last value under, between visits.
|
|
11
|
+
*
|
|
12
|
+
* Persistence is the one thing that needs a name, because a name is what
|
|
13
|
+
* survives a reload — a call site does not. It is opt-in for that reason,
|
|
14
|
+
* and two resources sharing a name share what is stored, which is what
|
|
15
|
+
* naming them the same asks for.
|
|
16
|
+
*/
|
|
17
|
+
readonly persist?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Loads something, and keeps it as reactive state.
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const user = useResource(async ({ signal, tags }) => {
|
|
24
|
+
* tags(tag('user', { id: props.id }));
|
|
25
|
+
* return (await fetch(`/api/users/${props.id}`, { signal })).json() as Promise<User>;
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Everything read before the first `await` is a dependency, exactly as in an
|
|
30
|
+
* `effect` — including a token read to build a header. Read it with `peek()`
|
|
31
|
+
* if that is not what you want.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useResource<T>(load: (context: LoadContext) => Promise<T>, options?: ResourceOptions): Resource<T>;
|
|
34
|
+
export interface Action<I, R> {
|
|
35
|
+
readonly data: Resource<R>['data'];
|
|
36
|
+
readonly error: Resource<R>['error'];
|
|
37
|
+
readonly status: Resource<R>['status'];
|
|
38
|
+
readonly running: Resource<R>['loading'];
|
|
39
|
+
/** Runs it. Never rejects: failure is reported through `error`. */
|
|
40
|
+
run(input: I): Promise<R | undefined>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Changes something, and says what it changed.
|
|
44
|
+
*
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const rename = useAction(async (input: Rename, { signal, invalidates }) => {
|
|
47
|
+
* const changed = await patch(input, signal);
|
|
48
|
+
* invalidates(...changed.tags); // the server knows which user that was
|
|
49
|
+
* return changed.user;
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function useAction<I, R>(run: (input: I, context: ActionContext) => Promise<R>): Action<I, R>;
|
|
54
|
+
export interface ObservableLike<T> {
|
|
55
|
+
subscribe(observer: {
|
|
56
|
+
next?: (value: T) => void;
|
|
57
|
+
error?: (error: unknown) => void;
|
|
58
|
+
}): {
|
|
59
|
+
unsubscribe: () => void;
|
|
60
|
+
} | (() => void);
|
|
61
|
+
}
|
|
62
|
+
export interface BridgeOptions {
|
|
63
|
+
/** What `reload()` should do, if the source can do it. */
|
|
64
|
+
readonly reload?: () => Promise<unknown>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A source that pushes, as a resource.
|
|
68
|
+
*
|
|
69
|
+
* For the case a per-call-site resource is the wrong shape: the same entity in
|
|
70
|
+
* twenty places, which must stay consistent. A normalising client already
|
|
71
|
+
* solves that, and this makes its observable a cell — one write in its cache,
|
|
72
|
+
* twenty views updated together.
|
|
73
|
+
*
|
|
74
|
+
* The contract is the smallest one every client satisfies: `subscribe` with a
|
|
75
|
+
* `next` and an `error`, returning either an unsubscribe function or something
|
|
76
|
+
* carrying one. Apollo, urql, RxJS and TanStack's `QueryObserver` all do.
|
|
77
|
+
*/
|
|
78
|
+
export declare function fromObservable<T>(source: ObservableLike<T>, options?: BridgeOptions): Resource<T>;
|
|
79
|
+
/** A promise, as a resource. The loader form without tags or dependencies. */
|
|
80
|
+
export declare function fromPromise<T>(factory: () => Promise<T>): Resource<T>;
|
|
81
|
+
export type { LoadContext, ActionContext, Resource, Status };
|
|
82
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAiB,KAAK,GAAG,EAAE,MAAM,WAAW,CAAC;AAGpD,eAAO,MAAM,WAAW,gDAA6B,CAAC;AAEtD,wBAAgB,OAAO,IAAI,SAAS,CAEnC;AAED,6DAA6D;AAC7D,wBAAgB,aAAa,IAAI,CAAC,GAAG,QAAQ,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAGrE;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1C,OAAO,GAAE,eAAoB,GAC5B,QAAQ,CAAC,CAAC,CAAC,CAqGb;AAED,MAAM,WAAW,MAAM,CAAC,CAAC,EAAE,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACzC,mEAAmE;IACnE,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;CACvC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAC5B,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GACpD,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAmDd;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,SAAS,CAAC,QAAQ,EAAE;QAClB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;KAClC,GAAG;QAAE,WAAW,EAAE,MAAM,IAAI,CAAA;KAAE,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAC9B,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE,aAAkB,GAC1B,QAAQ,CAAC,CAAC,CAAC,CAoCb;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAErE;AAQD,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC"}
|