@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.
Files changed (3) hide show
  1. package/README.md +20 -14
  2. package/lib/index.mjs +24 -13
  3. 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; choose Vue in the selector or pass `--framework vue`.
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, saved reads and `persist: false` calls still work, but new durable mutations fail before being queued.
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`, `field`, and `count` to describe the hidden data needed to resolve a public field:
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, field } from '@nkzw/fate/server';
3115
+ import { computed, count } from '@nkzw/fate/server';
3116
3116
 
3117
3117
  export const userDataView = dataView<UserItem>('User')({
3118
- email: computed<UserItem, string | null, AppContext>({
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: field('email'),
3121
+ email: true,
3122
3122
  },
3123
- resolve: (_item, deps) => (deps.email as string | null) ?? null,
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<PostItem, number>({
3129
+ commentCount: computed({
3130
3130
  select: {
3131
3131
  count: count('comments'),
3132
3132
  },
3133
- resolve: (_item, deps) => (deps.count as number) ?? 0,
3133
+ resolve: (_item: PostItem, { count }) => count,
3134
3134
  }),
3135
3135
  id: true,
3136
3136
  });
3137
3137
  ```
3138
3138
 
3139
- The adapters fetch the hidden `field(...)` and `count(...)` dependencies for you. This keeps private fields like `email` available to the resolver without exposing them to the client selection.
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
- return navigator.locks.request(`fate:${JSON.stringify([name, key])}`, async () => {
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 entries = [];
68
- let cursor = await transaction.store.openCursor(range);
69
- while (cursor && String(cursor.key).startsWith(prefix) && entries.length < limit) {
70
- entries.push({
71
- key: String(cursor.key),
72
- value: cursor.value
73
- });
74
- cursor = await cursor.continue();
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.15.0",
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.15.0",
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"