@fuzdev/fuz_app 0.60.0 → 0.62.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.
Files changed (63) hide show
  1. package/dist/actions/CLAUDE.md +28 -22
  2. package/dist/auth/CLAUDE.md +4 -4
  3. package/dist/server/app_server.d.ts +54 -6
  4. package/dist/server/app_server.d.ts.map +1 -1
  5. package/dist/server/app_server.js +32 -4
  6. package/dist/testing/CLAUDE.md +8 -8
  7. package/dist/ui/AccountSessions.svelte +21 -6
  8. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  9. package/dist/ui/AdminAccounts.svelte +32 -25
  10. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  11. package/dist/ui/AdminAuditLog.svelte +3 -3
  12. package/dist/ui/AdminInvites.svelte +20 -15
  13. package/dist/ui/AdminOverview.svelte +19 -21
  14. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  15. package/dist/ui/AdminRoleGrantHistory.svelte +3 -3
  16. package/dist/ui/AdminSessions.svelte +19 -21
  17. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  18. package/dist/ui/AdminSettings.svelte +1 -3
  19. package/dist/ui/AdminSettings.svelte.d.ts.map +1 -1
  20. package/dist/ui/CLAUDE.md +123 -69
  21. package/dist/ui/ConfirmButton.svelte +82 -24
  22. package/dist/ui/ConfirmButton.svelte.d.ts +8 -34
  23. package/dist/ui/ConfirmButton.svelte.d.ts.map +1 -1
  24. package/dist/ui/OpenSignupToggle.svelte +6 -4
  25. package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
  26. package/dist/ui/RoleGrantOfferForm.svelte +4 -4
  27. package/dist/ui/RoleGrantOfferHistory.svelte +3 -3
  28. package/dist/ui/RoleGrantOfferInbox.svelte +10 -6
  29. package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -1
  30. package/dist/ui/account_sessions_state.svelte.d.ts +17 -7
  31. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  32. package/dist/ui/account_sessions_state.svelte.js +32 -33
  33. package/dist/ui/admin_accounts_state.svelte.d.ts +48 -17
  34. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  35. package/dist/ui/admin_accounts_state.svelte.js +58 -76
  36. package/dist/ui/admin_invites_state.svelte.d.ts +14 -7
  37. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
  38. package/dist/ui/admin_invites_state.svelte.js +32 -48
  39. package/dist/ui/admin_sessions_state.svelte.d.ts +15 -8
  40. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
  41. package/dist/ui/admin_sessions_state.svelte.js +30 -47
  42. package/dist/ui/app_settings_state.svelte.d.ts +8 -3
  43. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
  44. package/dist/ui/app_settings_state.svelte.js +19 -27
  45. package/dist/ui/async_slot.svelte.d.ts +173 -0
  46. package/dist/ui/async_slot.svelte.d.ts.map +1 -0
  47. package/dist/ui/async_slot.svelte.js +241 -0
  48. package/dist/ui/audit_log_state.svelte.d.ts +8 -2
  49. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  50. package/dist/ui/audit_log_state.svelte.js +19 -18
  51. package/dist/ui/keyed_async_slot.svelte.d.ts +139 -0
  52. package/dist/ui/keyed_async_slot.svelte.d.ts.map +1 -0
  53. package/dist/ui/keyed_async_slot.svelte.js +177 -0
  54. package/dist/ui/role_grant_offers_state.svelte.d.ts +39 -7
  55. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
  56. package/dist/ui/role_grant_offers_state.svelte.js +34 -15
  57. package/dist/ui/table_state.svelte.d.ts +10 -7
  58. package/dist/ui/table_state.svelte.d.ts.map +1 -1
  59. package/dist/ui/table_state.svelte.js +11 -8
  60. package/package.json +1 -1
  61. package/dist/ui/loadable.svelte.d.ts +0 -60
  62. package/dist/ui/loadable.svelte.d.ts.map +0 -1
  63. package/dist/ui/loadable.svelte.js +0 -80
@@ -1,11 +1,20 @@
1
1
  /**
2
2
  * Reactive state for admin account management.
3
3
  *
4
+ * Holds one fetch `AsyncSlot` (`list`) plus three `KeyedAsyncSlot`s —
5
+ * `grant` (offer creation, keyed by `account_id:role` or
6
+ * `account_id:role:to_actor_id`), `revoke` (role_grant revoke, keyed
7
+ * by `role_grant_id`), `retract` (offer retraction, keyed by
8
+ * `offer_id`). Per-row supersession is correct (clicking row B no
9
+ * longer aborts row A) and `error(key)` surfaces failure per-row.
10
+ * Method names use the `submit_*` prefix to avoid slot-name
11
+ * collisions.
12
+ *
4
13
  * @module
5
14
  */
