@treygrr/nuxt-airplane 0.0.0-stage → 0.1.2

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 (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -2
  3. package/dist/module.d.mts +41 -0
  4. package/dist/module.json +12 -0
  5. package/dist/module.mjs +316 -0
  6. package/dist/runtime/client.d.ts +230 -0
  7. package/dist/runtime/client.js +361 -0
  8. package/dist/runtime/composables/defineAirplaneRequest.d.ts +7 -0
  9. package/dist/runtime/composables/defineAirplaneRequest.js +4 -0
  10. package/dist/runtime/composables/defineAirplaneStores.d.ts +8 -0
  11. package/dist/runtime/composables/defineAirplaneStores.js +4 -0
  12. package/dist/runtime/composables/defineMutationHandler.d.ts +6 -0
  13. package/dist/runtime/composables/defineMutationHandler.js +4 -0
  14. package/dist/runtime/composables/defineMutationHandlers.d.ts +6 -0
  15. package/dist/runtime/composables/defineMutationHandlers.js +4 -0
  16. package/dist/runtime/composables/defineQueryHandler.d.ts +6 -0
  17. package/dist/runtime/composables/defineQueryHandler.js +4 -0
  18. package/dist/runtime/composables/defineQueryHandlers.d.ts +6 -0
  19. package/dist/runtime/composables/defineQueryHandlers.js +4 -0
  20. package/dist/runtime/composables/useAirplane.d.ts +3 -0
  21. package/dist/runtime/composables/useAirplane.js +4 -0
  22. package/dist/runtime/composables/useMutation.d.ts +14 -0
  23. package/dist/runtime/composables/useMutation.js +21 -0
  24. package/dist/runtime/composables/useQuery.d.ts +19 -0
  25. package/dist/runtime/composables/useQuery.js +63 -0
  26. package/dist/runtime/composables/useSync.d.ts +37 -0
  27. package/dist/runtime/composables/useSync.js +48 -0
  28. package/dist/runtime/db.d.ts +26 -0
  29. package/dist/runtime/db.js +109 -0
  30. package/dist/runtime/plugin.client.d.ts +417 -0
  31. package/dist/runtime/plugin.client.js +43 -0
  32. package/dist/runtime/types.d.ts +218 -0
  33. package/dist/runtime/types.js +8 -0
  34. package/dist/types.d.mts +3 -0
  35. package/package.json +96 -4
@@ -0,0 +1,4 @@
1
+ import { HANDLER_KIND } from "../types.js";
2
+ export function defineQueryHandlers(handlers) {
3
+ return Object.defineProperty(handlers, HANDLER_KIND, { value: "queries" });
4
+ }
@@ -0,0 +1,3 @@
1
+ import type { AirplaneClient } from '../client.js';
2
+ /** The airplane client behind every composable. Only available in the browser. */
3
+ export declare function useAirplane(): AirplaneClient | undefined;
@@ -0,0 +1,4 @@
1
+ import { useNuxtApp } from "#app";
2
+ export function useAirplane() {
3
+ return useNuxtApp().$airplane;
4
+ }
@@ -0,0 +1,14 @@
1
+ import { type Ref } from 'vue';
2
+ import type { MutationName, MutationRow, MutationVariables } from '../types.js';
3
+ export interface UseMutationReturn<V> {
4
+ /**
5
+ * Applies the mutation to the local cache and queues it for the server.
6
+ * Resolves with the outbox entry as soon as the change is visible locally;
7
+ * the request itself is sent in the background by the sync loop.
8
+ */
9
+ mutate: (variables: V) => Promise<MutationRow>;
10
+ /** `true` while the local `apply` handler runs. */
11
+ loading: Ref<boolean>;
12
+ error: Ref<Error | null>;
13
+ }
14
+ export declare function useMutation<N extends MutationName>(name: N): UseMutationReturn<MutationVariables<N>>;
@@ -0,0 +1,21 @@
1
+ import { ref, shallowRef } from "vue";
2
+ import { useAirplane } from "./useAirplane.js";
3
+ export function useMutation(name) {
4
+ const loading = ref(false);
5
+ const error = shallowRef(null);
6
+ const client = useAirplane();
7
+ async function mutate(variables) {
8
+ if (!client) throw new Error("[nuxt-airplane] Mutations can only run on the client");
9
+ loading.value = true;
10
+ error.value = null;
11
+ try {
12
+ return await client.enqueue(name, variables);
13
+ } catch (e) {
14
+ error.value = e;
15
+ throw e;
16
+ } finally {
17
+ loading.value = false;
18
+ }
19
+ }
20
+ return { mutate, loading, error };
21
+ }
@@ -0,0 +1,19 @@
1
+ import { type MaybeRefOrGetter, type Ref } from 'vue';
2
+ import type { QueryName, QueryResult, QueryVariables } from '../types.js';
3
+ export interface UseQueryOptions {
4
+ /** Set to `false` to only read the cache, never the network. */
5
+ fetch?: boolean;
6
+ }
7
+ export interface UseQueryReturn<T> {
8
+ /** Live data from the local cache. Updates when mutations touch it. */
9
+ data: Ref<T | null>;
10
+ /** `true` until the cache has been read and (if online) the first fetch is done. */
11
+ loading: Ref<boolean>;
12
+ /** `true` while a network request is in flight. */
13
+ fetching: Ref<boolean>;
14
+ error: Ref<Error | null>;
15
+ /** When the cached data was last fetched from the server, or `null`. */
16
+ updatedAt: Ref<number | null>;
17
+ refresh: () => Promise<void>;
18
+ }
19
+ export declare function useQuery<N extends QueryName>(name: N, variables?: MaybeRefOrGetter<QueryVariables<N> | undefined>, options?: UseQueryOptions): UseQueryReturn<QueryResult<N>>;
@@ -0,0 +1,63 @@
1
+ import { computed, getCurrentInstance, onMounted, onScopeDispose, ref, shallowRef, toValue, watch } from "vue";
2
+ import { queryKey } from "../client.js";
3
+ import { useAirplane } from "./useAirplane.js";
4
+ export function useQuery(name, variables, options = {}) {
5
+ const data = shallowRef(null);
6
+ const loading = ref(true);
7
+ const fetching = ref(false);
8
+ const error = shallowRef(null);
9
+ const updatedAt = ref(null);
10
+ const client = useAirplane();
11
+ if (!client) {
12
+ return { data, loading, fetching, error, updatedAt, refresh: async () => {
13
+ } };
14
+ }
15
+ const vars = computed(() => toValue(variables));
16
+ const key = computed(() => queryKey(name, vars.value));
17
+ async function refresh() {
18
+ if (options.fetch === false || !client.state.online) return;
19
+ fetching.value = true;
20
+ try {
21
+ await client.fetchQuery(name, vars.value);
22
+ error.value = null;
23
+ } catch (e) {
24
+ error.value = e;
25
+ } finally {
26
+ fetching.value = false;
27
+ loading.value = false;
28
+ }
29
+ }
30
+ let stopWatch;
31
+ function subscribe() {
32
+ stopWatch?.();
33
+ const { snapshot, unsubscribe } = client.watchQuery(name, vars.value);
34
+ const stop = watch(snapshot, (s) => {
35
+ data.value = s.data ?? null;
36
+ updatedAt.value = s.updatedAt;
37
+ if (s.cached) loading.value = false;
38
+ }, { immediate: true });
39
+ stopWatch = () => {
40
+ stop();
41
+ unsubscribe();
42
+ };
43
+ }
44
+ watch(key, (next, prev) => {
45
+ client.active.delete(prev);
46
+ client.active.set(next, refresh);
47
+ loading.value = true;
48
+ subscribe();
49
+ void refresh();
50
+ });
51
+ function start() {
52
+ client.active.set(key.value, refresh);
53
+ subscribe();
54
+ void refresh();
55
+ }
56
+ if (getCurrentInstance()) onMounted(start);
57
+ else start();
58
+ onScopeDispose(() => {
59
+ client.active.delete(key.value);
60
+ stopWatch?.();
61
+ });
62
+ return { data, loading, fetching, error, updatedAt, refresh };
63
+ }
@@ -0,0 +1,37 @@
1
+ import { type ComputedRef, type Ref } from 'vue';
2
+ import type { MutationRow } from '../types.js';
3
+ export interface UseSyncReturn {
4
+ /** Effective state: the browser reports a connection and `goOffline()` is not in effect. */
5
+ online: Ref<boolean>;
6
+ /** What the browser reports, ignoring `goOffline()`. */
7
+ networkOnline: Ref<boolean>;
8
+ /** `true` while held offline by `goOffline()`. */
9
+ forcedOffline: Ref<boolean>;
10
+ /** A sync batch is being sent. */
11
+ syncing: Ref<boolean>;
12
+ /** Nothing is queued and nothing is being sent. */
13
+ synced: ComputedRef<boolean>;
14
+ /** Mutations still waiting to reach the server, oldest first. */
15
+ pending: Ref<MutationRow[]>;
16
+ /** Pending mutations held back because an older one failed (see the `stopOnFailure` option). */
17
+ blocked: Ref<number>;
18
+ /** Size of the batch the current (or last) sync started with. */
19
+ syncTotal: Ref<number>;
20
+ /** How many of that batch have been sent so far. */
21
+ syncCount: Ref<number>;
22
+ /** Message of the network error that stopped the last sync, if any. */
23
+ lastError: Ref<string | null>;
24
+ /** Paginated queries currently being fetched to the end for offline use. */
25
+ prefetching: Ref<number>;
26
+ /** Send the queue now. Does nothing while offline. */
27
+ sync: () => Promise<void>;
28
+ /** Undo a queued mutation and remove it from the queue. */
29
+ rollback: (id: number) => Promise<void>;
30
+ /** Put a failed mutation back in the queue and sync. */
31
+ retry: (id: number) => Promise<void>;
32
+ /** Hold the client offline regardless of the network until `goOnline()`. */
33
+ goOffline: () => void;
34
+ /** Release `goOffline()` and sync the queue. */
35
+ goOnline: () => void;
36
+ }
37
+ export declare function useSync(): UseSyncReturn;
@@ -0,0 +1,48 @@
1
+ import { computed, toRef } from "vue";
2
+ import { useAirplane } from "./useAirplane.js";
3
+ export function useSync() {
4
+ const client = useAirplane();
5
+ if (!client) {
6
+ const off = async () => {
7
+ };
8
+ return {
9
+ online: computed(() => true),
10
+ networkOnline: computed(() => true),
11
+ forcedOffline: computed(() => false),
12
+ syncing: computed(() => false),
13
+ synced: computed(() => true),
14
+ pending: computed(() => []),
15
+ blocked: computed(() => 0),
16
+ syncTotal: computed(() => 0),
17
+ syncCount: computed(() => 0),
18
+ lastError: computed(() => null),
19
+ prefetching: computed(() => 0),
20
+ sync: off,
21
+ rollback: off,
22
+ retry: off,
23
+ goOffline: () => {
24
+ },
25
+ goOnline: () => {
26
+ }
27
+ };
28
+ }
29
+ const { state } = client;
30
+ return {
31
+ online: toRef(state, "online"),
32
+ networkOnline: toRef(state, "networkOnline"),
33
+ forcedOffline: toRef(state, "forcedOffline"),
34
+ syncing: toRef(state, "syncing"),
35
+ synced: client.synced,
36
+ pending: toRef(state, "pending"),
37
+ blocked: toRef(state, "blocked"),
38
+ syncTotal: toRef(state, "syncTotal"),
39
+ syncCount: toRef(state, "syncCount"),
40
+ lastError: toRef(state, "lastError"),
41
+ prefetching: toRef(state, "prefetching"),
42
+ sync: client.sync,
43
+ rollback: client.rollback,
44
+ retry: client.retry,
45
+ goOffline: client.goOffline,
46
+ goOnline: client.goOnline
47
+ };
48
+ }
@@ -0,0 +1,26 @@
1
+ import type { AirplaneDexie, UndoEntry } from './types.js';
2
+ export declare const MODULE_STORES: {
3
+ queries: string;
4
+ mutations: string;
5
+ meta: string;
6
+ };
7
+ /** Tables that hold module bookkeeping rather than cached data. */
8
+ export declare const BOOKKEEPING_TABLES: Set<string>;
9
+ export interface CreateDbOptions {
10
+ /**
11
+ * Warn (once per table and index) when a boolean is written to an indexed
12
+ * key path, since IndexedDB cannot index booleans. Defaults to dev mode.
13
+ */
14
+ warnOnBooleanIndex?: boolean;
15
+ }
16
+ export declare function createDb(name: string, stores?: Record<string, string>, version?: number, { warnOnBooleanIndex }?: CreateDbOptions): AirplaneDexie;
17
+ /**
18
+ * Runs `fn` in a read-write transaction over every table and returns the
19
+ * writes it made. If `fn` throws, the transaction aborts and nothing is kept.
20
+ */
21
+ export declare function withUndoLog<T>(db: AirplaneDexie, fn: () => Promise<T> | T): Promise<{
22
+ result: T;
23
+ undo: UndoEntry[];
24
+ }>;
25
+ /** Puts rows back as they were, newest write first. */
26
+ export declare function applyUndo(db: AirplaneDexie, undo: UndoEntry[]): Promise<void>;
@@ -0,0 +1,109 @@
1
+ import { Dexie } from "dexie";
2
+ export const MODULE_STORES = {
3
+ queries: "&key, document, updatedAt",
4
+ mutations: "++id, status, createdAt",
5
+ meta: "&key"
6
+ };
7
+ export const BOOKKEEPING_TABLES = /* @__PURE__ */ new Set(["mutations", "meta"]);
8
+ export function createDb(name, stores = {}, version = 1, { warnOnBooleanIndex = import.meta.dev } = {}) {
9
+ const db = new Dexie(name);
10
+ db.version(version).stores({ ...MODULE_STORES, ...stores });
11
+ installUndoLog(db, warnOnBooleanIndex);
12
+ return db;
13
+ }
14
+ function valueAt(row, keyPath) {
15
+ return keyPath.split(".").reduce((o, k) => o && typeof o === "object" ? o[k] : void 0, row);
16
+ }
17
+ function warnBooleanIndexes(table, values, warned) {
18
+ const { primaryKey, indexes } = table.schema;
19
+ for (const index of [primaryKey, ...indexes]) {
20
+ if (!index.keyPath || index.name?.includes("(virtual-from:")) continue;
21
+ const label = index.name ?? "primary key";
22
+ const id = `${table.name}/${label}`;
23
+ if (warned.has(id)) continue;
24
+ const paths = Array.isArray(index.keyPath) ? index.keyPath : [index.keyPath];
25
+ for (const value of values) {
26
+ const at = paths.findIndex((path) => typeof valueAt(value, path) === "boolean");
27
+ if (at === -1) continue;
28
+ warned.add(id);
29
+ const where = paths.length > 1 ? `index "${label}" at "${paths[at]}"` : `index "${label}"`;
30
+ console.warn(`[nuxt-airplane] Table "${table.name}": ${where} received a boolean, and IndexedDB cannot index booleans; store it as 0 | 1 (see the docs on stores).`);
31
+ break;
32
+ }
33
+ }
34
+ }
35
+ const logs = /* @__PURE__ */ new WeakMap();
36
+ function keysOf(index, values) {
37
+ return index.extractKey ? values.map((v) => index.extractKey(v)) : void 0;
38
+ }
39
+ async function recordBefore(table, req) {
40
+ const pk = table.schema.primaryKey;
41
+ const trans = req.trans;
42
+ const name = table.name;
43
+ if (req.type === "deleteRange") {
44
+ const values = (await table.query({ trans, query: { index: pk, range: req.range }, values: true })).result;
45
+ const keys2 = keysOf(pk, values) ?? (await table.query({ trans, query: { index: pk, range: req.range }, values: false })).result;
46
+ return { entries: values.map((before2, i) => ({ table: name, key: keys2[i], before: before2 })) };
47
+ }
48
+ if (req.type === "delete") {
49
+ const before2 = await table.getMany({ trans, keys: req.keys });
50
+ return { entries: req.keys.map((key, i) => ({ table: name, key, before: before2[i] })) };
51
+ }
52
+ const keys = req.keys ?? keysOf(pk, req.values);
53
+ if (!keys) {
54
+ const entries = [];
55
+ return { entries, afterMutate: (result) => result.forEach((key) => entries.push({ table: name, key })) };
56
+ }
57
+ const before = await table.getMany({ trans, keys });
58
+ return { entries: keys.map((key, i) => ({ table: name, key, before: before[i] })) };
59
+ }
60
+ function installUndoLog(db, warnOnBooleanIndex = false) {
61
+ const warned = /* @__PURE__ */ new Set();
62
+ db.use({
63
+ stack: "dbcore",
64
+ name: "nuxt-airplane-undo",
65
+ create: (core) => ({
66
+ ...core,
67
+ table(name) {
68
+ const table = core.table(name);
69
+ if (BOOKKEEPING_TABLES.has(name)) return table;
70
+ return {
71
+ ...table,
72
+ async mutate(req) {
73
+ if (warnOnBooleanIndex && (req.type === "add" || req.type === "put")) warnBooleanIndexes(table, req.values, warned);
74
+ const log = logs.get(req.trans);
75
+ if (!log) return table.mutate(req);
76
+ const { entries, afterMutate } = await recordBefore(table, req);
77
+ const res = await table.mutate(req);
78
+ afterMutate?.(res.results ?? []);
79
+ log.push(...entries);
80
+ return res;
81
+ }
82
+ };
83
+ }
84
+ })
85
+ });
86
+ }
87
+ export async function withUndoLog(db, fn) {
88
+ const undo = [];
89
+ const result = await db.transaction("rw", db.tables, async () => {
90
+ const idbtrans = Dexie.currentTransaction.idbtrans;
91
+ logs.set(idbtrans, undo);
92
+ try {
93
+ return await fn();
94
+ } finally {
95
+ logs.delete(idbtrans);
96
+ }
97
+ });
98
+ return { result, undo };
99
+ }
100
+ export async function applyUndo(db, undo) {
101
+ await db.transaction("rw", db.tables, async () => {
102
+ for (const entry of [...undo].reverse()) {
103
+ const table = db.table(entry.table);
104
+ if (entry.before === void 0) await table.delete(entry.key);
105
+ else if (table.schema.primKey.keyPath) await table.put(entry.before);
106
+ else await table.put(entry.before, entry.key);
107
+ }
108
+ });
109
+ }