@pathscale/ui 2.12.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,8 @@
1
- import type { Accessor } from "solid-js";
2
1
  /**
3
2
  * A write, without a query library. The companion to `createQuery`.
4
3
  *
5
4
  * Replaces `useMutation`. The same rule applies as there: reading this never
6
- * suspends and never throws. `mutate` reports failure through `error()`;
5
+ * suspends and never throws. `mutate` reports failure through `error`;
7
6
  * `mutateAsync` rejects, for a caller that wants to await and handle it.
8
7
  */
9
8
  export interface CreateMutationOptions<TArgs extends unknown[], TResult> {
@@ -13,15 +12,17 @@ export interface CreateMutationOptions<TArgs extends unknown[], TResult> {
13
12
  /** Runs after success or failure, like TanStack's `onSettled`. */
14
13
  onSettled?: () => void | Promise<void>;
15
14
  }
15
+ /** Read as properties, for the same reason as `QueryResult`. */
16
16
  export interface MutationResult<TArgs extends unknown[], TResult> {
17
- /** Fire and forget. Failure lands on `error()` rather than as a rejection. */
17
+ /** Fire and forget. Failure lands on `error` rather than as a rejection. */
18
18
  mutate: (...args: TArgs) => void;
19
19
  /** Fire and await. Rejects on failure. */
20
20
  mutateAsync: (...args: TArgs) => Promise<TResult>;
21
- isPending: Accessor<boolean>;
22
- error: Accessor<unknown>;
21
+ readonly isPending: boolean;
22
+ readonly error: unknown;
23
+ readonly isError: boolean;
23
24
  /** The last successful result. */
24
- data: Accessor<TResult | undefined>;
25
+ readonly data: TResult | undefined;
25
26
  /** Clear `error` and `data`. */
26
27
  reset: () => void;
27
28
  }
@@ -29,9 +29,18 @@ const createMutation = (options)=>{
29
29
  mutateAsync(...args).catch(()=>{});
30
30
  },
31
31
  mutateAsync,
32
- isPending,
33
- error,
34
- data,
32
+ get isPending () {
33
+ return isPending();
34
+ },
35
+ get error () {
36
+ return error();
37
+ },
38
+ get isError () {
39
+ return void 0 !== error();
40
+ },
41
+ get data () {
42
+ return data();
43
+ },
35
44
  reset: ()=>{
36
45
  setError(void 0);
37
46
  setData(()=>void 0);
@@ -1,4 +1,3 @@
1
- import type { Accessor } from "solid-js";
2
1
  /**
3
2
  * Asynchronous reads, without a query library.
4
3
  *
@@ -16,7 +15,7 @@ import type { Accessor } from "solid-js";
16
15
  * had nothing to print. That is how a support-chat button that had not
17
16
  * connected replaced an entire application with a blank error page.
18
17
  *
19
- * Here, `data()` is `undefined` until there is data, `isLoading()` is true only
18
+ * Here, `data` is `undefined` until there is data, `isLoading` is true only
20
19
  * while a fetch is actually in flight, and neither ever throws. A caller that
21
20
  * wants to suspend can do so explicitly; a caller that forgets cannot take the
22
21
  * page down.
@@ -32,15 +31,41 @@ export interface CreateQueryOptions<T> {
32
31
  /** Held back while false. Default true. */
33
32
  enabled?: boolean;
34
33
  }
34
+ /**
35
+ * Read as properties, not as accessors.
36
+ *
37
+ * `solid-query` returned a store, so every consumer of it is written
38
+ * `query.data`, `query.isLoading`, `query.isError` -- roughly three thousand
39
+ * such reads across these applications. Exposing accessors here would mean
40
+ * editing all of them to add `()`, turning a migration of the ~300 files that
41
+ * *define* queries into one that touches every file that reads one.
42
+ *
43
+ * These are getters over signals, so a read inside a tracked scope still
44
+ * subscribes exactly as an accessor call would.
45
+ */
35
46
  export interface QueryResult<T> {
36
47
  /** The last value read, or `undefined` before the first one arrives. */
37
- data: Accessor<T | undefined>;
48
+ readonly data: T | undefined;
38
49
  /** The last failure, cleared by the next successful read. */
39
- error: Accessor<unknown>;
50
+ readonly error: unknown;
40
51
  /** True only while a fetch is in flight. Never true for a disabled query. */
41
- isLoading: Accessor<boolean>;
52
+ readonly isLoading: boolean;
53
+ /**
54
+ * Alias of `isLoading`, for call sites carried over from `solid-query`.
55
+ *
56
+ * Note the deliberate difference in meaning. There, `isPending` was "has no
57
+ * data", which stayed true forever for a query held back by `enabled: false`
58
+ * -- so `if (isPending) return <Spinner/>` spun for the life of the page.
59
+ * Here it means "a fetch is in flight", so a disabled query reads as not
60
+ * pending and its consumer renders instead of hanging.
61
+ */
62
+ readonly isPending: boolean;
63
+ /** True when the last read failed. */
64
+ readonly isError: boolean;
65
+ /** True when a value has arrived and the last read did not fail. */
66
+ readonly isSuccess: boolean;
42
67
  /** True once a value has arrived at least once. */
43
- isReady: Accessor<boolean>;
68
+ readonly isReady: boolean;
44
69
  /** Read again now, regardless of `enabled`. */
45
70
  refetch: () => Promise<void>;
46
71
  }
@@ -45,10 +45,27 @@ const createQuery = (options)=>{
45
45
  generation++;
46
46
  });
