@tailorkit/app 0.1.0-beta.20 → 0.1.0-beta.22

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,184 +1,5 @@
1
- # @tailorkit/app
1
+ # TailorKit App
2
2
 
3
- Apache-2.0 app SDK. The implementation lives here; `tailorkit/client` reexports the browser API and `tailorkit/server` reexports backend definitions. The package root also exports `defineServer`. Define SQLite tables, synchronous queries/mutations and async actions with Zod arguments. Drizzle and oRPC v2 run underneath; app code uses this package's APIs.
3
+ The SDK for building TailorKit extensions.
4
4
 
5
- ```ts
6
- import { defineServer, table, text, tk } from "tailorkit/server";
7
- import { z } from "zod";
8
-
9
- const notes = table("notes", { id: text().primaryKey(), title: text().notNull() });
10
- const functions = {
11
- list: tk.query.handler(({ db }) => db.select().from(notes).all()),
12
- add: tk.mutation
13
- .input(z.object({ title: z.string().min(1) }))
14
- .handler(({ db, input }) =>
15
- db.insert(notes).values({ id: crypto.randomUUID(), title: input.title }).returning().all(),
16
- ),
17
- };
18
- export default defineServer(functions);
19
- ```
20
-
21
- Use `tk.query`, `tk.mutation` and `tk.action` with `.handler(fn)`. Both `.input(schema)` and `.output(schema)` are optional and can be chained in either order. Without `.input()`, handler `input` is `undefined` and callers omit the input; supplying a value is rejected. Without `.output()`, return values are allowed and their types are inferred from the handler, including awaited action results. There is no default output schema. When supplied, `.output()` constrains handler returns in TypeScript and validates them at runtime; nonconforming results throw `Invalid function result`. Output transformations determine the types returned to clients and action callbacks. Builders are immutable. The original object-form `query`, `mutation` and `action` helpers remain supported.
22
-
23
- Chained handlers receive validated `input`, `db`, and verified `identity` (`userId`, `projectId`, `appId`, `installationId`, `deploymentId`, `expiresAt`). Queries expose selection builders; mutations additionally expose insert/update/delete. Use synchronous `.all()`, `.get()` and `.run()`. Async query/mutation handlers are rejected: writes, output validation and the accepted-write receipt commit in one synchronous transaction. `.output(schema)` validates the output before commit. Throw `AppError` for an intentional public error; unexpected errors are hidden.
24
-
25
- Schema exports include `table`, `text`, `integer`, `real`, `boolean` and common comparison/order operators. This is a small supported surface. Apps can import Drizzle for advanced configurations, but those features are not guaranteed to work with tracking or this driver.
26
-
27
- ## Queries, mutations and actions
28
-
29
- | Type | Database access | External calls | Automatic query reruns |
30
- | -------- | --------------------------------------- | -------------- | ------------------------------------------- |
31
- | Query | Read only | Blocked | When subscribed |
32
- | Mutation | Read/write in one atomic transaction | Blocked | Invalidates affected queries after commit |
33
- | Action | Through `ctx.queries` / `ctx.mutations` | Async HTTPS | One-off calls; no subscription invalidation |
34
-
35
- Use `tk.action` for external API calls. Action handlers receive validated `input`, verified `identity`, an abort `signal`, and typed `ctx.queries.<name>(args)` / `ctx.mutations.<name>(args)` methods. They have no `db` handle. Actions can be synchronous or async; query/mutation handlers remain synchronous.
36
-
37
- Pass a shared query/mutation collection to `.functions(functions)`, and register that same collection under the same names in `defineServer`. The namespaces expose only the declared queries and mutations; their argument and result types are inferred from the definitions. No generated client references are needed on the server. Actions with no database calls can omit `.functions()`.
38
-
39
- To extend the example above, add this action and include it in the app:
40
-
41
- ```ts
42
- const importNote = tk.action
43
- .functions(functions)
44
- .input(z.object({ url: z.url() }))
45
- .handler(async (ctx) => {
46
- await ctx.queries.list();
47
- const response = await fetch(ctx.input.url, { signal: ctx.signal });
48
- const data = z.object({ title: z.string() }).parse(await response.json());
49
- return ctx.mutations.add({ title: data.title });
50
- });
51
- export default defineServer({ ...functions, importNote });
52
- ```
53
-
54
- Clients call `await client.action(api.importNote, { url })`. Actions are one-shot calls, never subscriptions, and are not automatically retried. Every query/mutation call executes separately; the action as a whole is not a transaction. An earlier mutation stays committed if later external work or result validation fails. Combine related database writes into one mutation when they must be atomic. External writes need the provider's idempotency mechanism before retrying an action. Mutation methods accept an optional second argument `{ requestId }`, for example `ctx.mutations.add({ title }, { requestId })`, to reuse the normal mutation receipt when explicitly retrying a database step.
55
-
56
- Cloudflare runs queries, mutations and actions in one dynamic SQLite facet. The server runtime installs an ALS-backed global fetch guard before importing app modules. Ordinary fetch works in actions, including SDKs that capture fetch at import time; queries and mutations reject it. This is a DX guard, not a security boundary between handlers. Actions call queries and mutations directly in the same facet, with fresh database contexts and no application call-count limit. A scoped RPC capability handles outbound HTTP and reports committed tables to refresh subscriptions before the action continues. Platform credentials remain trusted, native networking stays disabled, and external I/O does not hold the database queue. Actions share module state with other handlers.
57
-
58
- Cloudflare CPU and subrequest limits still apply (10 ms CPU and 64 worker subrequests). Local database calls do not consume RPC subrequests. There are no application concurrency or call-count quotas or separate action timeout; token expiry bounds action lifetime. The SDK stops its database methods after completion or cancellation, and database execution checks token expiry. Publication is checked when admitting external calls; deployment replacement cancels active actions. Already committed writes and accepted external effects remain committed. Actions are not durable jobs. Nested action calls, scheduling, internal-only functions and app-secret management are deferred.
59
-
60
- ## Source layout
61
-
62
- App projects keep UI views in `src/views/`, backend implementations in `src/functions/`, and database tables in `src/schema.ts`. `src/server.ts` imports functions and registers them with `defineServer`; `src/client.ts` registers views and configures the root provider. `tailorkit init` generates this layout, a sample query, and the provider setup. With the umbrella package, import backend definitions from `tailorkit/server` and Preact hooks from `tailorkit/client`.
63
-
64
- Export the function type from `src/server.ts`:
65
-
66
- ```ts
67
- const app = defineServer({ ...functions, importNote });
68
- export default app;
69
- ```
70
-
71
- The shared `#tailorkit` module exposes a browser API inferred from that type:
72
-
73
- ```ts
74
- import { createApi } from "tailorkit/client";
75
- import type app from "./server";
76
-
77
- export const api = createApi<typeof app.functions>();
78
- ```
79
-
80
- Views import `api` from `#tailorkit`. Adding functions or changing their input/output schemas updates types immediately through the type-only import: no backend type generation or build is needed. `createApi` creates references by function name without importing or evaluating server implementations. Init sets up this link. Host schema generation includes it when the configured server entry exists (default `src/server.ts`), including default detection with no server configuration. It resolves the import relative to the generated file, including custom output paths. Missing server files omit the backend API export. Enable backend builds with `server: {}` in config. Client and server entry points default to `src/client.ts` and `src/server.ts`. Configure `client.entry` and `server.entry` in `tailorkit.config.ts` to change them; paths are relative to that config file. API type generation and browser import protection follow the configured server entry. CLI entry overrides are unsupported.
81
-
82
- The SDK itself uses these directories:
83
-
84
- - `src/database/`: SQLite schema builders, database types and the tracked Drizzle driver.
85
- - `src/server/`: query/mutation/action definitions, atomic database execution, async action execution, realtime coordination and server RPC transport. Tests live beside the implementation.
86
- - `src/client/`: browser WebSocket connection, typed references and session fetching/caching in `session.ts`.
87
- - `src/auth/`: shared token policy, signing/verification, public-key validation and trusted platform key fetching/caching. Change token lifetime and renewal timing in `policy.ts`.
88
-
89
- The root `index.ts`, `client.ts`, `server.ts` and `protocol.ts` files expose the public entry points. `views.ts` implements remote views and `preact.ts` implements the provider and hooks. `errors.ts` is shared by the client and server. Private execution and Cloudflare hosting live in `apps/apps-worker`; JWT helpers live in the private `@tailorkit/api-utils/app-auth` module.
90
-
91
- Auth signing and verification use Effect v4. `issueAppTokenEffect`, `appTokenVerifierEffect` and `createAppRuntimeVerifierEffect` compose with `Effect.gen`; their Promise counterparts run those programs at the caller boundary. The runtime verifier takes configured public JWKS via `publicKeys` and never fetches signing keys. The Cloudflare adapter supplies its `APP_RUNTIME_PUBLIC_KEYS` variable. Signing and verification share Effect's clock, including JWT expiry checks.
92
-
93
- ## Build and upload
94
-
95
- Add `server: {}` to `tailorkit.config.ts` to build the backend from `src/server.ts`. The client builds from `src/client.ts` by default. `client.entry` and `server.entry` override these source entry points while output filenames stay the same. `tailorkit build` emits `.tailorkit/client/client.js` and `.tailorkit/server/server.js`, and copies committed Drizzle migration files into `.tailorkit/migrations/`. Intermediate server entries are created under the output root’s `tmp/` directory and removed after builds or when the watcher closes; `server/` contains the upload artifact. The server exports the application definition and embedded migrations; Cloudflare bootstrap code belongs to the runtime worker. The builder writes no backend reference source files. The browser builder rejects server implementation imports. `build.outDir` and `--out-dir` change the common output root for all artifacts; preview serves only its `client/` directory.
96
-
97
- `tailorkit deploy` gzips the client and private server artifacts before uploading them through platform-issued blob upload URLs. Checksums and content lengths describe the compressed bytes stored in R2. The runtime verifies and decompresses server code before loading it; client delivery serves gzip to browsers that accept it. The platform checks that decoded client bundles fit within 3 MiB before publication, and decoded delivery streams enforce the same limit. Existing uncompressed deployments remain supported. Apps need no Cloudflare credentials. R2 stores them under separate `client/` and `server/` prefixes; only client assets are public. Client-only apps continue to build without a server artifact. Watch mode watches both builds.
98
-
99
- The legacy `app-storage` SDK, `storage` config and storage CLI commands have been removed. Use `server` config and this SDK. The host session endpoint `/backend/session` uses the existing host `authenticate` callback. The platform verifies scope access and resolves the installation and runtime URL automatically; no extra host backend configuration is required.
100
-
101
- ## Preact hooks
102
-
103
- Configure the provider once in `src/client.ts`:
104
-
105
- ```ts
106
- import { ClientProvider, defineClient } from "tailorkit/client";
107
- import defaultView from "./views/default";
108
-
109
- export default defineClient({
110
- component: ClientProvider,
111
- slots: { panel: { "/": defaultView } },
112
- });
113
- ```
114
-
115
- Views use `useQuery(api.list)`, `useMutation(api.add)` and `useAction(api.importNote)` from `tailorkit/client`. Queries return `data`, `error`, `status`, `isLoading`, `isPending`, `isSuccess` and `isError`. TanStack Store manages query and call state, with selectors keeping unrelated query updates from rerendering other observers. Matching active queries share one live subscription; inline input objects do not cause resubscription when their values are unchanged. Changing inputs clears stale data, fresh snapshots clear subscription errors, and unmounting the last consumer unsubscribes. `{ enabled: false }` as the third argument disables a query.
116
-
117
- Mutations expose `mutate(input)` and `mutateAsync(input)`; actions expose `execute(input)` and `executeAsync(input)`. Both expose `data`, `error`, `status`, `isPending`, `isSuccess`, `isError` and `reset()`, and accept `{ onSuccess, onError }` callbacks. Calls start only when invoked. The latest invocation determines the displayed state, and late responses cannot overwrite a newer invocation or a reset. Fire-and-forget methods capture errors in hook state; async methods additionally reject for caller-controlled workflows. Mutations automatically retry connection failures and temporary backend unavailability with exponential backoff from one second up to thirty seconds, retaining their request ID and original arguments. They stay pending during retries and stop when the client closes or a permanent error occurs. Rejected authentication triggers one session refresh before failing. Actions are never automatically retried.
118
-
119
- `ClientProvider` creates and closes its client automatically. A custom wrapper can pass a caller-owned `client` and/or a global `onError` callback; caller-owned clients remain open when the provider unmounts. The root `component` stays mounted when the host switches views and unmounts when the app root is cleared. No manual client creation, subscriptions or effect cleanup is needed in views.
120
-
121
- ## Imperative client
122
-
123
- ```ts
124
- import { createClient } from "tailorkit/client";
125
- import { api } from "#tailorkit";
126
-
127
- const client = createClient();
128
- const stop = client.subscribe(api.list, undefined, (notes) => render(notes));
129
- await client.mutate(api.add, { title: "Shared with every active client" });
130
- stop(); // Abort this query stream and unregister its subscription.
131
- client.close(); // Close the client's connection when it is no longer needed.
132
- ```
133
-
134
- Shared oRPC fetch procedures carry one-off calls; an oRPC WebSocket carries full query snapshots. The SDK derives its client types from the public oRPC contract in `@tailorkit/app/protocol`. Iterator cancellation unregisters subscriptions. The sandbox bridge supplies scoped JWTs only; backend traffic connects directly to the host-authorized runtime origin. Platform-issued tokens last five minutes. The host SDK caches sessions until one minute before expiry. The socket reconnects one minute before expiry, resubscribes active queries and receives fresh snapshots. No subscription pings or per-query expiry leases are required. A broken connection drops server subscriptions; reconnect creates new subscriptions. Inactive/unsubscribed queries receive no updates.
135
-
136
- Automatic mutation retries reuse the original UUID. The atomic receipt returns the original result without writing or invalidating again. For explicit retries across separate `client.mutate` calls, retain a UUID and pass `{ requestId }` to each call. Reusing that UUID with different arguments or identity is rejected. Retry delays can be configured through `createClient({ retryDelayMs })`; query subscriptions use this delay too.
137
-
138
- ## Reactivity and provider boundary
139
-
140
- The Drizzle driver records tables read by each query, including empty selections and joins, and tables changed by successful writes. Only committed writes invalidate. The supervisor reruns affected active queries as their original verified users and replaces each dependency set. Initial subscription registration and mutations share a queue, avoiding an initial-snapshot race. Slow consumers retain the latest complete pending snapshot.
141
-
142
- The host provides the `/rpc` base URL. `client.query`, `client.mutate` and `client.action` POST to `/rpc/queries`, `/rpc/mutations` and `/rpc/actions` respectively, with the session JWT in the Authorization header. `client.subscribe` opens `/rpc/queries` as a WebSocket and reconnects before token expiry. The trusted installation Durable Object owns subscriptions and invalidates them after both client and action mutations. The isolated SQLite facet returns a serializable response, touched tables and commit status over direct RPC, including table metadata on errors. Rebuild existing clients alongside server bundles when adopting these routes.
143
-
144
- Nested groups are supported in `defineServer({ todos: { list, add }, tasks: { importTodos } })` and inferred by `createApi<typeof app.functions>()`. Calls such as `client.query(api.todos.list)` preserve the wire name `todos.list`. An action declaring `.functions({ todos: { list, add } })` receives `ctx.queries.todos.list()` and `ctx.mutations.todos.add(...)`.
145
-
146
- `@tailorkit/app/protocol` defines the public oRPC contract and application migration input types. The private BUSL-1.1 worker in `apps/apps-worker` owns execution, ALS and the fetch guard, database transactions and receipts, authentication, subscriptions and Cloudflare hosting. The public Apache-2.0 SDK contains app definitions, schema helpers, clients, hooks and build tooling. App authors and browser clients do not need Effect.
147
-
148
- ## Local validation and current limits
149
-
150
- ```sh
151
- pnpm --filter @tailorkit/app build
152
- pnpm --filter tailorkit build
153
- pnpm --filter backend-todo build
154
- pnpm --filter @tailorkit/app test
155
- pnpm --filter @tailorkit/apps-worker test
156
- ```
157
-
158
- The runtime test runs disposable local Wrangler/workerd/R2 state and two clients against the example's real backend, including an external API action and writes continuing while it waits. It checks persistent writes, atomic replay protection, realtime snapshots, JWT renewal and recovery after restart. It needs no Cloudflare account and deploys nothing. See `examples/apps/backend-todo` and the runtime README.
159
-
160
- ## Database migrations
161
-
162
- Generate app migrations with Drizzle Kit v1 and commit the generated files. `table()` defines a Drizzle-compatible schema; schema changes take effect through these migrations.
163
-
164
- ```sh
165
- pnpm db:generate
166
- # Or run directly, with an optional migration name:
167
- pnpm exec tailorkit db generate --name add_notes
168
- ```
169
-
170
- Run `pnpm db:generate` after changing `src/schema.ts`, review and commit the generated migrations, then build and deploy normally. Init and the todo examples include matching Drizzle v1 development dependencies and this script. No `drizzle.config.ts` is needed: generation uses SQLite, `src/schema.ts`, and root `migrations/` by default. `tailorkit.config.ts` is the only framework config; `server.migrations` changes the directory for both generation and builds. No database connection is needed to generate migrations. Drizzle handles generation and rename prompts; TailorKit supplies the paths.
171
-
172
- The builder reads Drizzle v1's `migrations/<timestamp>_<name>/migration.sql` folders, splits their statement breakpoints and embeds the SQL with SHA-256 checksums in the private server artifact. To use another directory, set `server.migrations` in `tailorkit.config.ts`. An explicitly configured directory must exist. An absent default `./migrations` directory is allowed only when there are no schema tables to migrate. Older `_journal.json` migration layouts are unsupported.
173
-
174
- Before bundling, the builder uses Drizzle Kit to generate a fresh snapshot of `src/schema.ts` in a temporary directory and compares its schema entities with the latest committed migration snapshot. Missing or stale migrations fail the build with a `tailorkit db generate` instruction. This check does not write to migration history, connect to a database, or check deployed database state. Generating migrations stays an explicit step so SQL can be reviewed. The builder watches the schema and migration files in watch mode.
175
-
176
- When an installation's SQLite facet starts, the framework synchronously checks its `tailorkit_migrations` journal and applies missing migrations before exposing queries or mutations. The entire pending upgrade and its journal entries run in one storage transaction. Failed upgrades roll back and prevent that facet from serving database calls. Each installation upgrades independently when accessed, including installations created after deployment. Actions that call queries or mutations use this same initialized facet.
177
-
178
- Migration history is append-only: never edit, delete or insert migrations before already applied entries. Their IDs and checksums must match the stored history. Republishing code with an older migration history is rejected; automatic database downgrades are unsupported. Keep migrations small enough for the runtime's existing 10 ms CPU limit; large backfills need a separate workflow. Existing databases created outside this migration flow are not automatically adopted.
179
-
180
- Subscriptions use oRPC with Durable Object hibernation. The trusted installation persists query registrations and dependencies, restores them after wake-up, and uses durable alarms for token expiry. Each installation serializes database calls and query reruns in its trusted realtime coordinator; action database calls pass through the trusted bridge into that same coordinator, with limits of 128 connections and 256 active queries. CPU/subrequest limits apply to isolated app code; installation quotas and receipt retention remain future work. It supports one live deployment per installation, full snapshots and table-level tracking. Offline sync, optimistic updates, middleware, Redis notification delivery and advanced dependency tracking are deferred.
181
-
182
- Application bundles export a `runtimeManifest` alongside the default application definition and migrations. The SDK builder emits `{ apiVersion: 1, requires: ["database", "actions"] }`. The facet validates the exports, API version and required features before touching SQLite. Unsupported bundles fail with `INCOMPATIBLE_VERSION`. Breaking handler/module changes increment `apiVersion`; additive capabilities can be named in `requires`. Bundles only require a runtime upgrade when their declared interface or required features are unsupported. The shared `ApplicationModule` and `RuntimeManifest` types are exported by `@tailorkit/app/server`.
183
-
184
- Server builds inline dynamic imports into the single uploaded `server/server.js` file. Deployment type checking covers both configured client and server entries. Existing mounted clients reconnect after server updates without losing their UI state; server changes must remain compatible with those clients.
5
+ Apps require Preact 11. Install `preact@^11` alongside the SDK.
package/dist/builder.js CHANGED
@@ -75,7 +75,7 @@ function validateLogoAsset(content, contentType) {
75
75
  import { build as viteBuild } from "vite";
76
76
 
77
77
  // src/preact-version.ts
78
- var MINIMUM_PREACT_MAJOR_VERSION = 10;
78
+ var MINIMUM_PREACT_MAJOR_VERSION = 11;
79
79
  var parsePreactVersion = (version) => {
80
80
  const match = /^(\d+)\./u.exec(version);
81
81
  if (!match?.[1]) {
@@ -1086,7 +1086,7 @@ function getInstalledPreactVersion(root) {
1086
1086
  }
1087
1087
  } catch (error) {
1088
1088
  throw new Error(
1089
- `TailorKit requires Preact to build an app. Install preact@^10 and try again.`,
1089
+ `TailorKit requires Preact to build an app. Install preact@^11 and try again.`,
1090
1090
  { cause: error }
1091
1091
  );
1092
1092
  }
@@ -6,7 +6,7 @@ import {
6
6
  // src/views.ts
7
7
  import { createContext, h, render } from "preact";
8
8
  import { useContext } from "preact/hooks";
9
- var preactVersion = "10.29.8";
9
+ var preactVersion = "11.0.0";
10
10
  var defineView = (options) => {
11
11
  const path = options.view;
12
12
  const Context2 = createContext(
@@ -106,10 +106,10 @@ var createRemoteComponent = (name, options = {}) => {
106
106
  };
107
107
 
108
108
  // src/preact.ts
109
- import { createStore as createStore2 } from "@tanstack/store";
110
- import { useSelector } from "@tanstack/preact-store";
109
+ import { atom as atom2, computed } from "nanostores";
110
+ import { useStore } from "@nanostores/preact";
111
111
  import { createContext as createContext2, h as h2 } from "preact";
112
- import { useCallback, useContext as useContext2, useEffect, useMemo, useRef } from "preact/hooks";
112
+ import { useCallback, useContext as useContext2, useEffect, useLayoutEffect, useMemo, useRef } from "preact/hooks";
113
113
 
114
114
  // src/client/connection.ts
115
115
  import { createORPCClient } from "@orpc/client";
@@ -408,7 +408,7 @@ function waitForRetry(signal, delayMs) {
408
408
  }
409
409
 
410
410
  // src/client/query-store.ts
411
- import { createStore } from "@tanstack/store";
411
+ import { atom } from "nanostores";
412
412
  var pendingQuery = { data: void 0, error: null, status: "pending" };
413
413
  function serializeInput(input) {
414
414
  return JSON.stringify({ input, defined: input !== void 0 }, (_key, value) => {
@@ -419,7 +419,7 @@ function serializeInput(input) {
419
419
  });
420
420
  }
421
421
  var QueryStore = class {
422
- state = createStore(/* @__PURE__ */ new Map());
422
+ state = atom(/* @__PURE__ */ new Map());
423
423
  entries = /* @__PURE__ */ new Map();
424
424
  disposed = false;
425
425
  client;
@@ -442,7 +442,7 @@ var QueryStore = class {
442
442
  current.observers += 1;
443
443
  const publish = (state) => {
444
444
  if (!this.disposed && this.entries.get(key) === current) {
445
- this.state.setState((previous) => new Map(previous).set(key, state));
445
+ this.state.set(new Map(this.state.get()).set(key, state));
446
446
  }
447
447
  };
448
448
  if (first) {
@@ -474,11 +474,9 @@ var QueryStore = class {
474
474
  if (current.observers === 0 && this.entries.get(key) === current) {
475
475
  this.entries.delete(key);
476
476
  current.stop?.();
477
- this.state.setState((previous) => {
478
- const next = new Map(previous);
479
- next.delete(key);
480
- return next;
481
- });
477
+ const next = new Map(this.state.get());
478
+ next.delete(key);
479
+ this.state.set(next);
482
480
  }
483
481
  };
484
482
  }
@@ -490,7 +488,7 @@ var QueryStore = class {
490
488
  entry.stop?.();
491
489
  entry.stop = void 0;
492
490
  }
493
- this.state.setState(() => /* @__PURE__ */ new Map());
491
+ this.state.set(/* @__PURE__ */ new Map());
494
492
  if (this.ownsClient) {
495
493
  this.client.close();
496
494
  }
@@ -511,10 +509,10 @@ function ClientProvider({ children, client, onError }) {
511
509
  () => new QueryStore(client ?? createClient(), !client, (error) => errorHandler.current?.(error)),
512
510
  [client]
513
511
  );
514
- useEffect(() => () => store.dispose(), [store]);
512
+ useLayoutEffect(() => () => store.dispose(), [store]);
515
513
  return h2(Context.Provider, { value: store }, children);
516
514
  }
517
- function useStore() {
515
+ function useQueryStore() {
518
516
  const store = useContext2(Context);
519
517
  if (!store) {
520
518
  throw new Error("TailorKit backend hooks require a ClientProvider at the app root.");
@@ -522,17 +520,21 @@ function useStore() {
522
520
  return store;
523
521
  }
524
522
  function useQuery(ref, ...[input, options]) {
525
- const store = useStore();
523
+ const store = useQueryStore();
526
524
  const enabled = options?.enabled ?? true;
527
525
  const name = ref.name;
528
526
  const serialized = serializeInput(input);
529
527
  const key = `${name}\0${serialized}`;
530
528
  const stableInput = useMemo(() => JSON.parse(serialized).input, [serialized]);
531
529
  const stableReference = useMemo(() => reference(name, "query"), [name]);
532
- const state = useSelector(
533
- store.state,
534
- (queries) => enabled ? queries.get(key) ?? pendingQuery : pendingQuery
530
+ const query = useMemo(
531
+ () => computed(
532
+ store.state,
533
+ (queries) => enabled ? queries.get(key) ?? pendingQuery : pendingQuery
534
+ ),
535
+ [store, key, enabled]
535
536
  );
537
+ const state = useStore(query);
536
538
  useEffect(() => {
537
539
  if (enabled) {
538
540
  return store.watch(key, stableReference, stableInput);
@@ -548,25 +550,25 @@ function useQuery(ref, ...[input, options]) {
548
550
  }
549
551
  var idleCall = { data: void 0, error: null, status: "idle" };
550
552
  function useCall(kind, ref, options) {
551
- const store = useStore();
553
+ const store = useQueryStore();
552
554
  const name = ref.name;
553
555
  const identity = useMemo(
554
556
  () => ({
555
557
  store,
556
558
  name,
557
559
  kind,
558
- state: createStore2(idleCall)
560
+ state: atom2(idleCall)
559
561
  }),
560
562
  [store, name, kind]
561
563
  );
562
- const state = useSelector(identity.state);
564
+ const state = useStore(identity.state);
563
565
  const current = useRef({ identity, sequence: 0, mounted: true });
564
566
  if (current.current.identity !== identity) {
565
567
  current.current = { identity, sequence: current.current.sequence + 1, mounted: true };
566
568
  }
567
569
  const callbacks = useRef(options);
568
570
  callbacks.current = options;
569
- useEffect(() => {
571
+ useLayoutEffect(() => {
570
572
  current.current.mounted = true;
571
573
  return () => {
572
574
  current.current.mounted = false;
@@ -579,21 +581,21 @@ function useCall(kind, ref, options) {
579
581
  const settings = callbacks.current;
580
582
  const args = input;
581
583
  const isCurrent = () => current.current.mounted && current.current.identity === identity && current.current.sequence === sequence;
582
- identity.state.setState(() => ({ ...idleCall, status: "pending" }));
584
+ identity.state.set({ ...idleCall, status: "pending" });
583
585
  let data;
584
586
  try {
585
587
  data = kind === "mutation" ? await store.client.mutate(reference(name, "mutation"), args) : await store.client.action(reference(name, "action"), args);
586
588
  } catch (error) {
587
589
  const failure = appError(error);
588
590
  if (isCurrent()) {
589
- identity.state.setState(() => ({ data: void 0, error: failure, status: "error" }));
591
+ identity.state.set({ data: void 0, error: failure, status: "error" });
590
592
  settings.onError?.(failure, args);
591
593
  }
592
594
  store.onError(failure);
593
595
  throw failure;
594
596
  }
595
597
  if (isCurrent()) {
596
- identity.state.setState(() => ({ data, error: null, status: "success" }));
598
+ identity.state.set({ data, error: null, status: "success" });
597
599
  settings.onSuccess?.(data, args);
598
600
  }
599
601
  return data;
@@ -609,7 +611,7 @@ function useCall(kind, ref, options) {
609
611
  );
610
612
  const reset = useCallback(() => {
611
613
  current.current.sequence += 1;
612
- identity.state.setState(() => idleCall);
614
+ identity.state.set(idleCall);
613
615
  }, [identity]);
614
616
  return {
615
617
  ...state,
@@ -10,7 +10,7 @@ export declare const pendingQuery: QueryState<never>;
10
10
  export declare function serializeInput(input: unknown): string;
11
11
  /** One subscription per active query/input pair, scoped to this provider's client. */
12
12
  export declare class QueryStore {
13
- readonly state: import("@tanstack/store").Store<Map<string, QueryState<unknown>>, never>;
13
+ readonly state: import("nanostores").PreinitializedWritableAtom<Map<string, QueryState<unknown>>> & object;
14
14
  private readonly entries;
15
15
  private disposed;
16
16
  readonly client: Client;
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  useAction,
11
11
  useMutation,
12
12
  useQuery
13
- } from "./chunk-RHCGWGSG.js";
13
+ } from "./chunk-LQJEPSUZ.js";
14
14
  import {
15
15
  AppError,
16
16
  appError
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  useAction,
11
11
  useMutation,
12
12
  useQuery
13
- } from "./chunk-RHCGWGSG.js";
13
+ } from "./chunk-LQJEPSUZ.js";
14
14
  import {
15
15
  action,
16
16
  and,
@@ -1,4 +1,4 @@
1
- export declare const MINIMUM_PREACT_MAJOR_VERSION = 10;
1
+ export declare const MINIMUM_PREACT_MAJOR_VERSION = 11;
2
2
  export interface PreactVersionCheck {
3
3
  major: number;
4
4
  version: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailorkit/app",
3
- "version": "0.1.0-beta.20",
3
+ "version": "0.1.0-beta.22",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,12 +45,12 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
+ "@nanostores/preact": "1.1.0",
48
49
  "@orpc/client": "2.0.0-beta.40",
49
50
  "@orpc/contract": "2.0.0-beta.40",
50
- "@tanstack/preact-store": "0.13.4",
51
- "@tanstack/store": "0.11.2",
52
51
  "cosmiconfig": "^10.0.1",
53
52
  "drizzle-orm": "1.0.0-rc.4-5d5b77c",
53
+ "nanostores": "1.5.4",
54
54
  "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
55
55
  "zod": "^4.6.5"
56
56
  },
@@ -59,13 +59,13 @@
59
59
  "@tailorkit/config": "0.0.0",
60
60
  "@types/node": "26.6.4",
61
61
  "happy-dom": "^20.14.5",
62
- "preact": "^10.29.8",
62
+ "preact": "^11.0.0",
63
63
  "tsup": "^8.5.1",
64
64
  "typescript": "^7.0.2",
65
65
  "vite-plus": "1.0.0"
66
66
  },
67
67
  "peerDependencies": {
68
- "preact": "^10.0.0"
68
+ "preact": "^11.0.0"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsup --config tsup.config.ts && tsc -p tsconfig.build.json",