@nkzw/fate-indexeddb 1.15.0 → 1.16.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 +20 -14
- package/lib/index.mjs +24 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -69,7 +69,9 @@ vp create fate
|
|
|
69
69
|
|
|
70
70
|
Explore the [fate stack](https://fate.technology/#stack) to see the tools included in your new project.
|
|
71
71
|
|
|
72
|
-
Choose a backend in the template selector: Void, tRPC with Drizzle or Prisma, GraphQL with Prisma, or a client for an existing GraphQL server. React is the default
|
|
72
|
+
Choose a backend in the template selector: Void, tRPC with Drizzle or Prisma, GraphQL with Prisma, or a client for an existing GraphQL server. React is the default. Choose Vue in the selector or pass `--framework vue`.
|
|
73
|
+
|
|
74
|
+
Choose **Client Only** or pass `--template client-only` to start with a static Void app that saves its data in IndexedDB. See [Persisting Client State](/docs/guide/client-state.md#persisting-client-state).
|
|
73
75
|
|
|
74
76
|
### Manual Installation
|
|
75
77
|
|
|
@@ -880,8 +882,6 @@ For GraphQL servers with incremental delivery enabled, [streaming batched reads]
|
|
|
880
882
|
|
|
881
883
|
You can wrap a list of references using `useListView` to enable connection-style lists with pagination support.
|
|
882
884
|
|
|
883
|
-
Run the regular example with `vp run dev` (tRPC) or `vp run dev:graphql` (GraphQL), then open **Pagination** from its header (`http://localhost:6001/pagination/preview`). The demo uses the existing posts, client, and router to exercise independent view ranges, navigation, refreshes, and optimistic insertion and rollback. Reading and pagination work without signing in; insertions require an example account and successful insertions create real posts in its database. Browser network throttling makes pending work easier to observe.
|
|
884
|
-
|
|
885
885
|
For example, you can define a `CommentView` and reuse it inside of a `CommentConnectionView`:
|
|
886
886
|
|
|
887
887
|
```tsx
|
|
@@ -1606,7 +1606,7 @@ Pending mutations are never evicted to make room for cached data. If a new mutat
|
|
|
1606
1606
|
|
|
1607
1607
|
### Actions & Mutations
|
|
1608
1608
|
|
|
1609
|
-
With persistence configured, fate saves mutations before applying their optimistic updates or sending them to the server. The API is the same as for regular [Actions](/docs/guide/actions.md):
|
|
1609
|
+
With persistence and a durable mutation transport configured, fate saves server mutations before applying their optimistic updates or sending them to the server. The API is the same as for regular [Actions](/docs/guide/actions.md):
|
|
1610
1610
|
|
|
1611
1611
|
```tsx
|
|
1612
1612
|
const [result, like] = useActionState(fate.actions.post.like, null);
|
|
@@ -1712,7 +1712,7 @@ return idempotency.execute({
|
|
|
1712
1712
|
|
|
1713
1713
|
Forward the entire identity on every attempt. When `identity.replayOnly` is true, the endpoint must return the saved result or a 404 without running the mutation. `createMutationIdempotency` handles this for you.
|
|
1714
1714
|
|
|
1715
|
-
Without a durable adapter,
|
|
1715
|
+
Without a durable adapter, server mutations send immediately by default. Use `persist: true` to require durable delivery. The call fails before sending if persistence or the adapter is missing. Existing queued mutations stay saved and report a delivery error until the adapter is available again. Remote read caching and client state persistence work independently of the adapter.
|
|
1716
1716
|
|
|
1717
1717
|
For GraphQL, return a standard `extensions.code` or numeric `extensions.http.status` for terminal client errors. Unrecognized execution errors remain retryable for saved mutations.
|
|
1718
1718
|
|
|
@@ -3109,34 +3109,34 @@ For Prisma, pass Prisma query options such as `{ where: { ... } }` in `extra` in
|
|
|
3109
3109
|
|
|
3110
3110
|
### Data View Resolvers
|
|
3111
3111
|
|
|
3112
|
-
fate data views support computed fields. Use `computed
|
|
3112
|
+
fate data views support computed fields. Use `computed` to describe the hidden data needed to resolve a public field.
|
|
3113
3113
|
|
|
3114
3114
|
```tsx
|
|
3115
|
-
import { computed, count
|
|
3115
|
+
import { computed, count } from '@nkzw/fate/server';
|
|
3116
3116
|
|
|
3117
3117
|
export const userDataView = dataView<UserItem>('User')({
|
|
3118
|
-
email: computed
|
|
3119
|
-
authorize: ({ id }, context) => context?.sessionUser?.id === id,
|
|
3118
|
+
email: computed({
|
|
3119
|
+
authorize: ({ id }, context?: AppContext) => context?.sessionUser?.id === id,
|
|
3120
3120
|
select: {
|
|
3121
|
-
email:
|
|
3121
|
+
email: true,
|
|
3122
3122
|
},
|
|
3123
|
-
resolve: (_item,
|
|
3123
|
+
resolve: (_item: UserItem, { email }) => email ?? null,
|
|
3124
3124
|
}),
|
|
3125
3125
|
id: true,
|
|
3126
3126
|
});
|
|
3127
3127
|
|
|
3128
3128
|
export const postDataView = dataView<PostItem>('Post')({
|
|
3129
|
-
commentCount: computed
|
|
3129
|
+
commentCount: computed({
|
|
3130
3130
|
select: {
|
|
3131
3131
|
count: count('comments'),
|
|
3132
3132
|
},
|
|
3133
|
-
resolve: (_item,
|
|
3133
|
+
resolve: (_item: PostItem, { count }) => count,
|
|
3134
3134
|
}),
|
|
3135
3135
|
id: true,
|
|
3136
3136
|
});
|
|
3137
3137
|
```
|
|
3138
3138
|
|
|
3139
|
-
|
|
3139
|
+
Set a dependency to `true` to select a field by the same name. Use `field('path')` for aliases or nested paths and `count('relation')` for relation counts. The adapters fetch these dependencies for you. This keeps private fields like `email` available to the resolver without exposing them to the client selection.
|
|
3140
3140
|
|
|
3141
3141
|
### Connecting the Client
|
|
3142
3142
|
|
|
@@ -3569,6 +3569,12 @@ After changing your database schema or auth configuration, run `vp run db:genera
|
|
|
3569
3569
|
|
|
3570
3570
|
See [Void's Cloudflare deployment guide](https://void.cloud/integrations/cloudflare) for custom domains, secrets, and CI configuration.
|
|
3571
3571
|
|
|
3572
|
+
### Client-only Apps
|
|
3573
|
+
|
|
3574
|
+
Choose `--template client-only` when creating the app. The template uses Void Pages Routing and prerenders both routes as static shells. IndexedDB stores application data in each browser.
|
|
3575
|
+
|
|
3576
|
+
Run `vp exec void deploy --platform cloudflare` from the generated app to deploy the assets. You can also run `vp run build` and serve `dist/client/` from another static host that supports clean URLs.
|
|
3577
|
+
|
|
3572
3578
|
### Migrating from cf-fate
|
|
3573
3579
|
|
|
3574
3580
|
Fate 1.6 replaces the standalone Cloudflare adapter and template with the Void integration. Use `--template void` for new projects and `transport: 'void'` in the fate Vite plugin.
|
package/lib/index.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { openDB } from "idb";
|
|
2
2
|
// src/index.ts
|
|
3
|
+
const prefixEnd = (prefix) => {
|
|
4
|
+
for (let index = prefix.length - 1; index >= 0; index--) {
|
|
5
|
+
const unit = prefix.charCodeAt(index);
|
|
6
|
+
if (unit < 65535) return prefix.slice(0, index) + String.fromCharCode(unit + 1);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
3
9
|
function createIndexedDBStorage({ name = "fate" } = {}) {
|
|
4
10
|
let connectionLeases = 0;
|
|
5
11
|
let connection;
|
|
@@ -42,7 +48,8 @@ function createIndexedDBStorage({ name = "fate" } = {}) {
|
|
|
42
48
|
return {
|
|
43
49
|
exclusive(key, run) {
|
|
44
50
|
if (typeof navigator === "undefined" || !navigator.locks) throw new Error("fate(indexeddb): Web Locks are required for safe durable writes across tabs.");
|
|
45
|
-
|
|
51
|
+
const lockName = `fate:${JSON.stringify([name, key])}`;
|
|
52
|
+
const request = (lockName) => navigator.locks.request(lockName, async () => {
|
|
46
53
|
connectionLeases++;
|
|
47
54
|
try {
|
|
48
55
|
return await run();
|
|
@@ -50,6 +57,7 @@ function createIndexedDBStorage({ name = "fate" } = {}) {
|
|
|
50
57
|
releaseDatabase();
|
|
51
58
|
}
|
|
52
59
|
});
|
|
60
|
+
return lockName.length <= 1024 ? request(lockName) : crypto.subtle.digest("SHA-256", new TextEncoder().encode(lockName)).then((digest) => request(`fate:sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`));
|
|
53
61
|
},
|
|
54
62
|
async read(key) {
|
|
55
63
|
const db = await acquireDatabase();
|
|
@@ -60,21 +68,24 @@ function createIndexedDBStorage({ name = "fate" } = {}) {
|
|
|
60
68
|
}
|
|
61
69
|
},
|
|
62
70
|
async scan(prefix, after, limit = 64) {
|
|
71
|
+
const lowerOpen = after !== void 0 && after >= prefix;
|
|
72
|
+
const lower = lowerOpen ? after : prefix;
|
|
73
|
+
if (!(limit > 0) || !lower.startsWith(prefix)) return [];
|
|
74
|
+
const upper = prefixEnd(prefix);
|
|
75
|
+
const range = upper === void 0 ? IDBKeyRange.lowerBound(lower, lowerOpen) : IDBKeyRange.bound(lower, upper, lowerOpen, true);
|
|
76
|
+
const count = Math.min(Math.ceil(limit), 4294967295);
|
|
63
77
|
const db = await acquireDatabase();
|
|
64
78
|
try {
|
|
65
|
-
const range = IDBKeyRange.lowerBound(after !== void 0 && after >= prefix ? after : prefix, after !== void 0 && after >= prefix);
|
|
66
79
|
const transaction = db.transaction("fate");
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
await transaction.done;
|
|
77
|
-
return entries;
|
|
80
|
+
const [keys, values] = await Promise.all([
|
|
81
|
+
transaction.store.getAllKeys(range, count),
|
|
82
|
+
transaction.store.getAll(range, count),
|
|
83
|
+
transaction.done
|
|
84
|
+
]);
|
|
85
|
+
return keys.map((key, index) => ({
|
|
86
|
+
key: String(key),
|
|
87
|
+
value: values[index]
|
|
88
|
+
}));
|
|
78
89
|
} finally {
|
|
79
90
|
releaseDatabase();
|
|
80
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nkzw/fate-indexeddb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Optional IndexedDB persistence adapter for fate.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"idb": "^8.0.4"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@nkzw/fate": "1.
|
|
22
|
+
"@nkzw/fate": "1.16.0",
|
|
23
23
|
"fake-indexeddb": "^6.2.7",
|
|
24
24
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.5.0",
|
|
25
25
|
"vite-plus": "1.5.0"
|