47
47
  return {
48
- data,
49
- error,
50
- isLoading,
51
- isReady,
48
+ get data () {
49
+ return data();
50
+ },
51
+ get error () {
52
+ return error();
53
+ },
54
+ get isLoading () {
55
+ return isLoading();
56
+ },
57
+ get isPending () {
58
+ return isLoading();
59
+ },
60
+ get isError () {
61
+ return void 0 !== error();
62
+ },
63
+ get isSuccess () {
64
+ return isReady() && void 0 === error();
65
+ },
66
+ get isReady () {
67
+ return isReady();
68
+ },
52
69
  refetch: run
53
70
  };
54
71
  };
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { createRoot, flush } from "solid-js";
2
+ import { createRenderEffect, createRoot, flush } from "solid-js";
3
3
  import { createMutation } from "./createMutation.js";
4
4
  import { createQuery, invalidateQueries } from "./createQuery.js";
5
5
  const tick = ()=>new Promise((resolve)=>setTimeout(resolve, 0));
@@ -17,9 +17,9 @@ describe("createQuery", ()=>{
17
17
  return "value";
18
18
  }
19
19
  }));
20
- expect(q.isLoading()).toBe(false);
21
- expect(q.data()).toBeUndefined();
22
- expect(q.isReady()).toBe(false);
20
+ expect(q.isLoading).toBe(false);
21
+ expect(q.data).toBeUndefined();
22
+ expect(q.isReady).toBe(false);
23
23
  return d;
24
24
  });
25
25
  await tick();
@@ -37,15 +37,15 @@ describe("createQuery", ()=>{
37
37
  }));
38
38
  flush();
39
39
  const whileLoading = {
40
- loading: q.isLoading(),
41
- ready: q.isReady()
40
+ loading: q.isLoading,
41
+ ready: q.isReady
42
42
  };
43
43
  resolveFetch?.("hello");
44
44
  await tick();
45
45
  return {
46
46
  whileLoading,
47
- data: q.data(),
48
- ready: q.isReady(),
47
+ data: q.data,
48
+ ready: q.isReady,
49
49
  dispose
50
50
  };
51
51
  });
@@ -57,7 +57,7 @@ describe("createQuery", ()=>{
57
57
  expect(result.ready).toBe(true);
58
58
  result.dispose();
59
59
  });
60
- test("a failure lands on error() rather than being thrown", async ()=>{
60
+ test("a failure lands on error rather than being thrown", async ()=>{
61
61
  const result = await createRoot(async (dispose)=>{
62
62
  const q = createQuery(()=>({
63
63
  key: [
@@ -70,8 +70,8 @@ describe("createQuery", ()=>{
70
70
  flush();
71
71
  await tick();
72
72
  return {
73
- error: q.error(),
74
- loading: q.isLoading(),
73
+ error: q.error,
74
+ loading: q.isLoading,
75
75
  dispose
76
76
  };
77
77
  });
@@ -134,6 +134,33 @@ describe("createQuery", ()=>{
134
134
  expect(calls).toBe(1);
135
135
  });
136
136
  });
137
+ describe("property reads stay reactive", ()=>{
138
+ test("a tracked scope re-runs when data lands", async ()=>{
139
+ let resolveFetch;
140
+ const seen = [];
141
+ const result = await createRoot(async (dispose)=>{
142
+ const q = createQuery(()=>({
143
+ key: [
144
+ "reactive"
145
+ ],
146
+ fetcher: ()=>new Promise((r)=>resolveFetch = r)
147
+ }));
148
+ createRenderEffect(()=>q.data, (value)=>{
149
+ seen.push(value);
150
+ });
151
+ flush();
152
+ resolveFetch?.("arrived");
153
+ await tick();
154
+ flush();
155
+ return {
156
+ dispose
157
+ };
158
+ });
159
+ expect(seen[0]).toBeUndefined();
160
+ expect(seen.at(-1)).toBe("arrived");
161
+ result.dispose();
162
+ });
163
+ });
137
164
  describe("createMutation", ()=>{
138
165
  test("mutate reports failure without an unhandled rejection", async ()=>{
139
166
  const result = await createRoot(async (dispose)=>{
@@ -145,8 +172,8 @@ describe("createMutation", ()=>{
145
172
  m.mutate();
146
173
  await tick();
147
174
  return {
148
- error: m.error(),
149
- pending: m.isPending(),
175
+ error: m.error,
176
+ pending: m.isPending,
150
177
  dispose
151
178
  };
152
179
  });
@@ -166,7 +193,7 @@ describe("createMutation", ()=>{
166
193
  const value = await m.mutateAsync("app");
167
194
  return {
168
195
  value,
169
- data: m.data(),
196
+ data: m.data,
170
197
  dispose
171
198
  };
172
199
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "solid-layouts-library-v2",
3
3
  "package": "@pathscale/ui",
4
- "version": "2.12.0",
4
+ "version": "3.0.0",
5
5
  "components": {
6
6
  "Accordion": {
7
7
  "kind": "embedded"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pathscale/ui",
3
- "version": "2.12.0",
3
+ "version": "3.0.0",
4
4
  "author": "pathscale",
5
5
  "repository": {
6
6
  "type": "git",