@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.
- package/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/module.d.mts +41 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +316 -0
- package/dist/runtime/client.d.ts +230 -0
- package/dist/runtime/client.js +361 -0
- package/dist/runtime/composables/defineAirplaneRequest.d.ts +7 -0
- package/dist/runtime/composables/defineAirplaneRequest.js +4 -0
- package/dist/runtime/composables/defineAirplaneStores.d.ts +8 -0
- package/dist/runtime/composables/defineAirplaneStores.js +4 -0
- package/dist/runtime/composables/defineMutationHandler.d.ts +6 -0
- package/dist/runtime/composables/defineMutationHandler.js +4 -0
- package/dist/runtime/composables/defineMutationHandlers.d.ts +6 -0
- package/dist/runtime/composables/defineMutationHandlers.js +4 -0
- package/dist/runtime/composables/defineQueryHandler.d.ts +6 -0
- package/dist/runtime/composables/defineQueryHandler.js +4 -0
- package/dist/runtime/composables/defineQueryHandlers.d.ts +6 -0
- package/dist/runtime/composables/defineQueryHandlers.js +4 -0
- package/dist/runtime/composables/useAirplane.d.ts +3 -0
- package/dist/runtime/composables/useAirplane.js +4 -0
- package/dist/runtime/composables/useMutation.d.ts +14 -0
- package/dist/runtime/composables/useMutation.js +21 -0
- package/dist/runtime/composables/useQuery.d.ts +19 -0
- package/dist/runtime/composables/useQuery.js +63 -0
- package/dist/runtime/composables/useSync.d.ts +37 -0
- package/dist/runtime/composables/useSync.js +48 -0
- package/dist/runtime/db.d.ts +26 -0
- package/dist/runtime/db.js +109 -0
- package/dist/runtime/plugin.client.d.ts +417 -0
- package/dist/runtime/plugin.client.js +43 -0
- package/dist/runtime/types.d.ts +218 -0
- package/dist/runtime/types.js +8 -0
- package/dist/types.d.mts +3 -0
- package/package.json +96 -4
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { type MutationHandlers, type MutationRow, type AirplaneRequestOptions, type AirplaneRuntimeConfig, type AirplaneStores, type OperationDocument, type QueryHandlers, type QueryName } from './types.js';
|
|
2
|
+
export interface AirplaneClientOptions extends AirplaneRuntimeConfig {
|
|
3
|
+
documents: Record<string, OperationDocument>;
|
|
4
|
+
/** Hash of the compiled documents, mixed into the cache key. */
|
|
5
|
+
documentsHash: string;
|
|
6
|
+
mutationHandlers: MutationHandlers;
|
|
7
|
+
queryHandlers: QueryHandlers;
|
|
8
|
+
stores: AirplaneStores;
|
|
9
|
+
request: AirplaneRequestOptions;
|
|
10
|
+
/** Runs a function inside the Nuxt app context (so composables work in `headers`). */
|
|
11
|
+
runWithContext?: <T>(fn: () => T) => T | Promise<Awaited<T>>;
|
|
12
|
+
}
|
|
13
|
+
/** What `useQuery` observes: the live data plus the cache row's metadata. */
|
|
14
|
+
export interface QuerySnapshot {
|
|
15
|
+
data: unknown;
|
|
16
|
+
updatedAt: number | null;
|
|
17
|
+
/** A response for this query has been stored at some point. */
|
|
18
|
+
cached: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** `ArchiveSite` → `Archive site`, `SendHTTPRequest` → `Send http request`, for mutations without a `describe`. */
|
|
21
|
+
export declare function humanize(name: string): string;
|
|
22
|
+
/** `undefined`, `null` and `{}` all mean "no variables" and share a key. */
|
|
23
|
+
export declare function queryKey(name: string, variables: unknown): string;
|
|
24
|
+
export type AirplaneClient = ReturnType<typeof createAirplaneClient>;
|
|
25
|
+
export declare function createAirplaneClient(options: AirplaneClientOptions): {
|
|
26
|
+
db: import("./types.js").AirplaneDexie;
|
|
27
|
+
state: {
|
|
28
|
+
networkOnline: boolean;
|
|
29
|
+
forcedOffline: boolean;
|
|
30
|
+
readonly online: boolean;
|
|
31
|
+
syncing: boolean;
|
|
32
|
+
pending: {
|
|
33
|
+
id: number;
|
|
34
|
+
document: string;
|
|
35
|
+
variables: unknown;
|
|
36
|
+
status: import("./types.js").MutationStatus;
|
|
37
|
+
createdAt: number;
|
|
38
|
+
description: string;
|
|
39
|
+
error?: string | undefined;
|
|
40
|
+
undo?: {
|
|
41
|
+
table: string;
|
|
42
|
+
key: string | number | Date | void[][] | {
|
|
43
|
+
readonly byteLength: number;
|
|
44
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
45
|
+
readonly maxByteLength: number;
|
|
46
|
+
readonly resizable: boolean;
|
|
47
|
+
resize: (newByteLength?: number) => void;
|
|
48
|
+
readonly detached: boolean;
|
|
49
|
+
transfer: (newByteLength?: number) => ArrayBuffer;
|
|
50
|
+
transferToFixedLength: (newByteLength?: number) => ArrayBuffer;
|
|
51
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
52
|
+
} | {
|
|
53
|
+
readonly buffer: {
|
|
54
|
+
readonly byteLength: number;
|
|
55
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
56
|
+
readonly maxByteLength: number;
|
|
57
|
+
readonly resizable: boolean;
|
|
58
|
+
resize: (newByteLength?: number) => void;
|
|
59
|
+
readonly detached: boolean;
|
|
60
|
+
transfer: (newByteLength?: number) => ArrayBuffer;
|
|
61
|
+
transferToFixedLength: (newByteLength?: number) => ArrayBuffer;
|
|
62
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
63
|
+
} | {
|
|
64
|
+
readonly byteLength: number;
|
|
65
|
+
slice: (begin?: number, end?: number) => SharedArrayBuffer;
|
|
66
|
+
readonly growable: boolean;
|
|
67
|
+
readonly maxByteLength: number;
|
|
68
|
+
grow: (newByteLength?: number) => void;
|
|
69
|
+
readonly [Symbol.toStringTag]: "SharedArrayBuffer";
|
|
70
|
+
};
|
|
71
|
+
readonly byteLength: number;
|
|
72
|
+
readonly byteOffset: number;
|
|
73
|
+
} | {
|
|
74
|
+
readonly buffer: {
|
|
75
|
+
readonly byteLength: number;
|
|
76
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
77
|
+
readonly maxByteLength: number;
|
|
78
|
+
readonly resizable: boolean;
|
|
79
|
+
resize: (newByteLength?: number) => void;
|
|
80
|
+
readonly detached: boolean;
|
|
81
|
+
transfer: (newByteLength?: number) => ArrayBuffer;
|
|
82
|
+
transferToFixedLength: (newByteLength?: number) => ArrayBuffer;
|
|
83
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
84
|
+
} | {
|
|
85
|
+
readonly byteLength: number;
|
|
86
|
+
slice: (begin?: number, end?: number) => SharedArrayBuffer;
|
|
87
|
+
readonly growable: boolean;
|
|
88
|
+
readonly maxByteLength: number;
|
|
89
|
+
grow: (newByteLength?: number) => void;
|
|
90
|
+
readonly [Symbol.toStringTag]: "SharedArrayBuffer";
|
|
91
|
+
};
|
|
92
|
+
readonly byteLength: number;
|
|
93
|
+
readonly byteOffset: number;
|
|
94
|
+
getFloat32: (byteOffset: number, littleEndian?: boolean) => number;
|
|
95
|
+
getFloat64: (byteOffset: number, littleEndian?: boolean) => number;
|
|
96
|
+
getInt8: (byteOffset: number) => number;
|
|
97
|
+
getInt16: (byteOffset: number, littleEndian?: boolean) => number;
|
|
98
|
+
getInt32: (byteOffset: number, littleEndian?: boolean) => number;
|
|
99
|
+
getUint8: (byteOffset: number) => number;
|
|
100
|
+
getUint16: (byteOffset: number, littleEndian?: boolean) => number;
|
|
101
|
+
getUint32: (byteOffset: number, littleEndian?: boolean) => number;
|
|
102
|
+
setFloat32: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
103
|
+
setFloat64: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
104
|
+
setInt8: (byteOffset: number, value: number) => void;
|
|
105
|
+
setInt16: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
106
|
+
setInt32: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
107
|
+
setUint8: (byteOffset: number, value: number) => void;
|
|
108
|
+
setUint16: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
109
|
+
setUint32: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
110
|
+
getBigInt64: (byteOffset: number, littleEndian?: boolean) => bigint;
|
|
111
|
+
getBigUint64: (byteOffset: number, littleEndian?: boolean) => bigint;
|
|
112
|
+
setBigInt64: (byteOffset: number, value: bigint, littleEndian?: boolean) => void;
|
|
113
|
+
setBigUint64: (byteOffset: number, value: bigint, littleEndian?: boolean) => void;
|
|
114
|
+
getFloat16: (byteOffset: number, littleEndian?: boolean) => number;
|
|
115
|
+
setFloat16: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
116
|
+
readonly [Symbol.toStringTag]: string;
|
|
117
|
+
} | readonly (string | number | Date | void[][] | {
|
|
118
|
+
readonly byteLength: number;
|
|
119
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
120
|
+
readonly maxByteLength: number;
|
|
121
|
+
readonly resizable: boolean;
|
|
122
|
+
resize: (newByteLength?: number) => void;
|
|
123
|
+
readonly detached: boolean;
|
|
124
|
+
transfer: (newByteLength?: number) => ArrayBuffer;
|
|
125
|
+
transferToFixedLength: (newByteLength?: number) => ArrayBuffer;
|
|
126
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
127
|
+
} | {
|
|
128
|
+
readonly buffer: {
|
|
129
|
+
readonly byteLength: number;
|
|
130
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
131
|
+
readonly maxByteLength: number;
|
|
132
|
+
readonly resizable: boolean;
|
|
133
|
+
resize: (newByteLength?: number) => void;
|
|
134
|
+
readonly detached: boolean;
|
|
135
|
+
transfer: (newByteLength?: number) => ArrayBuffer;
|
|
136
|
+
transferToFixedLength: (newByteLength?: number) => ArrayBuffer;
|
|
137
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
138
|
+
} | {
|
|
139
|
+
readonly byteLength: number;
|
|
140
|
+
slice: (begin?: number, end?: number) => SharedArrayBuffer;
|
|
141
|
+
readonly growable: boolean;
|
|
142
|
+
readonly maxByteLength: number;
|
|
143
|
+
grow: (newByteLength?: number) => void;
|
|
144
|
+
readonly [Symbol.toStringTag]: "SharedArrayBuffer";
|
|
145
|
+
};
|
|
146
|
+
readonly byteLength: number;
|
|
147
|
+
readonly byteOffset: number;
|
|
148
|
+
} | {
|
|
149
|
+
readonly buffer: {
|
|
150
|
+
readonly byteLength: number;
|
|
151
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
152
|
+
readonly maxByteLength: number;
|
|
153
|
+
readonly resizable: boolean;
|
|
154
|
+
resize: (newByteLength?: number) => void;
|
|
155
|
+
readonly detached: boolean;
|
|
156
|
+
transfer: (newByteLength?: number) => ArrayBuffer;
|
|
157
|
+
transferToFixedLength: (newByteLength?: number) => ArrayBuffer;
|
|
158
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
159
|
+
} | {
|
|
160
|
+
readonly byteLength: number;
|
|
161
|
+
slice: (begin?: number, end?: number) => SharedArrayBuffer;
|
|
162
|
+
readonly growable: boolean;
|
|
163
|
+
readonly maxByteLength: number;
|
|
164
|
+
grow: (newByteLength?: number) => void;
|
|
165
|
+
readonly [Symbol.toStringTag]: "SharedArrayBuffer";
|
|
166
|
+
};
|
|
167
|
+
readonly byteLength: number;
|
|
168
|
+
readonly byteOffset: number;
|
|
169
|
+
getFloat32: (byteOffset: number, littleEndian?: boolean) => number;
|
|
170
|
+
getFloat64: (byteOffset: number, littleEndian?: boolean) => number;
|
|
171
|
+
getInt8: (byteOffset: number) => number;
|
|
172
|
+
getInt16: (byteOffset: number, littleEndian?: boolean) => number;
|
|
173
|
+
getInt32: (byteOffset: number, littleEndian?: boolean) => number;
|
|
174
|
+
getUint8: (byteOffset: number) => number;
|
|
175
|
+
getUint16: (byteOffset: number, littleEndian?: boolean) => number;
|
|
176
|
+
getUint32: (byteOffset: number, littleEndian?: boolean) => number;
|
|
177
|
+
setFloat32: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
178
|
+
setFloat64: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
179
|
+
setInt8: (byteOffset: number, value: number) => void;
|
|
180
|
+
setInt16: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
181
|
+
setInt32: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
182
|
+
setUint8: (byteOffset: number, value: number) => void;
|
|
183
|
+
setUint16: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
184
|
+
setUint32: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
185
|
+
getBigInt64: (byteOffset: number, littleEndian?: boolean) => bigint;
|
|
186
|
+
getBigUint64: (byteOffset: number, littleEndian?: boolean) => bigint;
|
|
187
|
+
setBigInt64: (byteOffset: number, value: bigint, littleEndian?: boolean) => void;
|
|
188
|
+
setBigUint64: (byteOffset: number, value: bigint, littleEndian?: boolean) => void;
|
|
189
|
+
getFloat16: (byteOffset: number, littleEndian?: boolean) => number;
|
|
190
|
+
setFloat16: (byteOffset: number, value: number, littleEndian?: boolean) => void;
|
|
191
|
+
readonly [Symbol.toStringTag]: string;
|
|
192
|
+
})[];
|
|
193
|
+
before?: unknown;
|
|
194
|
+
}[] | undefined;
|
|
195
|
+
}[];
|
|
196
|
+
readonly blocked: number;
|
|
197
|
+
syncTotal: number;
|
|
198
|
+
syncCount: number;
|
|
199
|
+
lastError: string | null;
|
|
200
|
+
prefetching: number;
|
|
201
|
+
};
|
|
202
|
+
synced: import("vue").ComputedRef<boolean>;
|
|
203
|
+
documents: Record<string, OperationDocument>;
|
|
204
|
+
active: Map<string, () => Promise<void>>;
|
|
205
|
+
fetchQuery: <T>(name: string, variables: unknown) => Promise<T>;
|
|
206
|
+
prefetch: (name: string, variables: unknown, data: unknown) => Promise<void>;
|
|
207
|
+
watchQuery: (name: string, variables: unknown) => {
|
|
208
|
+
snapshot: Readonly<import("vue").Ref<{
|
|
209
|
+
readonly data: Readonly<unknown>;
|
|
210
|
+
readonly updatedAt: number | null;
|
|
211
|
+
readonly cached: boolean;
|
|
212
|
+
}, {
|
|
213
|
+
readonly data: Readonly<unknown>;
|
|
214
|
+
readonly updatedAt: number | null;
|
|
215
|
+
readonly cached: boolean;
|
|
216
|
+
}>>;
|
|
217
|
+
unsubscribe: () => void;
|
|
218
|
+
};
|
|
219
|
+
refetchActive: (only?: Iterable<QueryName>) => Promise<void>;
|
|
220
|
+
enqueue: (name: string, variables: unknown) => Promise<MutationRow>;
|
|
221
|
+
rollback: (id: number) => Promise<void>;
|
|
222
|
+
retry: (id: number) => Promise<void>;
|
|
223
|
+
sync: () => Promise<void>;
|
|
224
|
+
goOffline: () => void;
|
|
225
|
+
goOnline: () => void;
|
|
226
|
+
cacheKey: string;
|
|
227
|
+
ready: import("dexie").PromiseExtended<void>;
|
|
228
|
+
clear: () => Promise<void>;
|
|
229
|
+
clearCache: () => Promise<void>;
|
|
230
|
+
};
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { liveQuery } from "dexie";
|
|
2
|
+
import { hash } from "ohash";
|
|
3
|
+
import { reactive, computed, shallowRef, readonly } from "vue";
|
|
4
|
+
import { BOOKKEEPING_TABLES, applyUndo, createDb, withUndoLog } from "./db.js";
|
|
5
|
+
import {
|
|
6
|
+
GraphQLRequestError
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
export function humanize(name) {
|
|
9
|
+
const words = name.replace(/[_-]+/g, " ").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
10
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
11
|
+
}
|
|
12
|
+
export function queryKey(name, variables) {
|
|
13
|
+
const normalized = variables && typeof variables === "object" && Object.keys(variables).length ? variables : null;
|
|
14
|
+
return `${name}:${hash(normalized)}`;
|
|
15
|
+
}
|
|
16
|
+
export function createAirplaneClient(options) {
|
|
17
|
+
const db = createDb(options.dbName, options.stores.stores, options.stores.version);
|
|
18
|
+
const { documents, mutationHandlers, queryHandlers } = options;
|
|
19
|
+
const stopOnFailure = options.stopOnFailure ?? true;
|
|
20
|
+
const opened = db.open().catch(async (error) => {
|
|
21
|
+
if (!["VersionError", "UpgradeError", "SchemaError"].includes(error.name)) throw error;
|
|
22
|
+
console.warn(`[nuxt-airplane] Could not open "${options.dbName}" (${error.name}), recreating it. Bump the store version to migrate instead.`);
|
|
23
|
+
await db.delete();
|
|
24
|
+
await db.open();
|
|
25
|
+
});
|
|
26
|
+
const cacheKey = `${options.cacheKey ?? ""}:${options.documentsHash}`;
|
|
27
|
+
async function clearCache() {
|
|
28
|
+
const tables = db.tables.filter((t) => !BOOKKEEPING_TABLES.has(t.name));
|
|
29
|
+
await db.transaction("rw", tables, async () => {
|
|
30
|
+
for (const table of tables) await table.clear();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const ready = opened.then(async () => {
|
|
34
|
+
const stored = (await db.meta.get("cacheKey"))?.value;
|
|
35
|
+
if (stored !== void 0 && stored !== cacheKey) {
|
|
36
|
+
if (import.meta.dev) console.info(`[nuxt-airplane] Cache key changed, clearing cached data (outbox kept).`);
|
|
37
|
+
await clearCache();
|
|
38
|
+
}
|
|
39
|
+
if (stored !== cacheKey) await db.meta.put({ key: "cacheKey", value: cacheKey });
|
|
40
|
+
await requeueStuck();
|
|
41
|
+
});
|
|
42
|
+
async function requeueStuck() {
|
|
43
|
+
await db.mutations.where("status").equals("syncing").modify({ status: "pending" });
|
|
44
|
+
}
|
|
45
|
+
const state = reactive({
|
|
46
|
+
/** What the browser reports; assumed online where `navigator.onLine` does not exist (Node, some webviews). */
|
|
47
|
+
networkOnline: typeof navigator === "object" && typeof navigator.onLine === "boolean" ? navigator.onLine : true,
|
|
48
|
+
/** Set by `goOffline()`: behave as offline whatever the network says. */
|
|
49
|
+
forcedOffline: false,
|
|
50
|
+
/** Effective state the client acts on. */
|
|
51
|
+
get online() {
|
|
52
|
+
return this.networkOnline && !this.forcedOffline;
|
|
53
|
+
},
|
|
54
|
+
syncing: false,
|
|
55
|
+
/** Mutations still waiting to be sent, oldest first. */
|
|
56
|
+
pending: [],
|
|
57
|
+
/** Pending mutations held back because an older one failed; `0` with `stopOnFailure: false`. */
|
|
58
|
+
get blocked() {
|
|
59
|
+
if (!stopOnFailure) return 0;
|
|
60
|
+
const failed = this.pending.findIndex((m) => m.status === "failed");
|
|
61
|
+
return failed < 0 ? 0 : this.pending.slice(failed + 1).filter((m) => m.status === "pending").length;
|
|
62
|
+
},
|
|
63
|
+
/** Size of the batch the current (or last) sync started with. */
|
|
64
|
+
syncTotal: 0,
|
|
65
|
+
/** Mutations of that batch that have reached the server so far. */
|
|
66
|
+
syncCount: 0,
|
|
67
|
+
/** Current pending list could not be sent because of a network error */
|
|
68
|
+
lastError: null,
|
|
69
|
+
/** Paginated queries currently being walked to the end by `prefetch`. */
|
|
70
|
+
prefetching: 0
|
|
71
|
+
});
|
|
72
|
+
const synced = computed(() => !state.syncing && state.pending.length === 0);
|
|
73
|
+
liveQuery(() => db.mutations.orderBy("createdAt").toArray()).subscribe({ next: (rows) => state.pending = rows });
|
|
74
|
+
if (typeof window !== "undefined") {
|
|
75
|
+
window.addEventListener("online", () => {
|
|
76
|
+
state.networkOnline = true;
|
|
77
|
+
void sync();
|
|
78
|
+
});
|
|
79
|
+
window.addEventListener("offline", () => state.networkOnline = false);
|
|
80
|
+
}
|
|
81
|
+
function goOffline() {
|
|
82
|
+
state.forcedOffline = true;
|
|
83
|
+
}
|
|
84
|
+
function goOnline() {
|
|
85
|
+
state.forcedOffline = false;
|
|
86
|
+
void sync();
|
|
87
|
+
}
|
|
88
|
+
function document(name) {
|
|
89
|
+
const doc = documents[name];
|
|
90
|
+
if (!doc) throw new Error(`[nuxt-airplane] Unknown GraphQL document "${name}"`);
|
|
91
|
+
return doc;
|
|
92
|
+
}
|
|
93
|
+
async function requestHeaders(ctx) {
|
|
94
|
+
const headers = new Headers({ "content-type": "application/json", ...options.headers });
|
|
95
|
+
const { headers: dynamic } = options.request;
|
|
96
|
+
const extra = typeof dynamic === "function" ? await (options.runWithContext ? options.runWithContext(() => dynamic(ctx)) : dynamic(ctx)) : dynamic;
|
|
97
|
+
new Headers(extra ?? void 0).forEach((value, key) => headers.set(key, value));
|
|
98
|
+
return headers;
|
|
99
|
+
}
|
|
100
|
+
async function request(name, variables) {
|
|
101
|
+
const doc = document(name);
|
|
102
|
+
const ctx = { document: name, kind: doc.kind, variables };
|
|
103
|
+
const doFetch = options.request.fetch ?? globalThis.fetch;
|
|
104
|
+
const res = await doFetch(options.endpoint, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: await requestHeaders(ctx),
|
|
107
|
+
credentials: options.request.credentials,
|
|
108
|
+
body: JSON.stringify({ query: doc.source, variables: variables ?? void 0 }),
|
|
109
|
+
signal: AbortSignal.timeout(options.request.timeout ?? 3e4)
|
|
110
|
+
});
|
|
111
|
+
await options.request.onResponse?.(res, ctx);
|
|
112
|
+
const body = await res.json().catch(() => ({}));
|
|
113
|
+
if (body.errors?.length) throw new GraphQLRequestError(body.errors);
|
|
114
|
+
if (!res.ok) throw new Error(`[nuxt-airplane] ${res.status} ${res.statusText}`);
|
|
115
|
+
if (body.data == null) throw new Error("[nuxt-airplane] Response has no data");
|
|
116
|
+
return body.data;
|
|
117
|
+
}
|
|
118
|
+
function queryHandler(name) {
|
|
119
|
+
return queryHandlers[name];
|
|
120
|
+
}
|
|
121
|
+
const active = /* @__PURE__ */ new Map();
|
|
122
|
+
async function storeResponse(name, variables, data) {
|
|
123
|
+
const h = queryHandler(name);
|
|
124
|
+
const row = { key: queryKey(name, variables), document: name, variables, updatedAt: Date.now() };
|
|
125
|
+
await db.transaction("rw", db.tables, async () => {
|
|
126
|
+
await h?.store?.({ db, variables }, data);
|
|
127
|
+
await db.queries.put({ ...row, data: h?.read ? null : data });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function fetchQuery(name, variables) {
|
|
131
|
+
await ready;
|
|
132
|
+
const data = await request(name, variables);
|
|
133
|
+
await storeResponse(name, variables, data);
|
|
134
|
+
await rebasePending();
|
|
135
|
+
if (queryHandler(name)?.prefetch) void prefetch(name, variables, data);
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
const prefetching = /* @__PURE__ */ new Set();
|
|
139
|
+
async function prefetch(name, variables, data) {
|
|
140
|
+
const p = queryHandler(name)?.prefetch;
|
|
141
|
+
if (!p || prefetching.has(name) || p.when && !p.when()) return;
|
|
142
|
+
prefetching.add(name);
|
|
143
|
+
state.prefetching++;
|
|
144
|
+
let pages = 0;
|
|
145
|
+
try {
|
|
146
|
+
let vars = variables;
|
|
147
|
+
let page = data;
|
|
148
|
+
while (state.online && pages < (p.maxPages ?? 1e3)) {
|
|
149
|
+
const next = p.next(page, vars);
|
|
150
|
+
if (!next) break;
|
|
151
|
+
vars = next;
|
|
152
|
+
page = await request(name, vars);
|
|
153
|
+
await storeResponse(name, vars, page);
|
|
154
|
+
pages++;
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (import.meta.dev) console.warn(`[nuxt-airplane] Prefetch of "${name}" stopped:`, error);
|
|
158
|
+
} finally {
|
|
159
|
+
prefetching.delete(name);
|
|
160
|
+
state.prefetching--;
|
|
161
|
+
}
|
|
162
|
+
if (pages) await rebasePending();
|
|
163
|
+
}
|
|
164
|
+
function watchQuery(name, variables) {
|
|
165
|
+
const snapshot = shallowRef({ data: null, updatedAt: null, cached: false });
|
|
166
|
+
const h = queryHandler(name);
|
|
167
|
+
const key = queryKey(name, variables);
|
|
168
|
+
const subscription = liveQuery(async () => {
|
|
169
|
+
const row = await db.queries.get(key);
|
|
170
|
+
const data = h?.read ? await h.read({ db, variables }) : row?.data;
|
|
171
|
+
const cached = h?.read ? !!row || await db.queries.where("document").equals(name).count() > 0 : !!row;
|
|
172
|
+
return { data: data ?? null, updatedAt: row?.updatedAt ?? null, cached };
|
|
173
|
+
}).subscribe({ next: (s) => snapshot.value = s });
|
|
174
|
+
return { snapshot: readonly(snapshot), unsubscribe: () => subscription.unsubscribe() };
|
|
175
|
+
}
|
|
176
|
+
async function refetchActive(only) {
|
|
177
|
+
const names = only && [...only];
|
|
178
|
+
await Promise.allSettled(
|
|
179
|
+
[...active.entries()].filter(([key]) => !names || names.some((name) => key.startsWith(`${name}:`))).map(([, refresh]) => refresh())
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function createCacheApi(invalidated) {
|
|
183
|
+
async function updateRows(name, filter, updater) {
|
|
184
|
+
const rows = (await db.queries.where("document").equals(name).toArray()).filter((r) => filter(r.key));
|
|
185
|
+
const next = rows.filter((row) => row.data !== null && row.data !== void 0).map((row) => {
|
|
186
|
+
const data = structuredClone(row.data);
|
|
187
|
+
const returned = updater(data, row.variables);
|
|
188
|
+
return { ...row, data: returned === void 0 ? data : returned };
|
|
189
|
+
});
|
|
190
|
+
await db.queries.bulkPut(next);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
async update(name, variablesOrUpdater, maybeUpdater) {
|
|
194
|
+
if (typeof variablesOrUpdater === "function") {
|
|
195
|
+
await updateRows(name, () => true, variablesOrUpdater);
|
|
196
|
+
} else {
|
|
197
|
+
const key = queryKey(name, variablesOrUpdater);
|
|
198
|
+
await updateRows(name, (k) => k === key, maybeUpdater);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
async get(name, variables) {
|
|
202
|
+
const h = queryHandler(name);
|
|
203
|
+
if (h?.read) return await h.read({ db, variables }) ?? void 0;
|
|
204
|
+
return (await db.queries.get(queryKey(name, variables)))?.data;
|
|
205
|
+
},
|
|
206
|
+
async set(name, variables, data) {
|
|
207
|
+
await db.queries.put({ key: queryKey(name, variables), document: name, variables, data, updatedAt: Date.now() });
|
|
208
|
+
},
|
|
209
|
+
async invalidate(name) {
|
|
210
|
+
await db.queries.where("document").equals(name).delete();
|
|
211
|
+
invalidated?.add(name);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function mutationHandler(name) {
|
|
216
|
+
return mutationHandlers[name];
|
|
217
|
+
}
|
|
218
|
+
async function applyMutation(row, { rebase = false } = {}) {
|
|
219
|
+
const h = mutationHandler(row.document);
|
|
220
|
+
if (!h?.apply) return;
|
|
221
|
+
const invalidated = /* @__PURE__ */ new Set();
|
|
222
|
+
const { undo } = await withUndoLog(db, () => h.apply({ db, cache: createCacheApi(invalidated), variables: row.variables }));
|
|
223
|
+
await db.mutations.update(row.id, { undo });
|
|
224
|
+
if (!rebase && invalidated.size) await refetchActive(invalidated);
|
|
225
|
+
}
|
|
226
|
+
async function rebasePending() {
|
|
227
|
+
const queue = await db.mutations.orderBy("createdAt").toArray();
|
|
228
|
+
for (const row of queue) await applyMutation(row, { rebase: true });
|
|
229
|
+
}
|
|
230
|
+
async function describeMutation(name, variables) {
|
|
231
|
+
const { describe } = mutationHandler(name) ?? {};
|
|
232
|
+
if (typeof describe === "string") return describe;
|
|
233
|
+
if (describe) return describe({ db, cache: createCacheApi(), variables });
|
|
234
|
+
return humanize(name);
|
|
235
|
+
}
|
|
236
|
+
async function enqueue(name, variables) {
|
|
237
|
+
if (document(name).kind !== "mutation") throw new Error(`[nuxt-airplane] "${name}" is not a mutation`);
|
|
238
|
+
await ready;
|
|
239
|
+
const description = await describeMutation(name, variables);
|
|
240
|
+
const id = await db.mutations.add({ document: name, variables, description, status: "pending", createdAt: Date.now() });
|
|
241
|
+
const row = await db.mutations.get(id);
|
|
242
|
+
try {
|
|
243
|
+
await applyMutation(row);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
await db.mutations.delete(id);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
void sync();
|
|
249
|
+
return row;
|
|
250
|
+
}
|
|
251
|
+
async function rollback(id) {
|
|
252
|
+
const row = await db.mutations.get(id);
|
|
253
|
+
if (!row || row.status === "syncing") return;
|
|
254
|
+
const h = mutationHandler(row.document);
|
|
255
|
+
if (h?.revert) {
|
|
256
|
+
await h.revert({ db, cache: createCacheApi(), variables: row.variables });
|
|
257
|
+
await db.mutations.delete(id);
|
|
258
|
+
} else {
|
|
259
|
+
const later = (await db.mutations.where("id").above(row.id).toArray()).filter((m) => m.status !== "syncing");
|
|
260
|
+
if (row.undo) await applyUndo(db, row.undo);
|
|
261
|
+
await db.mutations.delete(id);
|
|
262
|
+
for (const m of later) await applyMutation(m, { rebase: true });
|
|
263
|
+
}
|
|
264
|
+
void sync();
|
|
265
|
+
}
|
|
266
|
+
async function retry(id) {
|
|
267
|
+
await db.mutations.update(id, { status: "pending", error: void 0 });
|
|
268
|
+
await sync();
|
|
269
|
+
}
|
|
270
|
+
let syncPromise = null;
|
|
271
|
+
function sync() {
|
|
272
|
+
if (syncPromise) return syncPromise;
|
|
273
|
+
syncPromise = runSyncLocked().finally(() => syncPromise = null);
|
|
274
|
+
return syncPromise;
|
|
275
|
+
}
|
|
276
|
+
async function sendable() {
|
|
277
|
+
const rows = await db.mutations.orderBy("createdAt").toArray();
|
|
278
|
+
const queue = [];
|
|
279
|
+
for (const row of rows) {
|
|
280
|
+
if (row.status === "failed" && stopOnFailure) break;
|
|
281
|
+
if (row.status === "pending") queue.push(row);
|
|
282
|
+
}
|
|
283
|
+
return queue;
|
|
284
|
+
}
|
|
285
|
+
const syncLock = `nuxt-airplane:sync:${options.dbName}`;
|
|
286
|
+
async function runSyncLocked() {
|
|
287
|
+
const locks = typeof navigator === "object" ? navigator.locks : void 0;
|
|
288
|
+
if (!locks) return runSync();
|
|
289
|
+
await locks.request(syncLock, async () => {
|
|
290
|
+
await ready;
|
|
291
|
+
await requeueStuck();
|
|
292
|
+
await runSync();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async function runSync() {
|
|
296
|
+
await ready;
|
|
297
|
+
if (!state.online) return;
|
|
298
|
+
const queue = await sendable();
|
|
299
|
+
if (!queue.length) return;
|
|
300
|
+
state.syncing = true;
|
|
301
|
+
state.syncTotal = queue.length;
|
|
302
|
+
state.syncCount = 0;
|
|
303
|
+
state.lastError = null;
|
|
304
|
+
const refetch = /* @__PURE__ */ new Set();
|
|
305
|
+
let refetchAll = false;
|
|
306
|
+
try {
|
|
307
|
+
for (const row of queue) {
|
|
308
|
+
await db.mutations.update(row.id, { status: "syncing" });
|
|
309
|
+
try {
|
|
310
|
+
const result = await request(row.document, row.variables);
|
|
311
|
+
const h = mutationHandler(row.document);
|
|
312
|
+
await h?.reconcile?.({ db, cache: createCacheApi(), variables: row.variables }, result);
|
|
313
|
+
if (h?.refetch) h.refetch.forEach((n) => refetch.add(n));
|
|
314
|
+
else refetchAll = true;
|
|
315
|
+
await db.mutations.delete(row.id);
|
|
316
|
+
state.syncCount++;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error instanceof GraphQLRequestError) {
|
|
319
|
+
await db.mutations.update(row.id, { status: "failed", error: error.message });
|
|
320
|
+
if (stopOnFailure) break;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
await db.mutations.update(row.id, { status: "pending" });
|
|
324
|
+
state.lastError = error instanceof Error ? error.message : String(error);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} finally {
|
|
329
|
+
state.syncing = false;
|
|
330
|
+
}
|
|
331
|
+
if (state.syncCount) await refetchActive(refetchAll ? void 0 : refetch);
|
|
332
|
+
}
|
|
333
|
+
async function clear() {
|
|
334
|
+
await db.transaction("rw", db.tables, async () => {
|
|
335
|
+
for (const table of db.tables) await table.clear();
|
|
336
|
+
});
|
|
337
|
+
await db.meta.put({ key: "cacheKey", value: cacheKey });
|
|
338
|
+
}
|
|
339
|
+
void sync();
|
|
340
|
+
return {
|
|
341
|
+
db,
|
|
342
|
+
state,
|
|
343
|
+
synced,
|
|
344
|
+
documents,
|
|
345
|
+
active,
|
|
346
|
+
fetchQuery,
|
|
347
|
+
prefetch,
|
|
348
|
+
watchQuery,
|
|
349
|
+
refetchActive,
|
|
350
|
+
enqueue,
|
|
351
|
+
rollback,
|
|
352
|
+
retry,
|
|
353
|
+
sync,
|
|
354
|
+
goOffline,
|
|
355
|
+
goOnline,
|
|
356
|
+
cacheKey,
|
|
357
|
+
ready,
|
|
358
|
+
clear,
|
|
359
|
+
clearCache
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type AirplaneRequestOptions } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configure how requests reach the GraphQL endpoint: auth headers, credentials,
|
|
4
|
+
* a custom fetch, a response hook. Default-export it from a file under the
|
|
5
|
+
* handlers directory. Several files are merged, last one wins per option.
|
|
6
|
+
*/
|
|
7
|
+
export declare function defineAirplaneRequest(options: AirplaneRequestOptions): AirplaneRequestOptions;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AirplaneStores } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Add tables to the module's Dexie database, in Dexie's `stores()` syntax.
|
|
4
|
+
* Augment the global `NuxtAirplaneStores` interface to type their rows.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineAirplaneStores(stores: Record<string, string>, options?: {
|
|
7
|
+
version?: number;
|
|
8
|
+
}): AirplaneStores;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type MutationHandler, type MutationHandlers, type MutationName } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declare the handler of a single mutation, typed by its document name. Handy
|
|
4
|
+
* for one file per mutation; the results can be spread into `defineMutationHandlers`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineMutationHandler<N extends MutationName>(name: N, handler: MutationHandler<N>): MutationHandlers;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type MutationHandlers } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declare how queued mutations touch the local data. Files under `airplane/`
|
|
4
|
+
* (the handlers directory) that default-export one of these are picked up automatically.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineMutationHandlers(handlers: MutationHandlers): MutationHandlers;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type QueryHandler, type QueryHandlers, type QueryName } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declare the handler of a single query, typed by its document name. Handy
|
|
4
|
+
* for one file per query; the results can be spread into `defineQueryHandlers`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineQueryHandler<N extends QueryName>(name: N, handler: QueryHandler<N>): QueryHandlers;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type QueryHandlers } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declare how query responses are stored in, and read back from, your own
|
|
4
|
+
* tables. Queries without a handler are cached as-is.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineQueryHandlers(handlers: QueryHandlers): QueryHandlers;
|