@mittwald/react-ghostmaker 1.0.2 → 1.0.3
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/dist/esm/context.js +1 -1
- package/dist/esm/hash.js +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/invalidate.js +9 -16
- package/dist/esm/makeGhost.js +11 -12
- package/dist/esm/makeGhost.test.js +97 -67
- package/dist/esm/queries.js +13 -13
- package/dist/esm/resolveGhostChain.js +5 -5
- package/dist/esm/testMocks.js +12 -7
- package/dist/esm/useGhostChain.js +51 -49
- package/dist/types/context.d.ts +10 -2
- package/dist/types/hash.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/invalidate.d.ts +2 -3
- package/dist/types/makeGhost.d.ts +1 -1
- package/dist/types/queries.d.ts +3 -6
- package/dist/types/resolveGhostChain.d.ts +2 -2
- package/dist/types/testMocks.d.ts +3 -1
- package/dist/types/types.d.ts +4 -5
- package/dist/types/useGhostChain.d.ts +1 -1
- package/package.json +1 -1
package/dist/esm/context.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { createCascade } from "context";
|
|
2
2
|
export const ghostFnContext = createCascade();
|
|
3
3
|
const useGhostFnContext = () => ghostFnContext.use();
|
|
4
|
-
export const
|
|
4
|
+
export const getQueryContext = () => useGhostFnContext()?.query;
|
package/dist/esm/hash.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hash } from "object-code";
|
|
2
2
|
const hashCache = new WeakMap();
|
|
3
|
-
export const hashObject = (
|
|
4
|
-
const hashed = hashCache.get(
|
|
5
|
-
hashCache.set(
|
|
3
|
+
export const hashObject = (obj) => {
|
|
4
|
+
const hashed = hashCache.get(obj) ?? hash(obj);
|
|
5
|
+
hashCache.set(obj, hashed);
|
|
6
6
|
return hashed;
|
|
7
7
|
};
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { makeGhost } from "./makeGhost";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
2
|
+
export { getQueryContext } from "./context";
|
|
3
|
+
export { invalidateGhosts } from "./invalidate";
|
|
4
4
|
export { registerModelIdentifier } from "./modelIdentifier";
|
|
5
5
|
export * from "./maybeGhost";
|
package/dist/esm/invalidate.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const ghostId = getGhostId(queryKey);
|
|
11
|
-
queryClient.invalidateQueries({
|
|
12
|
-
predicate: (query) => query.meta?.ghostId === ghostId ||
|
|
13
|
-
matchQuery({
|
|
14
|
-
queryKey,
|
|
15
|
-
}, query),
|
|
16
|
-
});
|
|
1
|
+
export async function invalidateGhosts(queryClient, queryKey) {
|
|
2
|
+
for (let i = 1; i <= queryKey.length; i++) {
|
|
3
|
+
const partialKey = queryKey.slice(0, i);
|
|
4
|
+
const exact = i < queryKey.length;
|
|
5
|
+
await queryClient.invalidateQueries({
|
|
6
|
+
queryKey: partialKey,
|
|
7
|
+
exact,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
17
10
|
}
|
package/dist/esm/makeGhost.js
CHANGED
|
@@ -4,11 +4,12 @@ import { useGhostChain } from "./useGhostChain";
|
|
|
4
4
|
import { RenderComponent } from "./RenderComponent";
|
|
5
5
|
import { resolveGhostChain } from "./resolveGhostChain";
|
|
6
6
|
import { invalidateGhosts } from "./invalidate";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { queries } from "./queries";
|
|
8
|
+
function buildGhostDeep(target, ghostChain) {
|
|
9
|
+
const proxyTarget = (() => {
|
|
9
10
|
// Dummy function to allow ghosting function calls
|
|
10
11
|
});
|
|
11
|
-
const useQuery = (options) => useGhostChain(
|
|
12
|
+
const useQuery = (options) => useGhostChain(target, ghostChain, options);
|
|
12
13
|
const useGhostMethods = {
|
|
13
14
|
use: (options) => useQuery(options).value,
|
|
14
15
|
useGhost: useQuery,
|
|
@@ -26,11 +27,9 @@ function buildGhostDeep(model, ghostChain) {
|
|
|
26
27
|
});
|
|
27
28
|
return ghost;
|
|
28
29
|
},
|
|
29
|
-
invalidate: (queryClient) =>
|
|
30
|
-
invalidateGhosts(queryClient, model, ghostChain);
|
|
31
|
-
},
|
|
30
|
+
invalidate: (queryClient) => invalidateGhosts(queryClient, queries.ghostChain(target, ghostChain)),
|
|
32
31
|
};
|
|
33
|
-
const ghost = new Proxy(
|
|
32
|
+
const ghost = new Proxy(proxyTarget, {
|
|
34
33
|
get: ($, prop) => {
|
|
35
34
|
if (prop === isGhostMarker) {
|
|
36
35
|
return true;
|
|
@@ -39,7 +38,7 @@ function buildGhostDeep(model, ghostChain) {
|
|
|
39
38
|
return useGhostMethods[prop];
|
|
40
39
|
}
|
|
41
40
|
if (prop === "then") {
|
|
42
|
-
return (resolve, reject) => resolveGhostChain(
|
|
41
|
+
return (resolve, reject) => resolveGhostChain(target, ghostChain).then(resolve).catch(reject);
|
|
43
42
|
}
|
|
44
43
|
if (prop === Symbol.toPrimitive) {
|
|
45
44
|
return (hint) => {
|
|
@@ -49,7 +48,7 @@ function buildGhostDeep(model, ghostChain) {
|
|
|
49
48
|
return null;
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
|
-
return buildGhostDeep(
|
|
51
|
+
return buildGhostDeep(target, [
|
|
53
52
|
...ghostChain,
|
|
54
53
|
{
|
|
55
54
|
propName: String(prop),
|
|
@@ -61,11 +60,11 @@ function buildGhostDeep(model, ghostChain) {
|
|
|
61
60
|
if (prevCallStackEntry) {
|
|
62
61
|
prevCallStackEntry.args = args;
|
|
63
62
|
}
|
|
64
|
-
return buildGhostDeep(
|
|
63
|
+
return buildGhostDeep(target, ghostChain);
|
|
65
64
|
},
|
|
66
65
|
});
|
|
67
66
|
return ghost;
|
|
68
67
|
}
|
|
69
|
-
export function makeGhost(
|
|
70
|
-
return buildGhostDeep(
|
|
68
|
+
export function makeGhost(target) {
|
|
69
|
+
return buildGhostDeep(target, []);
|
|
71
70
|
}
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { cleanup, render
|
|
3
|
-
import { describe, expect, test, vitest, beforeEach } from "vitest";
|
|
4
|
-
import { CustomerDetailed, CustomerGhost, Project, ProjectGhost, customerMocks, getCustomerNameGhostIds, projectMocks, } from "./testMocks";
|
|
2
|
+
import { cleanup, render } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, test, vitest, beforeEach, afterEach } from "vitest";
|
|
4
|
+
import { CustomerDetailed, CustomerGhost, Project, ProjectDetailed, ProjectGhost, advanceSleepTimer, customerMocks, getCustomerNameGhostIds, projectMocks, } from "./testMocks";
|
|
5
5
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
|
-
import {
|
|
6
|
+
import { invalidateGhosts } from "./invalidate";
|
|
7
|
+
import { makeGhost } from "./makeGhost";
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vitest.useFakeTimers();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vitest.runOnlyPendingTimers();
|
|
13
|
+
vitest.useRealTimers();
|
|
14
|
+
});
|
|
7
15
|
test("Pre Test", async () => {
|
|
8
16
|
const project = new Project("P1");
|
|
9
17
|
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(0);
|
|
10
18
|
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(0);
|
|
11
|
-
const detailedProject = await
|
|
19
|
+
const [detailedProject] = await Promise.all([
|
|
20
|
+
project.getDetailed(),
|
|
21
|
+
advanceSleepTimer(),
|
|
22
|
+
]);
|
|
12
23
|
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
13
24
|
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(0);
|
|
14
25
|
expect(detailedProject.name).toBe("Project P1");
|
|
15
|
-
const customer = await
|
|
26
|
+
const [customer] = await Promise.all([
|
|
27
|
+
detailedProject.customer.getDetailed(),
|
|
28
|
+
advanceSleepTimer(),
|
|
29
|
+
]);
|
|
30
|
+
await vitest.runOnlyPendingTimersAsync();
|
|
16
31
|
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
17
32
|
expect(customer.id).toBe("C1");
|
|
18
33
|
});
|
|
@@ -28,62 +43,46 @@ describe("Await", () => {
|
|
|
28
43
|
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(0);
|
|
29
44
|
expect(customerMocks.getName).toHaveBeenCalledTimes(0);
|
|
30
45
|
expect(transform).toHaveBeenCalledTimes(0);
|
|
31
|
-
await customerNameGhost;
|
|
46
|
+
await Promise.all([customerNameGhost, advanceSleepTimer(2)]);
|
|
47
|
+
await vitest.runOnlyPendingTimersAsync();
|
|
32
48
|
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
33
49
|
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
34
50
|
expect(customerMocks.getName).toHaveBeenCalledTimes(1);
|
|
35
51
|
expect(transform).toHaveBeenCalledTimes(1);
|
|
36
52
|
});
|
|
37
53
|
test("simple usage", async () => {
|
|
38
|
-
const customerName = await
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const customerName = await ProjectGhost.ofId("Project A")
|
|
46
|
-
.getDetailed()
|
|
47
|
-
.customer.getDetailed()
|
|
48
|
-
.getName()
|
|
49
|
-
.transform((name) => name.toUpperCase());
|
|
50
|
-
expect(customerName).toBe("CUSTOMER C1");
|
|
51
|
-
});
|
|
52
|
-
test("with undefined result in call stack", async () => {
|
|
53
|
-
const customerName = await ProjectGhost.ofId("Project A")
|
|
54
|
-
.findDetailed()
|
|
55
|
-
.customer.getDetailed()
|
|
56
|
-
.getName()
|
|
57
|
-
.transform((name) => {
|
|
58
|
-
expect(name).toBeUndefined();
|
|
59
|
-
return name?.toUpperCase();
|
|
60
|
-
});
|
|
61
|
-
expect(customerName).toBeUndefined();
|
|
62
|
-
});
|
|
63
|
-
test("simple usage", async () => {
|
|
64
|
-
const customerName = await ProjectGhost.ofId("Project A")
|
|
65
|
-
.getDetailed()
|
|
66
|
-
.customer.getDetailed()
|
|
67
|
-
.getName();
|
|
54
|
+
const [customerName] = await Promise.all([
|
|
55
|
+
ProjectGhost.ofId("Project A")
|
|
56
|
+
.getDetailed()
|
|
57
|
+
.customer.getDetailed()
|
|
58
|
+
.getName(),
|
|
59
|
+
advanceSleepTimer(2),
|
|
60
|
+
]);
|
|
68
61
|
expect(customerName).toBe("Customer C1");
|
|
69
62
|
});
|
|
70
63
|
test("with transform", async () => {
|
|
71
|
-
const customerName = await
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
const [customerName] = await Promise.all([
|
|
65
|
+
ProjectGhost.ofId("Project A")
|
|
66
|
+
.getDetailed()
|
|
67
|
+
.customer.getDetailed()
|
|
68
|
+
.getName()
|
|
69
|
+
.transform((name) => name.toUpperCase()),
|
|
70
|
+
advanceSleepTimer(2),
|
|
71
|
+
]);
|
|
76
72
|
expect(customerName).toBe("CUSTOMER C1");
|
|
77
73
|
});
|
|
78
74
|
test("with undefined result in call stack", async () => {
|
|
79
|
-
const customerName = await
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
const [customerName] = await Promise.all([
|
|
76
|
+
ProjectGhost.ofId("Project A")
|
|
77
|
+
.findDetailed()
|
|
78
|
+
.customer.getDetailed()
|
|
79
|
+
.getName()
|
|
80
|
+
.transform((name) => {
|
|
81
|
+
expect(name).toBeUndefined();
|
|
82
|
+
return name?.toUpperCase();
|
|
83
|
+
}),
|
|
84
|
+
advanceSleepTimer(2),
|
|
85
|
+
]);
|
|
87
86
|
expect(customerName).toBeUndefined();
|
|
88
87
|
});
|
|
89
88
|
});
|
|
@@ -106,7 +105,10 @@ describe("Hooks", () => {
|
|
|
106
105
|
wrapper: (props) => (_jsx(QueryClientProvider, { client: queryClient, children: props.children })),
|
|
107
106
|
});
|
|
108
107
|
if (waitForSuspense) {
|
|
109
|
-
await
|
|
108
|
+
await Promise.all([
|
|
109
|
+
ui.findByTestId("hook-ready"),
|
|
110
|
+
vitest.runOnlyPendingTimersAsync(),
|
|
111
|
+
]);
|
|
110
112
|
}
|
|
111
113
|
return {
|
|
112
114
|
ui,
|
|
@@ -175,10 +177,14 @@ describe("Hooks", () => {
|
|
|
175
177
|
.customer.getDetailed()
|
|
176
178
|
.getName()
|
|
177
179
|
.useGhost());
|
|
180
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
181
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
182
|
+
expect(customerMocks.getName).toHaveBeenCalledTimes(1);
|
|
178
183
|
result.current?.invalidate();
|
|
179
|
-
await
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
await vitest.runOnlyPendingTimersAsync();
|
|
185
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
186
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
187
|
+
expect(customerMocks.getName).toHaveBeenCalledTimes(2);
|
|
182
188
|
});
|
|
183
189
|
test("ghost.invalidate() triggers re-execution of all async methods", async () => {
|
|
184
190
|
const ghost = ProjectGhost.ofId("Project A")
|
|
@@ -186,10 +192,14 @@ describe("Hooks", () => {
|
|
|
186
192
|
.customer.getDetailed()
|
|
187
193
|
.getName();
|
|
188
194
|
await renderHookWithSuspense(() => ghost.use());
|
|
189
|
-
|
|
195
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
196
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(customerMocks.getName).toHaveBeenCalledTimes(1);
|
|
190
198
|
ghost.invalidate(queryClient);
|
|
191
|
-
await
|
|
192
|
-
|
|
199
|
+
await vitest.runOnlyPendingTimersAsync();
|
|
200
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
201
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
202
|
+
expect(customerMocks.getName).toHaveBeenCalledTimes(2);
|
|
193
203
|
});
|
|
194
204
|
test("invalidateGhostsById() triggers re-execution of all async methods", async () => {
|
|
195
205
|
const ghost = ProjectGhost.ofId("Project A")
|
|
@@ -197,27 +207,47 @@ describe("Hooks", () => {
|
|
|
197
207
|
.customer.getDetailed()
|
|
198
208
|
.getName();
|
|
199
209
|
await renderHookWithSuspense(() => ghost.use());
|
|
200
|
-
|
|
201
|
-
expect(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
await
|
|
210
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
212
|
+
expect(customerMocks.getName).toHaveBeenCalledTimes(1);
|
|
213
|
+
invalidateGhosts(queryClient, getCustomerNameGhostIds.current.queryKey);
|
|
214
|
+
await vitest.runOnlyPendingTimersAsync();
|
|
215
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
216
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
217
|
+
expect(customerMocks.getName).toHaveBeenCalledTimes(2);
|
|
205
218
|
});
|
|
206
219
|
test("ghost.invalidate() invalidates all dependent ghosts", async () => {
|
|
207
220
|
const ghost = ProjectGhost.ofId("Project A").getDetailed();
|
|
208
221
|
const specialGhost = ghost.customer.getDetailed();
|
|
209
222
|
await renderHookWithSuspense(() => specialGhost.use());
|
|
210
|
-
|
|
223
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
211
225
|
ghost.invalidate(queryClient);
|
|
212
|
-
await
|
|
226
|
+
await vitest.runOnlyPendingTimersAsync();
|
|
227
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
228
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
213
229
|
});
|
|
214
|
-
test("ghost.invalidate()
|
|
230
|
+
test("ghost.invalidate() invalidates previous ghosts", async () => {
|
|
215
231
|
const ghost = ProjectGhost.ofId("Project A").getDetailed();
|
|
216
232
|
const specialGhost = ghost.customer.getDetailed();
|
|
217
233
|
await renderHookWithSuspense(() => ghost.use());
|
|
218
|
-
|
|
234
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(1);
|
|
235
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(0);
|
|
219
236
|
specialGhost.invalidate(queryClient);
|
|
220
|
-
await
|
|
237
|
+
await vitest.runOnlyPendingTimersAsync();
|
|
238
|
+
expect(projectMocks.getDetailed).toHaveBeenCalledTimes(2);
|
|
239
|
+
expect(customerMocks.getDetailed).toHaveBeenCalledTimes(0);
|
|
240
|
+
});
|
|
241
|
+
test("target changes invalidates dependent ghosts", async () => {
|
|
242
|
+
const projectGhost = ProjectGhost.ofId("Project A").getDetailed();
|
|
243
|
+
await renderHookWithSuspense(() => makeGhost(projectGhost.use()).getName().use());
|
|
244
|
+
expect(projectMocks.getName).toHaveBeenCalledTimes(1);
|
|
245
|
+
projectMocks.getDetailed = vitest
|
|
246
|
+
.fn()
|
|
247
|
+
.mockImplementation((id) => new ProjectDetailed(id, `CHANGED! Project ${id}`, "C1"));
|
|
248
|
+
projectGhost.invalidate(queryClient);
|
|
249
|
+
await vitest.runOnlyPendingTimersAsync();
|
|
250
|
+
expect(projectMocks.getName).toHaveBeenCalledTimes(2);
|
|
221
251
|
});
|
|
222
252
|
});
|
|
223
253
|
});
|
package/dist/esm/queries.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import is from "@sindresorhus/is";
|
|
2
2
|
import { modelIdentifiers } from "./modelIdentifier";
|
|
3
|
-
|
|
3
|
+
import { hashObject } from "./hash";
|
|
4
|
+
const getTargetQueryKey = (something) => {
|
|
4
5
|
if (is.primitive(something)) {
|
|
5
6
|
return String(something);
|
|
6
7
|
}
|
|
@@ -21,22 +22,21 @@ export const getObjectQueryKey = (something) => {
|
|
|
21
22
|
}
|
|
22
23
|
return objectName;
|
|
23
24
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const getArgKey = (arg) => {
|
|
26
|
+
if (is.primitive(arg)) {
|
|
27
|
+
return String(arg);
|
|
28
|
+
}
|
|
29
|
+
return hashObject(arg);
|
|
27
30
|
};
|
|
28
31
|
export const queries = {
|
|
29
|
-
ghostmaker: () => [
|
|
30
|
-
|
|
32
|
+
ghostmaker: () => ["react-ghostmaker"],
|
|
33
|
+
target: (target) => [
|
|
31
34
|
...queries.ghostmaker(),
|
|
32
|
-
|
|
35
|
+
getTargetQueryKey(target),
|
|
33
36
|
],
|
|
34
|
-
chainItem: (chainItem) => {
|
|
37
|
+
chainItem: (prev, chainItem) => {
|
|
35
38
|
const { propName, args = [] } = chainItem;
|
|
36
|
-
return [propName, ...args.map(
|
|
39
|
+
return [...prev, propName, ...args.map(getArgKey)];
|
|
37
40
|
},
|
|
38
|
-
ghostChain: (
|
|
39
|
-
...queries.model(model),
|
|
40
|
-
...chain.flatMap(queries.chainItem),
|
|
41
|
-
],
|
|
41
|
+
ghostChain: (target, chain) => chain.reduce(queries.chainItem, queries.target(target)),
|
|
42
42
|
};
|
|
@@ -2,7 +2,6 @@ import is, { assert } from "@sindresorhus/is";
|
|
|
2
2
|
import { isReactGhost, transformFnProp, } from "./types";
|
|
3
3
|
const resolveGhostChainItem = async (target, item) => {
|
|
4
4
|
const { propName, args } = item;
|
|
5
|
-
const model = target;
|
|
6
5
|
if (propName === transformFnProp) {
|
|
7
6
|
assert.array(args);
|
|
8
7
|
const transformFn = args[0];
|
|
@@ -13,10 +12,11 @@ const resolveGhostChainItem = async (target, item) => {
|
|
|
13
12
|
}
|
|
14
13
|
return transformed;
|
|
15
14
|
}
|
|
16
|
-
if (is.nullOrUndefined(
|
|
15
|
+
if (is.nullOrUndefined(target)) {
|
|
17
16
|
return undefined;
|
|
18
17
|
}
|
|
19
|
-
|
|
18
|
+
assert.object(target);
|
|
19
|
+
const property = target[propName];
|
|
20
20
|
if (args === undefined) {
|
|
21
21
|
return property;
|
|
22
22
|
}
|
|
@@ -24,8 +24,8 @@ const resolveGhostChainItem = async (target, item) => {
|
|
|
24
24
|
const asyncFn = property.bind(target);
|
|
25
25
|
return await asyncFn(...args);
|
|
26
26
|
};
|
|
27
|
-
export const resolveGhostChain = async (
|
|
28
|
-
let result =
|
|
27
|
+
export const resolveGhostChain = async (target, stack) => {
|
|
28
|
+
let result = target;
|
|
29
29
|
for (const item of stack) {
|
|
30
30
|
result = await resolveGhostChainItem(result, item);
|
|
31
31
|
}
|
package/dist/esm/testMocks.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { beforeEach, vitest } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
const sleep = (
|
|
2
|
+
import { getQueryContext, makeGhost, registerModelIdentifier } from ".";
|
|
3
|
+
const sleep = () => new Promise((resolve) => setTimeout(resolve, 200));
|
|
4
|
+
export const advanceSleepTimer = async (times = 1) => {
|
|
5
|
+
for (let i = 0; i < times; i++) {
|
|
6
|
+
await vitest.advanceTimersByTimeAsync(200);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
4
9
|
export let customerMocks;
|
|
5
10
|
export let projectMocks;
|
|
6
11
|
export const getCustomerNameGhostIds = {};
|
|
@@ -9,7 +14,7 @@ beforeEach(() => {
|
|
|
9
14
|
getCustomerNameGhostIds.current = undefined;
|
|
10
15
|
customerMocks = {
|
|
11
16
|
getName: vitest.fn().mockImplementation(() => {
|
|
12
|
-
getCustomerNameGhostIds.current =
|
|
17
|
+
getCustomerNameGhostIds.current = getQueryContext();
|
|
13
18
|
}),
|
|
14
19
|
getDetailed: vitest
|
|
15
20
|
.fn()
|
|
@@ -48,7 +53,7 @@ export class Customer {
|
|
|
48
53
|
return Customer.get(this.id);
|
|
49
54
|
}
|
|
50
55
|
static async get(id) {
|
|
51
|
-
await sleep(
|
|
56
|
+
await sleep();
|
|
52
57
|
return customerMocks.getDetailed(id);
|
|
53
58
|
}
|
|
54
59
|
}
|
|
@@ -61,11 +66,11 @@ export class Project {
|
|
|
61
66
|
return new Project(id);
|
|
62
67
|
}
|
|
63
68
|
async getDetailed() {
|
|
64
|
-
await sleep(
|
|
69
|
+
await sleep();
|
|
65
70
|
return projectMocks.getDetailed(this.id);
|
|
66
71
|
}
|
|
67
72
|
async findDetailed() {
|
|
68
|
-
await sleep(
|
|
73
|
+
await sleep();
|
|
69
74
|
return projectMocks.findDetailed(this.id);
|
|
70
75
|
}
|
|
71
76
|
}
|
|
@@ -84,7 +89,7 @@ export class ProjectDetailed {
|
|
|
84
89
|
}
|
|
85
90
|
async getStatus() {
|
|
86
91
|
projectMocks.getStatus();
|
|
87
|
-
await sleep(
|
|
92
|
+
await sleep();
|
|
88
93
|
return "active";
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -1,78 +1,80 @@
|
|
|
1
1
|
import is, { assert } from "@sindresorhus/is";
|
|
2
|
-
import { useMemo } from "react";
|
|
2
|
+
import { useEffect, useEffectEvent, useMemo, useRef } from "react";
|
|
3
3
|
import { transformFnProp, } from "./types";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
|
5
|
+
import { queries } from "./queries";
|
|
6
6
|
import { ghostFnContext } from "./context";
|
|
7
7
|
import { invalidateGhosts } from "./invalidate";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
8
|
+
import { hashObject } from "./hash";
|
|
9
|
+
export const useGhostChain = (target, chain, options) => {
|
|
10
|
+
const queryClient = useQueryClient();
|
|
11
|
+
const context = {
|
|
12
|
+
queryKey: queries.target(target),
|
|
13
|
+
};
|
|
14
|
+
const value = chain.reduce((currentTarget, item) => useGhostChainItem(currentTarget, item, options, context), target);
|
|
15
|
+
const invalidate = () => invalidateGhosts(queryClient, queries.ghostChain(target, chain));
|
|
16
|
+
return {
|
|
17
|
+
value,
|
|
18
|
+
invalidate,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
14
21
|
const useGhostChainItem = (target, item, options = {}, context) => {
|
|
15
22
|
const { propName, args } = item;
|
|
16
|
-
const model = target;
|
|
17
23
|
if (propName === transformFnProp) {
|
|
18
24
|
assert.array(args);
|
|
19
25
|
const transformFn = args[0];
|
|
20
26
|
assert.function(transformFn, "transform requires a mapping function");
|
|
21
27
|
const transformDependencies = args[1] ?? [];
|
|
22
28
|
assert.array(transformDependencies);
|
|
23
|
-
return useMemo(() => transformFn(
|
|
29
|
+
return useMemo(() => transformFn(target), [target, ...transformDependencies]);
|
|
24
30
|
}
|
|
25
|
-
if (is.nullOrUndefined(
|
|
26
|
-
|
|
27
|
-
useVoidQuery();
|
|
28
|
-
}
|
|
29
|
-
return model;
|
|
31
|
+
if (is.nullOrUndefined(target)) {
|
|
32
|
+
return target;
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
+
assert.object(target);
|
|
35
|
+
const targetPropertyName = propName;
|
|
36
|
+
const targetProperty = target[targetPropertyName];
|
|
37
|
+
context.queryKey = queries.chainItem(context.queryKey, item);
|
|
34
38
|
if (args === undefined) {
|
|
35
39
|
// no function call, just a property access
|
|
36
|
-
return
|
|
40
|
+
return targetProperty;
|
|
37
41
|
}
|
|
38
|
-
assert.function(
|
|
39
|
-
const
|
|
42
|
+
assert.function(targetProperty);
|
|
43
|
+
const targetFn = targetProperty.bind(target);
|
|
40
44
|
const queryKey = [...context.queryKey];
|
|
41
45
|
const query = useSuspenseQuery({
|
|
42
46
|
...options,
|
|
43
|
-
queryKey
|
|
44
|
-
queryFn: async () => {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
if (result instanceof Promise) {
|
|
48
|
-
return { result: await result };
|
|
49
|
-
}
|
|
50
|
-
return { result };
|
|
51
|
-
},
|
|
52
|
-
meta: {
|
|
53
|
-
ghostId: context.ghostId,
|
|
54
|
-
queryKey,
|
|
47
|
+
queryKey,
|
|
48
|
+
queryFn: async (ctx) => {
|
|
49
|
+
const targetFnWithContext = ghostFnContext.bind({ query: ctx }, targetFn);
|
|
50
|
+
return { result: await targetFnWithContext(...args) };
|
|
55
51
|
},
|
|
56
52
|
});
|
|
53
|
+
useTargetAutoInvalidate(target, queryKey);
|
|
57
54
|
if (query.error) {
|
|
58
55
|
throw query.error;
|
|
59
56
|
}
|
|
60
57
|
return query.data.result;
|
|
61
58
|
};
|
|
62
|
-
|
|
59
|
+
const useTargetAutoInvalidate = (target, queryKey) => {
|
|
63
60
|
const queryClient = useQueryClient();
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
const invalidate = () => queryClient.invalidateQueries({
|
|
62
|
+
queryKey,
|
|
63
|
+
});
|
|
64
|
+
const targetHash = hashObject(target);
|
|
65
|
+
const prevTargetHash = useRef(targetHash);
|
|
66
|
+
const joinedQueryKey = queryKey.join(".");
|
|
67
|
+
const prevQueryId = useRef(joinedQueryKey);
|
|
68
|
+
const needsRefresh = prevQueryId.current === joinedQueryKey &&
|
|
69
|
+
prevTargetHash.current !== targetHash;
|
|
70
|
+
prevTargetHash.current = targetHash;
|
|
71
|
+
prevQueryId.current = joinedQueryKey;
|
|
72
|
+
const onTargetChangeEvent = useEffectEvent(() => {
|
|
73
|
+
if (needsRefresh) {
|
|
74
|
+
invalidate();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
onTargetChangeEvent();
|
|
79
|
+
}, [needsRefresh]);
|
|
78
80
|
};
|
package/dist/types/context.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import type { QueryFunctionContext } from "@tanstack/react-query";
|
|
1
2
|
export interface GhostFnContext {
|
|
2
|
-
|
|
3
|
+
query?: QueryFunctionContext;
|
|
3
4
|
[key: string]: unknown;
|
|
4
5
|
}
|
|
5
6
|
export declare const ghostFnContext: import("context").CtxCascadeApi<GhostFnContext>;
|
|
6
|
-
export declare const
|
|
7
|
+
export declare const getQueryContext: () => {
|
|
8
|
+
client: import("@tanstack/react-query").QueryClient;
|
|
9
|
+
queryKey: readonly unknown[];
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
meta: import("@tanstack/react-query").QueryMeta | undefined;
|
|
12
|
+
pageParam?: unknown;
|
|
13
|
+
direction?: unknown;
|
|
14
|
+
} | undefined;
|
package/dist/types/hash.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const hashObject: (
|
|
1
|
+
export declare const hashObject: (obj: object) => number;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { makeGhost } from "./makeGhost";
|
|
2
2
|
export { type ReactGhost, UseGhostReturn } from "./types";
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
3
|
+
export { getQueryContext } from "./context";
|
|
4
|
+
export { invalidateGhosts } from "./invalidate";
|
|
5
5
|
export { registerModelIdentifier } from "./modelIdentifier";
|
|
6
6
|
export * from "./maybeGhost";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import { type QueryClient } from "@tanstack/react-query";
|
|
2
|
-
import type {
|
|
3
|
-
export declare
|
|
4
|
-
export declare function invalidateGhosts(queryClient: QueryClient, model: unknown, chain: GhostChain): void;
|
|
2
|
+
import type { QueryKey } from "./types";
|
|
3
|
+
export declare function invalidateGhosts(queryClient: QueryClient, queryKey: QueryKey): Promise<void>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type ReactGhost } from "./types";
|
|
2
|
-
export declare function makeGhost<T>(
|
|
2
|
+
export declare function makeGhost<T>(target: T): ReactGhost<T>;
|
package/dist/types/queries.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { GhostChain, GhostChainItem, QueryKey } from "./types";
|
|
2
|
-
export declare const getObjectQueryKey: (something: unknown) => string;
|
|
3
|
-
export declare const ghostmakerQueryKey = "react-ghostmaker";
|
|
4
|
-
export declare const getGhostId: (queryKey: QueryKey) => string;
|
|
5
2
|
export declare const queries: {
|
|
6
3
|
ghostmaker: () => string[];
|
|
7
|
-
|
|
8
|
-
chainItem: (chainItem: GhostChainItem) =>
|
|
9
|
-
ghostChain: (
|
|
4
|
+
target: (target: unknown) => string[];
|
|
5
|
+
chainItem: (prev: QueryKey, chainItem: GhostChainItem) => unknown[];
|
|
6
|
+
ghostChain: (target: unknown, chain: GhostChain) => unknown[];
|
|
10
7
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type GhostChain
|
|
2
|
-
export declare const resolveGhostChain: (
|
|
1
|
+
import { type GhostChain } from "./types";
|
|
2
|
+
export declare const resolveGhostChain: (target: unknown, stack: GhostChain) => Promise<unknown>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { QueryFunctionContext } from "@tanstack/react-query";
|
|
2
|
+
export declare const advanceSleepTimer: (times?: number) => Promise<void>;
|
|
1
3
|
export declare let customerMocks: {
|
|
2
4
|
getName: () => void;
|
|
3
5
|
getDetailed: (id: string) => CustomerDetailed;
|
|
@@ -9,7 +11,7 @@ export declare let projectMocks: {
|
|
|
9
11
|
getStatus: () => void;
|
|
10
12
|
};
|
|
11
13
|
export declare const getCustomerNameGhostIds: {
|
|
12
|
-
current?:
|
|
14
|
+
current?: QueryFunctionContext | undefined;
|
|
13
15
|
};
|
|
14
16
|
export declare class CustomerDetailed {
|
|
15
17
|
readonly id: string;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface ReactGhostMethods<T> extends Promise<T> {
|
|
|
13
13
|
use: (options?: useGhostOptions) => T;
|
|
14
14
|
render: (transform?: (item: T) => ReactNode) => ReactNode;
|
|
15
15
|
transform: <U, U2 = U>(fn: (item: T extends ReactGhost<infer T2> ? T2 : T) => U2, dependencies?: DependencyList) => ReactGhost<U2>;
|
|
16
|
-
invalidate: (queryClient: QueryClient) => void
|
|
16
|
+
invalidate: (queryClient: QueryClient) => Promise<void>;
|
|
17
17
|
}
|
|
18
18
|
export declare const transformFnProp: "___transformFn";
|
|
19
19
|
export declare const isGhostMarker: "___ghostMarker";
|
|
@@ -25,12 +25,11 @@ export interface GhostChainItem {
|
|
|
25
25
|
args?: ExplicitAny[];
|
|
26
26
|
}
|
|
27
27
|
export type GhostChain = GhostChainItem[];
|
|
28
|
-
export type
|
|
29
|
-
export type QueryKey =
|
|
30
|
-
export type InvalidateGhostFn = () => void
|
|
28
|
+
export type GhostChainTargetType = UnknownRecord | Class<unknown> | null | undefined;
|
|
29
|
+
export type QueryKey = readonly unknown[];
|
|
30
|
+
export type InvalidateGhostFn = () => Promise<void>;
|
|
31
31
|
export interface GhostChainContext {
|
|
32
32
|
queryKey: QueryKey;
|
|
33
|
-
ghostId: string;
|
|
34
33
|
}
|
|
35
34
|
export interface UseGhostReturn<T> {
|
|
36
35
|
value: T;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type GhostChain, type useGhostOptions, type UseGhostReturn } from "./types";
|
|
2
|
-
export declare const useGhostChain: <T>(
|
|
2
|
+
export declare const useGhostChain: <T>(target: unknown, chain: GhostChain, options?: useGhostOptions) => UseGhostReturn<T>;
|