6
- import { SvelteSet } from 'svelte/reactivity';
7
15
  import { create_context } from '@fuzdev/fuz_ui/context_helpers.js';
8
- import { Loadable } from './loadable.svelte.js';
16
+ import { AsyncSlot } from './async_slot.svelte.js';
17
+ import { KeyedAsyncSlot } from './keyed_async_slot.svelte.js';
9
18
  /**
10
19
  * Svelte context carrying the reactive `AdminAccountsRpc` accessor. The
11
20
  * provisioner (typically the admin route shell) calls `set(() => rpc)`;
@@ -17,16 +26,23 @@ import { Loadable } from './loadable.svelte.js';
17
26
  * not wired" path.
18
27
  */
19
28
  export const admin_accounts_rpc_context = create_context(() => () => null);
20
- export class AdminAccountsState extends Loadable {
29
+ /**
30
+ * Compose the `grant` keyed-slot key for an offer. Account-grain offers
31
+ * key on `${account_id}:${role}`; actor-targeted offers add the actor
32
+ * suffix so the two variants can be in flight simultaneously without
33
+ * colliding on per-row spinners.
34
+ */
35
+ export const grant_key = (account_id, role, to_actor_id) => to_actor_id ? `${account_id}:${role}:${to_actor_id}` : `${account_id}:${role}`;
36
+ export class AdminAccountsState {
21
37
  #get_rpc;
38
+ list = new AsyncSlot();
39
+ grant = new KeyedAsyncSlot();
40
+ revoke = new KeyedAsyncSlot();
41
+ retract = new KeyedAsyncSlot();
22
42
  accounts = $state.raw([]);
23
43
  grantable_roles = $state.raw([]);
24
- granting_keys = new SvelteSet();
25
- revoking_ids = new SvelteSet();
26
- retracting_ids = new SvelteSet();
27
44
  account_count = $derived(this.accounts.length);
28
45
  constructor(options) {
29
- super();
30
46
  this.#get_rpc = options?.get_rpc ?? (() => null);
31
47
  }
32
48
  /**
@@ -36,14 +52,15 @@ export class AdminAccountsState extends Loadable {
36
52
  get has_rpc() {
37
53
  return this.#get_rpc() !== null;
38
54
  }
39
- async fetch() {
55
+ #require_rpc() {
40
56
  const rpc = this.#get_rpc();
41
- if (!rpc) {
42
- this.error = 'rpc adapter not wired';
43
- return;
44
- }
45
- await this.run(async () => {
46
- const { accounts, grantable_roles } = await rpc.list_accounts();
57
+ if (!rpc)
58
+ throw new Error('rpc adapter not wired');
59
+ return rpc;
60
+ }
61
+ async fetch() {
62
+ await this.list.run(async () => {
63
+ const { accounts, grantable_roles } = await this.#require_rpc().list_accounts();
47
64
  this.accounts = accounts;
48
65
  this.grantable_roles = grantable_roles;
49
66
  });
@@ -59,40 +76,25 @@ export class AdminAccountsState extends Loadable {
59
76
  * across those calls.
60
77
  *
61
78
  * `to_actor_id` (optional) narrows the offer to a specific actor on
62
- * `account_id`; the in-flight `granting_keys` entry stays at
63
- * `account_id:role` for the account-grain default (so existing
64
- * consumers reading the 2-segment key keep working) and becomes
65
- * `account_id:role:to_actor_id` when actor-targeted, so the two
66
- * variants can be in flight without colliding on the per-row spinner.
67
- *
68
- * No-op when the rpc adapter is absent; `error` is set to a descriptive
69
- * message so the UI surfaces the misconfiguration.
79
+ * `account_id`; the keyed-slot key stays at `account_id:role` for the
80
+ * account-grain default (so existing consumers keep working) and
81
+ * becomes `account_id:role:to_actor_id` when actor-targeted, so the
82
+ * two variants can be in flight without colliding on the per-row
83
+ * spinner.
70
84
  */
71
- async create_role_grant(account_id, role, to_actor_id) {
72
- const rpc = this.#get_rpc();
73
- if (!rpc) {
74
- this.error = 'rpc adapter not wired';
75
- return undefined;
76
- }
77
- const key = to_actor_id ? `${account_id}:${role}:${to_actor_id}` : `${account_id}:${role}`;
78
- this.granting_keys.add(key);
79
- try {
80
- const { offer } = await rpc.create_role_grant({
85
+ async submit_grant(account_id, role, to_actor_id) {
86
+ const key = grant_key(account_id, role, to_actor_id);
87
+ const offer = await this.grant.run(key, async () => {
88
+ const result = await this.#require_rpc().create_role_grant({
81
89
  to_account_id: account_id,
82
90
  role,
83
91
  ...(to_actor_id ? { to_actor_id } : {}),
84
92
  });
85
- this.error = null;
93
+ return result.offer;
94
+ });
95
+ if (offer)
86
96
  await this.fetch();
87
- return offer;
88
- }
89
- catch (e) {
90
- this.error = e instanceof Error ? e.message : 'Failed to grant role_grant';
91
- return undefined;
92
- }
93
- finally {
94
- this.granting_keys.delete(key);
95
- }
97
+ return offer;
96
98
  }
97
99
  /**
98
100
  * Revoke an active role_grant via the `role_grant_revoke` RPC.
@@ -103,24 +105,16 @@ export class AdminAccountsState extends Loadable {
103
105
  * The optional `reason` is stamped on `role_grant.revoked_reason` and
104
106
  * surfaced on the revokee's WS notification.
105
107
  */
106
- async revoke_role_grant(actor_id, role_grant_id, reason) {
107
- const rpc = this.#get_rpc();
108
- if (!rpc) {
109
- this.error = 'rpc adapter not wired';
110
- return;
111
- }
112
- this.revoking_ids.add(role_grant_id);
113
- try {
114
- await rpc.revoke_role_grant({ actor_id, role_grant_id, reason: reason ?? null });
115
- this.error = null;
108
+ async submit_revoke(actor_id, role_grant_id, reason) {
109
+ await this.revoke.run(role_grant_id, async () => {
110
+ await this.#require_rpc().revoke_role_grant({
111
+ actor_id,
112
+ role_grant_id,
113
+ reason: reason ?? null,
114
+ });
115
+ });
116
+ if (this.revoke.succeeded(role_grant_id))
116
117
  await this.fetch();
117
- }
118
- catch (e) {
119
- this.error = e instanceof Error ? e.message : 'Failed to revoke role_grant';
120
- }
121
- finally {
122
- this.revoking_ids.delete(role_grant_id);
123
- }
124
118
  }
125
119
  /**
126
120
  * Retract a pending offer the admin issued via the `role_grant_offer_retract`
@@ -130,23 +124,11 @@ export class AdminAccountsState extends Loadable {
130
124
  * After success, refetches the listing so `pending_offers` drops the
131
125
  * row and the "+ {role}" button un-hides.
132
126
  */
133
- async retract_offer(offer_id) {
134
- const rpc = this.#get_rpc();
135
- if (!rpc) {
136
- this.error = 'rpc adapter not wired';
137
- return;
138
- }
139
- this.retracting_ids.add(offer_id);
140
- try {
141
- await rpc.retract_offer(offer_id);
142
- this.error = null;
127
+ async submit_retract(offer_id) {
128
+ await this.retract.run(offer_id, async () => {
129
+ await this.#require_rpc().retract_offer(offer_id);
130
+ });
131
+ if (this.retract.succeeded(offer_id))
143
132
  await this.fetch();
144
- }
145
- catch (e) {
146
- this.error = e instanceof Error ? e.message : 'Failed to retract offer';
147
- }
148
- finally {
149
- this.retracting_ids.delete(offer_id);
150
- }
151
133
  }
152
134
  }
@@ -5,11 +5,17 @@
5
5
  * class stays decoupled from the concrete RPC client so tests can inject
6
6
  * plain-function stubs. Mirrors `AdminAccountsRpc` / `AuditLogRpc`.
7
7
  *
8
+ * Holds two `AsyncSlot`s — `list` (fetch) and `create` (singular write) —
9
+ * plus one `KeyedAsyncSlot<Uuid>` (`remove`) for the per-row delete with
10
+ * correct per-row supersession and per-row error surfacing. Method names
11
+ * use the `submit_*` prefix to avoid slot-name collisions (`delete` is
12
+ * reserved at top-level positions; renamed for symmetry).
13
+ *
8
14
  * @module
9
15
  */
10
- import { SvelteSet } from 'svelte/reactivity';
11
16
  import type { Uuid } from '@fuzdev/fuz_util/id.js';
12
- import { Loadable } from './loadable.svelte.js';
17
+ import { AsyncSlot } from './async_slot.svelte.js';
18
+ import { KeyedAsyncSlot } from './keyed_async_slot.svelte.js';
13
19
  import type { InviteWithUsernamesJson } from '../auth/invite_schema.js';
14
20
  import type { InviteCreateInput, InviteCreateOutput, InviteDeleteInput, InviteDeleteOutput, InviteListOutput } from '../auth/admin_action_specs.js';
15
21
  /**
@@ -39,18 +45,19 @@ export interface AdminInvitesStateOptions {
39
45
  */
40
46
  get_rpc?: () => AdminInvitesRpc | null;
41
47
  }
42
- export declare class AdminInvitesState extends Loadable {
48
+ export declare class AdminInvitesState {
43
49
  #private;
50
+ readonly list: AsyncSlot<void, string>;
51
+ readonly create: AsyncSlot<void, string>;
52
+ readonly remove: KeyedAsyncSlot<string & import("zod").$brand<"Uuid">, void, string>;
44
53
  invites: Array<InviteWithUsernamesJson>;
45
- creating: boolean;
46
- readonly deleting_ids: SvelteSet<string>;
47
54
  readonly invite_count: number;
48
55
  readonly unclaimed_count: number;
49
56
  constructor(options?: AdminInvitesStateOptions);
50
57
  /** True when an RPC adapter is wired. All ops require it. */
51
58
  get has_rpc(): boolean;
52
59
  fetch(): Promise<void>;
53
- create_invite(email?: string, username?: string): Promise<boolean>;
54
- delete_invite(id: Uuid): Promise<void>;
60
+ submit_create(email?: string, username?: string): Promise<boolean>;
61
+ submit_delete(id: Uuid): Promise<void>;
55
62
  }
56
63
  //# sourceMappingURL=admin_invites_state.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin_invites_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/admin_invites_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,SAAS,EAAC,MAAM,mBAAmB,CAAC;AAE5C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AAC9C,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,0BAA0B,CAAC;AACtE,OAAO,KAAK,EACX,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,MAAM,+BAA+B,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtC,MAAM,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACnE,MAAM,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACnE;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB;qBAAwB,eAAe,GAAG,IAAI;yBAAtB,eAAe,GAAG,IAAI,wBAAtB,eAAe,GAAG,IAAI;CAEnF,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CACvC;AAED,qBAAa,iBAAkB,SAAQ,QAAQ;;IAG9C,OAAO,EAAE,KAAK,CAAC,uBAAuB,CAAC,CAAkB;IACzD,QAAQ,UAAqB;IAC7B,QAAQ,CAAC,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,CAAmB;IAE3D,QAAQ,CAAC,YAAY,SAAiC;IACtD,QAAQ,CAAC,eAAe,SAA8D;gBAE1E,OAAO,CAAC,EAAE,wBAAwB;IAK9C,6DAA6D;IAC7D,IAAI,OAAO,IAAI,OAAO,CAErB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBlE,aAAa,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAgB5C"}
1
+ {"version":3,"file":"admin_invites_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/admin_invites_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAC,SAAS,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,0BAA0B,CAAC;AACtE,OAAO,KAAK,EACX,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,MAAM,+BAA+B,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtC,MAAM,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACnE,MAAM,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACnE;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB;qBAAwB,eAAe,GAAG,IAAI;yBAAtB,eAAe,GAAG,IAAI,wBAAtB,eAAe,GAAG,IAAI;CAEnF,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CACvC;AAED,qBAAa,iBAAiB;;IAG7B,QAAQ,CAAC,IAAI,0BAAyB;IACtC,QAAQ,CAAC,MAAM,0BAAyB;IACxC,QAAQ,CAAC,MAAM,sEAAoC;IAEnD,OAAO,EAAE,KAAK,CAAC,uBAAuB,CAAC,CAAkB;IAEzD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAiC;IAC9D,QAAQ,CAAC,eAAe,EAAE,MAAM,CAA8D;gBAElF,OAAO,CAAC,EAAE,wBAAwB;IAI9C,6DAA6D;IAC7D,IAAI,OAAO,IAAI,OAAO,CAErB;IAQK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASlE,aAAa,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAM5C"}
@@ -5,79 +5,63 @@
5
5
  * class stays decoupled from the concrete RPC client so tests can inject
6
6
  * plain-function stubs. Mirrors `AdminAccountsRpc` / `AuditLogRpc`.
7
7
  *
8
+ * Holds two `AsyncSlot`s — `list` (fetch) and `create` (singular write) —
9
+ * plus one `KeyedAsyncSlot<Uuid>` (`remove`) for the per-row delete with
10
+ * correct per-row supersession and per-row error surfacing. Method names
11
+ * use the `submit_*` prefix to avoid slot-name collisions (`delete` is
12
+ * reserved at top-level positions; renamed for symmetry).
13
+ *
8
14
  * @module
9
15
  */
10
- import { SvelteSet } from 'svelte/reactivity';
11
16
  import { create_context } from '@fuzdev/fuz_ui/context_helpers.js';
12
- import { Loadable } from './loadable.svelte.js';
17
+ import { AsyncSlot } from './async_slot.svelte.js';
18
+ import { KeyedAsyncSlot } from './keyed_async_slot.svelte.js';
13
19
  /**
14
20
  * Svelte context carrying the reactive `AdminInvitesRpc` accessor. Mirrors
15
21
  * `admin_accounts_rpc_context`. Unset context falls back to `() => null`.
16
22
  */
17
23
  export const admin_invites_rpc_context = create_context(() => () => null);
18
- export class AdminInvitesState extends Loadable {
24
+ export class AdminInvitesState {
19
25
  #get_rpc;
26
+ list = new AsyncSlot();
27
+ create = new AsyncSlot();
28
+ remove = new KeyedAsyncSlot();
20
29
  invites = $state.raw([]);
21
- creating = $state.raw(false);
22
- deleting_ids = new SvelteSet();
23
30
  invite_count = $derived(this.invites.length);
24
31
  unclaimed_count = $derived(this.invites.filter((i) => !i.claimed_at).length);
25
32
  constructor(options) {
26
- super();
27
33
  this.#get_rpc = options?.get_rpc ?? (() => null);
28
34
  }
29
35
  /** True when an RPC adapter is wired. All ops require it. */
30
36
  get has_rpc() {
31
37
  return this.#get_rpc() !== null;
32
38
  }
33
- async fetch() {
39
+ #require_rpc() {
34
40
  const rpc = this.#get_rpc();
35
- if (!rpc) {
36
- this.error = 'rpc adapter not wired';
37
- return;
38
- }
39
- await this.run(async () => {
40
- const { invites } = await rpc.list();
41
+ if (!rpc)
42
+ throw new Error('rpc adapter not wired');
43
+ return rpc;
44
+ }
45
+ async fetch() {
46
+ await this.list.run(async () => {
47
+ const { invites } = await this.#require_rpc().list();
41
48
  this.invites = invites;
42
49
  });
43
50
  }
44
- async create_invite(email, username) {
45
- const rpc = this.#get_rpc();
46
- if (!rpc) {
47
- this.error = 'rpc adapter not wired';
48
- return false;
49
- }
50
- this.creating = true;
51
- this.error = null;
52
- try {
53
- await rpc.create({ email: email ?? null, username: username ?? null });
54
- await this.fetch();
55
- return true;
56
- }
57
- catch (e) {
58
- this.error = e instanceof Error ? e.message : 'Failed to create invite';
51
+ async submit_create(email, username) {
52
+ await this.create.run(async () => {
53
+ await this.#require_rpc().create({ email: email ?? null, username: username ?? null });
54
+ });
55
+ if (!this.create.succeeded)
59
56
  return false;
60
- }
61
- finally {
62
- this.creating = false;
63
- }
57
+ await this.fetch();
58
+ return true;
64
59
  }
65
- async delete_invite(id) {
66
- const rpc = this.#get_rpc();
67
- if (!rpc) {
68
- this.error = 'rpc adapter not wired';
69
- return;
70
- }
71
- this.deleting_ids.add(id);
72
- try {
73
- await rpc.delete({ invite_id: id });
60
+ async submit_delete(id) {
61
+ await this.remove.run(id, async () => {
62
+ await this.#require_rpc().delete({ invite_id: id });
63
+ });
64
+ if (this.remove.succeeded(id))
74
65
  await this.fetch();
75
- }
76
- catch (e) {
77
- this.error = e instanceof Error ? e.message : 'Failed to delete invite';
78
- }
79
- finally {
80
- this.deleting_ids.delete(id);
81
- }
82
66
  }
83
67
  }
@@ -6,18 +6,24 @@
6
6
  * `token_revoke_all`); the listing wraps the `admin_session_list` RPC
7
7
  * method.
8
8
  *
9
+ * Holds one fetch `AsyncSlot` (`list`) plus two `KeyedAsyncSlot`s keyed by
10
+ * `account_id` — `revoke_sessions` and `revoke_tokens`. Per-account
11
+ * concurrent revokes are independent (clicking row B does not abort row A)
12
+ * and per-row errors surface via `revoke_sessions.error(account_id)` /
13
+ * `revoke_tokens.error(account_id)`.
14
+ *
9
15
  * @module
10
16
  */
11
- import { SvelteSet } from 'svelte/reactivity';
12
17
  import type { Uuid } from '@fuzdev/fuz_util/id.js';
13
- import { Loadable } from './loadable.svelte.js';
18
+ import { AsyncSlot } from './async_slot.svelte.js';
19
+ import { KeyedAsyncSlot } from './keyed_async_slot.svelte.js';
14
20
  import type { AdminAccountsRpc } from './admin_accounts_state.svelte.js';
15
21
  import type { AdminSessionJson } from '../auth/audit_log_schema.js';
16
22
  /**
17
23
  * Options for `AdminSessionsState`.
18
24
  *
19
25
  * The RPC adapter drives every operation (listing + the two revoke-all
20
- * mutations). Without it, `fetch` and the revoke controls no-op with
26
+ * mutations). Without it, the slots' `run()` calls fail with
21
27
  * `'rpc adapter not wired'` on `error`.
22
28
  */
23
29
  export interface AdminSessionsStateOptions {
@@ -29,17 +35,18 @@ export interface AdminSessionsStateOptions {
29
35
  */
30
36
  get_rpc?: () => AdminAccountsRpc | null;
31
37
  }
32
- export declare class AdminSessionsState extends Loadable {
38
+ export declare class AdminSessionsState {
33
39
  #private;
40
+ readonly list: AsyncSlot<void, string>;
41
+ readonly revoke_sessions: KeyedAsyncSlot<string & import("zod").$brand<"Uuid">, void, string>;
42
+ readonly revoke_tokens: KeyedAsyncSlot<string & import("zod").$brand<"Uuid">, void, string>;
34
43
  sessions: Array<AdminSessionJson>;
35
- readonly revoking_account_ids: SvelteSet<string>;
36
- readonly revoking_token_account_ids: SvelteSet<string>;
37
44
  readonly active_count: number;
38
45
  constructor(options?: AdminSessionsStateOptions);
39
46
  /** True when an RPC adapter is wired. `fetch` and the revoke controls no-op without it. */
40
47
  get has_rpc(): boolean;
41
48
  fetch(): Promise<void>;
42
- revoke_all_for_account(account_id: Uuid): Promise<void>;
43
- revoke_all_tokens_for_account(account_id: Uuid): Promise<void>;
49
+ submit_revoke_sessions(account_id: Uuid): Promise<void>;
50
+ submit_revoke_tokens(account_id: Uuid): Promise<void>;
44
51
  }
45
52
  //# sourceMappingURL=admin_sessions_state.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin_sessions_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/admin_sessions_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAC,SAAS,EAAC,MAAM,mBAAmB,CAAC;AAC5C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AAC9C,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,6BAA6B,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACzC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;CACxC;AAED,qBAAa,kBAAmB,SAAQ,QAAQ;;IAG/C,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAkB;IACnD,QAAQ,CAAC,oBAAoB,EAAE,SAAS,CAAC,MAAM,CAAC,CAAmB;IACnE,QAAQ,CAAC,0BAA0B,EAAE,SAAS,CAAC,MAAM,CAAC,CAAmB;IAEzE,QAAQ,CAAC,YAAY,SAAkC;gBAE3C,OAAO,CAAC,EAAE,yBAAyB;IAK/C,2FAA2F;IAC3F,IAAI,OAAO,IAAI,OAAO,CAErB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,sBAAsB,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvD,6BAA6B,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAiBpE"}
1
+ {"version":3,"file":"admin_sessions_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/admin_sessions_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAC,SAAS,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,6BAA6B,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACzC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;CACxC;AAED,qBAAa,kBAAkB;;IAG9B,QAAQ,CAAC,IAAI,0BAAyB;IACtC,QAAQ,CAAC,eAAe,sEAAoC;IAC5D,QAAQ,CAAC,aAAa,sEAAoC;IAE1D,QAAQ,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAkB;IAEnD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAkC;gBAEnD,OAAO,CAAC,EAAE,yBAAyB;IAI/C,2FAA2F;IAC3F,IAAI,OAAO,IAAI,OAAO,CAErB;IAQK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,sBAAsB,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvD,oBAAoB,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAM3D"}
@@ -6,71 +6,54 @@
6
6
  * `token_revoke_all`); the listing wraps the `admin_session_list` RPC
7
7
  * method.
8
8
  *
9
+ * Holds one fetch `AsyncSlot` (`list`) plus two `KeyedAsyncSlot`s keyed by
10
+ * `account_id` — `revoke_sessions` and `revoke_tokens`. Per-account
11
+ * concurrent revokes are independent (clicking row B does not abort row A)
12
+ * and per-row errors surface via `revoke_sessions.error(account_id)` /
13
+ * `revoke_tokens.error(account_id)`.
14
+ *
9
15
  * @module
10
16
  */
11
- import { SvelteSet } from 'svelte/reactivity';
12
- import { Loadable } from './loadable.svelte.js';
13
- export class AdminSessionsState extends Loadable {
17
+ import { AsyncSlot } from './async_slot.svelte.js';
18
+ import { KeyedAsyncSlot } from './keyed_async_slot.svelte.js';
19
+ export class AdminSessionsState {
14
20
  #get_rpc;
21
+ list = new AsyncSlot();
22
+ revoke_sessions = new KeyedAsyncSlot();
23
+ revoke_tokens = new KeyedAsyncSlot();
15
24
  sessions = $state.raw([]);
16
- revoking_account_ids = new SvelteSet();
17
- revoking_token_account_ids = new SvelteSet();
18
25
  active_count = $derived(this.sessions.length);
19
26
  constructor(options) {
20
- super();
21
27
  this.#get_rpc = options?.get_rpc ?? (() => null);
22
28
  }
23
29
  /** True when an RPC adapter is wired. `fetch` and the revoke controls no-op without it. */
24
30
  get has_rpc() {
25
31
  return this.#get_rpc() !== null;
26
32
  }
27
- async fetch() {
33
+ #require_rpc() {
28
34
  const rpc = this.#get_rpc();
29
- if (!rpc) {
30
- this.error = 'rpc adapter not wired';
31
- return;
32
- }
33
- await this.run(async () => {
34
- const { sessions } = await rpc.list_sessions();
35
+ if (!rpc)
36
+ throw new Error('rpc adapter not wired');
37
+ return rpc;
38
+ }
39
+ async fetch() {
40
+ await this.list.run(async () => {
41
+ const { sessions } = await this.#require_rpc().list_sessions();
35
42
  this.sessions = sessions;
36
43
  });
37
44
  }
38
- async revoke_all_for_account(account_id) {
39
- const rpc = this.#get_rpc();
40
- if (!rpc) {
41
- this.error = 'rpc adapter not wired';
42
- return;
43
- }
44
- this.revoking_account_ids.add(account_id);
45
- try {
46
- await rpc.session_revoke_all({ account_id });
47
- this.error = null;
45
+ async submit_revoke_sessions(account_id) {
46
+ await this.revoke_sessions.run(account_id, async () => {
47
+ await this.#require_rpc().session_revoke_all({ account_id });
48
+ });
49
+ if (this.revoke_sessions.succeeded(account_id))
48
50
  await this.fetch();
49
- }
50
- catch (e) {
51
- this.error = e instanceof Error ? e.message : 'Failed to revoke sessions';
52
- }
53
- finally {
54
- this.revoking_account_ids.delete(account_id);
55
- }
56
51
  }
57
- async revoke_all_tokens_for_account(account_id) {
58
- const rpc = this.#get_rpc();
59
- if (!rpc) {
60
- this.error = 'rpc adapter not wired';
61
- return;
62
- }
63
- this.revoking_token_account_ids.add(account_id);
64
- try {
65
- await rpc.token_revoke_all({ account_id });
66
- this.error = null;
52
+ async submit_revoke_tokens(account_id) {
53
+ await this.revoke_tokens.run(account_id, async () => {
54
+ await this.#require_rpc().token_revoke_all({ account_id });
55
+ });
56
+ if (this.revoke_tokens.succeeded(account_id))
67
57
  await this.fetch();
68
- }
69
- catch (e) {
70
- this.error = e instanceof Error ? e.message : 'Failed to revoke tokens';
71
- }
72
- finally {
73
- this.revoking_token_account_ids.delete(account_id);
74
- }
75
58
  }
76
59
  }
@@ -5,9 +5,13 @@
5
5
  * `AdminInvitesRpc` / `AuditLogRpc`. Tests can inject plain-function stubs
6
6
  * and consumers adapt their typed RPC client to the same shape.
7
7
  *
8
+ * Holds two `AsyncSlot`s — `list` (the initial fetch) and `update` (the
9
+ * `app_settings_update` write). Slots track status/error; the canonical
10
+ * `settings` lives on the class so consumers don't unwrap `slot.data`.
11
+ *
8
12
  * @module
9
13
  */
10
- import { Loadable } from './loadable.svelte.js';
14
+ import { AsyncSlot } from './async_slot.svelte.js';
11
15
  import type { AppSettingsWithUsernameJson } from '../auth/app_settings_schema.js';
12
16
  import type { AppSettingsGetOutput, AppSettingsUpdateInput, AppSettingsUpdateOutput } from '../auth/admin_action_specs.js';
13
17
  /**
@@ -35,10 +39,11 @@ export interface AppSettingsStateOptions {
35
39
  */
36
40
  get_rpc?: () => AppSettingsRpc | null;
37
41
  }
38
- export declare class AppSettingsState extends Loadable {
42
+ export declare class AppSettingsState {
39
43
  #private;
44
+ readonly list: AsyncSlot<void, string>;
45
+ readonly update: AsyncSlot<void, string>;
40
46
  settings: AppSettingsWithUsernameJson | null;
41
- updating: boolean;
42
47
  constructor(options?: AppSettingsStateOptions);
43
48
  /** True when an RPC adapter is wired. All ops require it. */
44
49
  get has_rpc(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"app_settings_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/app_settings_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AAC9C,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,gCAAgC,CAAC;AAChF,OAAO,KAAK,EACX,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,MAAM,+BAA+B,CAAC;AAEvC;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACzC,MAAM,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC7E;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;qBAAwB,cAAc,GAAG,IAAI;yBAArB,cAAc,GAAG,IAAI,wBAArB,cAAc,GAAG,IAAI;CAEjF,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,cAAc,GAAG,IAAI,CAAC;CACtC;AAED,qBAAa,gBAAiB,SAAQ,QAAQ;;IAG7C,QAAQ,EAAE,2BAA2B,GAAG,IAAI,CAAoB;IAChE,QAAQ,UAAqB;gBAEjB,OAAO,CAAC,EAAE,uBAAuB;IAK7C,6DAA6D;IAC7D,IAAI,OAAO,IAAI,OAAO,CAErB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAiBvD"}
1
+ {"version":3,"file":"app_settings_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/app_settings_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EAAC,SAAS,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,gCAAgC,CAAC;AAChF,OAAO,KAAK,EACX,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,MAAM,+BAA+B,CAAC;AAEvC;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACzC,MAAM,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC7E;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;qBAAwB,cAAc,GAAG,IAAI;yBAArB,cAAc,GAAG,IAAI,wBAArB,cAAc,GAAG,IAAI;CAEjF,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,cAAc,GAAG,IAAI,CAAC;CACtC;AAED,qBAAa,gBAAgB;;IAG5B,QAAQ,CAAC,IAAI,0BAAyB;IACtC,QAAQ,CAAC,MAAM,0BAAyB;IAExC,QAAQ,EAAE,2BAA2B,GAAG,IAAI,CAAoB;gBAEpD,OAAO,CAAC,EAAE,uBAAuB;IAI7C,6DAA6D;IAC7D,IAAI,OAAO,IAAI,OAAO,CAErB;IAQK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAMvD"}
@@ -5,56 +5,48 @@
5
5
  * `AdminInvitesRpc` / `AuditLogRpc`. Tests can inject plain-function stubs
6
6
  * and consumers adapt their typed RPC client to the same shape.
7
7
  *
8
+ * Holds two `AsyncSlot`s — `list` (the initial fetch) and `update` (the
9
+ * `app_settings_update` write). Slots track status/error; the canonical
10
+ * `settings` lives on the class so consumers don't unwrap `slot.data`.
11
+ *
8
12
  * @module
9
13
  */
10
14
  import { create_context } from '@fuzdev/fuz_ui/context_helpers.js';
11
- import { Loadable } from './loadable.svelte.js';
15
+ import { AsyncSlot } from './async_slot.svelte.js';
12
16
  /**
13
17
  * Svelte context carrying the reactive `AppSettingsRpc` accessor. Mirrors
14
18
  * `admin_accounts_rpc_context`. Unset context falls back to `() => null` so
15
19
  * `OpenSignupToggle` mounted outside a provisioner hides gracefully.
16
20
  */
17
21
  export const app_settings_rpc_context = create_context(() => () => null);
18
- export class AppSettingsState extends Loadable {
22
+ export class AppSettingsState {
19
23
  #get_rpc;
24
+ list = new AsyncSlot();
25
+ update = new AsyncSlot();
20
26
  settings = $state.raw(null);
21
- updating = $state.raw(false);
22
27
  constructor(options) {
23
- super();
24
28
  this.#get_rpc = options?.get_rpc ?? (() => null);
25
29
  }
26
30
  /** True when an RPC adapter is wired. All ops require it. */
27
31
  get has_rpc() {
28
32
  return this.#get_rpc() !== null;
29
33
  }
30
- async fetch() {
34
+ #require_rpc() {
31
35
  const rpc = this.#get_rpc();
32
- if (!rpc) {
33
- this.error = 'rpc adapter not wired';
34
- return;
35
- }
36
- await this.run(async () => {
37
- const { settings } = await rpc.get();
36
+ if (!rpc)
37
+ throw new Error('rpc adapter not wired');
38
+ return rpc;
39
+ }
40
+ async fetch() {
41
+ await this.list.run(async () => {
42
+ const { settings } = await this.#require_rpc().get();
38
43
  this.settings = settings;
39
44
  });
40
45
  }
41
46
  async update_open_signup(value) {
42
- const rpc = this.#get_rpc();
43
- if (!rpc) {
44
- this.error = 'rpc adapter not wired';
45
- return;
46
- }
47
- this.updating = true;
48
- this.error = null;
49
- try {
50
- const { settings } = await rpc.update({ open_signup: value });
47
+ await this.update.run(async () => {
48
+ const { settings } = await this.#require_rpc().update({ open_signup: value });
51
49
  this.settings = settings;
52
- }
53
- catch (e) {
54
- this.error = e instanceof Error ? e.message : 'Failed to update settings';
55
- }
56
- finally {
57
- this.updating = false;
58
- }
50
+ });
59
51
  }
60
52
  }