@happyvertical/smrt-svelte 0.37.9 → 0.37.10

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 (86) hide show
  1. package/AGENTS.md +21 -85
  2. package/README.md +14 -10
  3. package/dist/components/workspace/MIGRATION.md +55 -0
  4. package/dist/components/workspace/README.md +22 -25
  5. package/dist/components/workspace/__tests__/AdminShell.test.js +55 -0
  6. package/dist/components/workspace/__tests__/admin-shell-hotkeys.test.js +34 -0
  7. package/dist/components/workspace/__tests__/admin-shell-settings.test.js +25 -0
  8. package/dist/components/workspace/__tests__/admin-shell-state.test.js +110 -0
  9. package/dist/components/workspace/__tests__/index.test.js +15 -24
  10. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts +2 -1
  11. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts.map +1 -1
  12. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.js +1 -1
  13. package/dist/components/workspace/admin-shell/ActivityBadge.svelte +25 -0
  14. package/dist/components/workspace/admin-shell/ActivityBadge.svelte.d.ts +9 -0
  15. package/dist/components/workspace/admin-shell/ActivityBadge.svelte.d.ts.map +1 -0
  16. package/dist/components/workspace/admin-shell/ActivityItem.svelte +85 -0
  17. package/dist/components/workspace/admin-shell/ActivityItem.svelte.d.ts +8 -0
  18. package/dist/components/workspace/admin-shell/ActivityItem.svelte.d.ts.map +1 -0
  19. package/dist/components/workspace/admin-shell/ActivityList.svelte +41 -0
  20. package/dist/components/workspace/admin-shell/ActivityList.svelte.d.ts +10 -0
  21. package/dist/components/workspace/admin-shell/ActivityList.svelte.d.ts.map +1 -0
  22. package/dist/components/workspace/admin-shell/ActivityToasts.svelte +140 -0
  23. package/dist/components/workspace/admin-shell/ActivityToasts.svelte.d.ts +16 -0
  24. package/dist/components/workspace/admin-shell/ActivityToasts.svelte.d.ts.map +1 -0
  25. package/dist/components/workspace/admin-shell/AdminShell.svelte +691 -0
  26. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +24 -0
  27. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -0
  28. package/dist/components/workspace/admin-shell/AppScopePanel.svelte +94 -0
  29. package/dist/components/workspace/admin-shell/AppScopePanel.svelte.d.ts +14 -0
  30. package/dist/components/workspace/admin-shell/AppScopePanel.svelte.d.ts.map +1 -0
  31. package/dist/components/workspace/admin-shell/HotkeyInput.svelte +74 -0
  32. package/dist/components/workspace/admin-shell/HotkeyInput.svelte.d.ts +11 -0
  33. package/dist/components/workspace/admin-shell/HotkeyInput.svelte.d.ts.map +1 -0
  34. package/dist/components/workspace/admin-shell/ShellCorner.svelte +25 -0
  35. package/dist/components/workspace/admin-shell/ShellCorner.svelte.d.ts +9 -0
  36. package/dist/components/workspace/admin-shell/ShellCorner.svelte.d.ts.map +1 -0
  37. package/dist/components/workspace/admin-shell/ShellDockTool.svelte +19 -0
  38. package/dist/components/workspace/admin-shell/ShellDockTool.svelte.d.ts +7 -0
  39. package/dist/components/workspace/admin-shell/ShellDockTool.svelte.d.ts.map +1 -0
  40. package/dist/components/workspace/admin-shell/ShellSettingsPanel.svelte +146 -0
  41. package/dist/components/workspace/admin-shell/ShellSettingsPanel.svelte.d.ts +4 -0
  42. package/dist/components/workspace/admin-shell/ShellSettingsPanel.svelte.d.ts.map +1 -0
  43. package/dist/components/workspace/admin-shell/ShortcutsOverlay.svelte +81 -0
  44. package/dist/components/workspace/admin-shell/ShortcutsOverlay.svelte.d.ts +7 -0
  45. package/dist/components/workspace/admin-shell/ShortcutsOverlay.svelte.d.ts.map +1 -0
  46. package/dist/components/workspace/admin-shell/SystemScopePanel.svelte +118 -0
  47. package/dist/components/workspace/admin-shell/SystemScopePanel.svelte.d.ts +9 -0
  48. package/dist/components/workspace/admin-shell/SystemScopePanel.svelte.d.ts.map +1 -0
  49. package/dist/components/workspace/admin-shell/SystemStatusChips.svelte +71 -0
  50. package/dist/components/workspace/admin-shell/SystemStatusChips.svelte.d.ts +8 -0
  51. package/dist/components/workspace/admin-shell/SystemStatusChips.svelte.d.ts.map +1 -0
  52. package/dist/components/workspace/admin-shell/TenantNav.svelte +101 -0
  53. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +10 -0
  54. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -0
  55. package/dist/components/workspace/admin-shell/context.d.ts +6 -0
  56. package/dist/components/workspace/admin-shell/context.d.ts.map +1 -0
  57. package/dist/components/workspace/admin-shell/context.js +16 -0
  58. package/dist/components/workspace/admin-shell/hotkeys.d.ts +14 -0
  59. package/dist/components/workspace/admin-shell/hotkeys.d.ts.map +1 -0
  60. package/dist/components/workspace/admin-shell/hotkeys.js +62 -0
  61. package/dist/components/workspace/admin-shell/settings.d.ts +14 -0
  62. package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -0
  63. package/dist/components/workspace/admin-shell/settings.js +139 -0
  64. package/dist/components/workspace/admin-shell/state.svelte.d.ts +47 -0
  65. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -0
  66. package/dist/components/workspace/admin-shell/state.svelte.js +252 -0
  67. package/dist/components/workspace/admin-shell/types.d.ts +137 -0
  68. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -0
  69. package/dist/components/workspace/admin-shell/types.js +13 -0
  70. package/dist/components/workspace/index.d.ts +25 -11
  71. package/dist/components/workspace/index.d.ts.map +1 -1
  72. package/dist/components/workspace/index.js +24 -10
  73. package/dist/i18n/strings.workspace.d.ts +30 -0
  74. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  75. package/dist/i18n/strings.workspace.js +31 -0
  76. package/dist/web/__tests__/harness.svelte +25 -0
  77. package/dist/web/__tests__/harness.svelte.d.ts +15 -0
  78. package/dist/web/__tests__/harness.svelte.d.ts.map +1 -0
  79. package/dist/web/__tests__/live-collection.svelte.test.js +184 -0
  80. package/dist/web/index.d.ts +13 -0
  81. package/dist/web/index.d.ts.map +1 -0
  82. package/dist/web/index.js +12 -0
  83. package/dist/web/live-collection.svelte.d.ts +132 -0
  84. package/dist/web/live-collection.svelte.d.ts.map +1 -0
  85. package/dist/web/live-collection.svelte.js +173 -0
  86. package/package.json +13 -4
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Behavior tests for the Svelte 5 live-query binding over `@happyvertical/smrt-web`
3
+ * (#1761, slice A).
4
+ *
5
+ * Per repo policy only the network boundary is mocked: the generated REST
6
+ * client fetchers passed to `createSmrtCollection`. Everything else — the
7
+ * client-data engine collection, the real `useLiveQuery` live view, its
8
+ * transactions, and the runes reactivity — is real. The binding is exercised
9
+ * through a thin host component (`harness.svelte`) so `useLiveQuery`'s internal
10
+ * `$effect` has a genuine component-init scope, matching this package's other
11
+ * runes-composable tests.
12
+ */
13
+ import { createSmrtCollection, } from '@happyvertical/smrt-web';
14
+ import { flushSync, mount, unmount } from 'svelte';
15
+ import { afterEach, describe, expect, it } from 'vitest';
16
+ import Harness from './harness.svelte';
17
+ function productDefinition(name) {
18
+ return {
19
+ name,
20
+ className: 'Product',
21
+ endpoint: `/${name}`,
22
+ idField: 'id',
23
+ actions: ['create', 'get', 'list', 'update'],
24
+ fields: {
25
+ name: { type: 'text', required: true },
26
+ price: { type: 'decimal' },
27
+ },
28
+ };
29
+ }
30
+ /**
31
+ * Scripted stand-in for `createClient('/api/v1').products`: resolves like the
32
+ * generated fetchers (JSON payloads, `{ error }` bodies on failure, never
33
+ * rejects on HTTP status) so the engine's optimistic/rollback path is real.
34
+ */
35
+ function makeScriptedFetchers(initialRows) {
36
+ const serverRows = [...initialRows];
37
+ let failNextCreate = null;
38
+ const fetchers = {
39
+ list: async () => serverRows.map((row) => ({ ...row })),
40
+ create: async (data) => {
41
+ if (failNextCreate) {
42
+ const error = failNextCreate;
43
+ failNextCreate = null;
44
+ return { error };
45
+ }
46
+ const created = { ...data, id: `server-${serverRows.length + 1}` };
47
+ serverRows.push(created);
48
+ return { ...created };
49
+ },
50
+ };
51
+ return {
52
+ fetchers,
53
+ failCreateWith(message) {
54
+ failNextCreate = message;
55
+ },
56
+ };
57
+ }
58
+ function mountView(handle) {
59
+ const target = document.createElement('div');
60
+ document.body.appendChild(target);
61
+ let exposed = undefined;
62
+ const component = mount(Harness, {
63
+ target,
64
+ props: {
65
+ // The harness is typed against the base object shape; ProductData is a
66
+ // structural subtype, so this cross-cast is purely nominal.
67
+ handle: handle,
68
+ onReady: (view) => {
69
+ exposed = view;
70
+ },
71
+ },
72
+ });
73
+ return {
74
+ view: exposed,
75
+ teardown: () => {
76
+ unmount(component);
77
+ target.remove();
78
+ },
79
+ };
80
+ }
81
+ describe('liveCollection', () => {
82
+ const cleanup = [];
83
+ afterEach(async () => {
84
+ for (const fn of cleanup.splice(0))
85
+ fn();
86
+ });
87
+ function track(mounted) {
88
+ cleanup.push(mounted.teardown);
89
+ return mounted.view;
90
+ }
91
+ it('exposes live rows that reflect the collection as plain DTOs (no $-prefixed keys)', async () => {
92
+ const scripted = makeScriptedFetchers([
93
+ { id: 'p1', name: 'Widget', price: 9.99 },
94
+ ]);
95
+ const collection = createSmrtCollection(productDefinition('live-dto'), {
96
+ fetchers: scripted.fetchers,
97
+ staleTimeMs: 60_000,
98
+ });
99
+ const view = track(mountView(collection));
100
+ await collection.preload();
101
+ flushSync();
102
+ expect(view.isReady).toBe(true);
103
+ expect(view.status).toBe('ready');
104
+ expect(view.rows).toHaveLength(1);
105
+ const [row] = view.rows;
106
+ // Exact equality (not toMatchObject): must be a plain DTO — no
107
+ // $synced/$origin/$key/$collectionId leaking through the engine live view.
108
+ expect(row).toEqual({ id: 'p1', name: 'Widget', price: 9.99 });
109
+ expect(Object.keys(row).some((key) => key.startsWith('$'))).toBe(false);
110
+ expect(JSON.stringify(row)).not.toContain('$');
111
+ });
112
+ it('reports loading before the first read resolves', () => {
113
+ const scripted = makeScriptedFetchers([{ id: 'p1', name: 'Widget' }]);
114
+ const collection = createSmrtCollection(productDefinition('live-loading'), {
115
+ fetchers: scripted.fetchers,
116
+ staleTimeMs: 60_000,
117
+ // Do not eagerly preload — observe the initial loading status.
118
+ });
119
+ const view = track(mountView(collection));
120
+ flushSync();
121
+ expect(view.isLoading).toBe(true);
122
+ expect(view.status).toBe('loading');
123
+ expect(view.error).toBeNull();
124
+ });
125
+ it('reflects an optimistic insert reactively and settles its pending state', async () => {
126
+ const scripted = makeScriptedFetchers([{ id: 'p1', name: 'Widget' }]);
127
+ const collection = createSmrtCollection(productDefinition('live-insert'), {
128
+ fetchers: scripted.fetchers,
129
+ staleTimeMs: 60_000,
130
+ });
131
+ const view = track(mountView(collection));
132
+ await collection.preload();
133
+ flushSync();
134
+ expect(view.rows).toHaveLength(1);
135
+ const mutation = view.insert({
136
+ id: 'local-1',
137
+ name: 'Gadget',
138
+ price: 19.99,
139
+ });
140
+ flushSync();
141
+ // Optimistic row is visible in the live rows synchronously, and pending.
142
+ expect(view.rows.map((row) => row.name).sort()).toEqual([
143
+ 'Gadget',
144
+ 'Widget',
145
+ ]);
146
+ expect(mutation.pending).toBe(true);
147
+ expect(mutation.settled).toBe(false);
148
+ await mutation.done;
149
+ flushSync();
150
+ // Persisted: pending cleared, no error, and the server-assigned row is in.
151
+ expect(mutation.pending).toBe(false);
152
+ expect(mutation.settled).toBe(true);
153
+ expect(mutation.error).toBeNull();
154
+ const gadget = view.rows.find((row) => row.name === 'Gadget');
155
+ expect(gadget?.id).toBe('server-2');
156
+ // Still plain DTOs after the mutation reconciles.
157
+ expect(Object.keys(gadget ?? {}).some((key) => key.startsWith('$'))).toBe(false);
158
+ });
159
+ it('rolls the optimistic row back and surfaces the error when the create fails', async () => {
160
+ const scripted = makeScriptedFetchers([{ id: 'p1', name: 'Widget' }]);
161
+ const collection = createSmrtCollection(productDefinition('live-rollback'), {
162
+ fetchers: scripted.fetchers,
163
+ staleTimeMs: 60_000,
164
+ });
165
+ const view = track(mountView(collection));
166
+ await collection.preload();
167
+ flushSync();
168
+ scripted.failCreateWith('Validation failed: name is reserved');
169
+ const mutation = view.insert({ id: 'local-2', name: 'FAIL Gadget' });
170
+ flushSync();
171
+ expect(view.rows.some((row) => row.name === 'FAIL Gadget')).toBe(true);
172
+ // `done` resolves (never rejects) on settle — the failure is surfaced only
173
+ // reactively via `error`, so a reactive-only consumer risks no unhandled
174
+ // rejection.
175
+ await expect(mutation.done).resolves.toBeUndefined();
176
+ flushSync();
177
+ // Optimistic state rolled back reactively; the mutation carries the error.
178
+ expect(view.rows.some((row) => row.name === 'FAIL Gadget')).toBe(false);
179
+ expect(view.rows).toHaveLength(1);
180
+ expect(mutation.pending).toBe(false);
181
+ expect(mutation.settled).toBe(true);
182
+ expect(String(mutation.error?.message)).toContain('Validation failed: name is reserved');
183
+ });
184
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@happyvertical/smrt-svelte/web` — Svelte 5 live-query bindings for the
3
+ * `@happyvertical/smrt-web` browser client data runtime (#1761, slice A).
4
+ *
5
+ * Consumers turn a `SmrtWebCollection<T>` into runes-reactive live-query state
6
+ * plus mutation helpers WITHOUT importing TanStack. The client-data engine
7
+ * stays an implementation detail of the runtime + this binding; nothing here
8
+ * re-exports an `@tanstack/*` type.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ export { type LiveCollection, type LiveCollectionMutation, type LiveCollectionOptions, type LiveCollectionStatus, liveCollection, } from './live-collection.svelte.js';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,cAAc,GACf,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@happyvertical/smrt-svelte/web` — Svelte 5 live-query bindings for the
3
+ * `@happyvertical/smrt-web` browser client data runtime (#1761, slice A).
4
+ *
5
+ * Consumers turn a `SmrtWebCollection<T>` into runes-reactive live-query state
6
+ * plus mutation helpers WITHOUT importing TanStack. The client-data engine
7
+ * stays an implementation detail of the runtime + this binding; nothing here
8
+ * re-exports an `@tanstack/*` type.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ export { liveCollection, } from './live-collection.svelte.js';
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Svelte 5 live-query bindings for `@happyvertical/smrt-web` (#1761, slice A).
3
+ *
4
+ * Turns a SMRT-owned {@link SmrtWebCollection} into runes-reactive live-query
5
+ * state plus mutation helpers, so a consumer writes reactive components WITHOUT
6
+ * importing TanStack. The client-data engine (`@tanstack/db` +
7
+ * `@tanstack/svelte-db`) is used only inside this module — its types never
8
+ * appear on the public surface, mirroring smrt-web's own engine-absorption
9
+ * boundary (ratified condition for #1761).
10
+ *
11
+ * Two engine facts drive the shape of this file:
12
+ *
13
+ * 1. `useLiveQuery` internally uses `$state`/`$derived`/`$effect`, so it (and
14
+ * therefore {@link liveCollection}) MUST be called during Svelte component
15
+ * initialization — exactly like this package's other `use*` composables.
16
+ * Its internal `$effect` binds to the calling component's lifecycle, so the
17
+ * live subscription is torn down automatically when the component unmounts.
18
+ *
19
+ * 2. `@tanstack/svelte-db` live-query rows carry ENUMERABLE engine virtual
20
+ * props (`$key`/`$origin`/`$synced`/`$collectionId`) that would otherwise
21
+ * escape through spread or JSON. Every exposed row is projected to a plain
22
+ * DTO by stripping `$`-prefixed keys (the `$` prefix is reserved for the
23
+ * engine; SMRT columns never begin with it), mirroring smrt-web's internal
24
+ * `toPlainRow`.
25
+ */
26
+ import { type SmrtWebCollection, type SmrtWebRow } from '@happyvertical/smrt-web';
27
+ /**
28
+ * Lifecycle status of a {@link LiveCollection}'s underlying read.
29
+ *
30
+ * - `loading` — the first load has not completed yet (also covers the engine's
31
+ * pre-start `idle` and post-teardown `cleaned-up` phases).
32
+ * - `ready` — data has arrived and the collection is live.
33
+ * - `error` — the read failed during sync initialization.
34
+ */
35
+ export type LiveCollectionStatus = 'loading' | 'ready' | 'error';
36
+ /**
37
+ * A live, runes-reactive view over a {@link SmrtWebCollection}. Read `rows`,
38
+ * `status`, and `error` directly in markup — they update as the collection
39
+ * changes. Do NOT destructure this object (Svelte 5 reactivity is lost on
40
+ * destructure); read through the object, e.g. `view.rows`, or wrap in
41
+ * `$derived`.
42
+ */
43
+ export interface LiveCollection<TData extends object> {
44
+ /** Live rows as plain DTOs (no `$`-prefixed engine props), insertion order. */
45
+ readonly rows: ReadonlyArray<SmrtWebRow<TData>>;
46
+ /** Coarse lifecycle status of the underlying read. */
47
+ readonly status: LiveCollectionStatus;
48
+ /**
49
+ * The most recent error surfaced by the underlying read, or `null`. Mutation
50
+ * errors are surfaced on the {@link LiveCollectionMutation} returned by the
51
+ * mutation helpers, not here.
52
+ */
53
+ readonly error: unknown;
54
+ /** True while the first load has not completed (`status === 'loading'`). */
55
+ readonly isLoading: boolean;
56
+ /** True once data has arrived (`status === 'ready'`). */
57
+ readonly isReady: boolean;
58
+ /** True when the underlying read failed (`status === 'error'`). */
59
+ readonly isError: boolean;
60
+ /**
61
+ * Optimistically insert a row and persist it through the collection's create
62
+ * surface. The row is visible in `rows` synchronously; the returned handle's
63
+ * reactive `pending`/`error` track the server outcome and a failed create
64
+ * rolls the optimistic row back automatically.
65
+ */
66
+ insert(row: SmrtWebRow<TData>): LiveCollectionMutation;
67
+ }
68
+ /**
69
+ * Reactive handle to one in-flight mutation. Read `pending`/`error`/`settled`
70
+ * in markup to drive optimistic UI; await {@link done} to observe completion
71
+ * imperatively. All fields are SMRT-owned — no engine transaction leaks.
72
+ */
73
+ export interface LiveCollectionMutation {
74
+ /** True while the write is persisting; flips to false on success or failure. */
75
+ readonly pending: boolean;
76
+ /** True once the write has settled (persisted or rolled back). */
77
+ readonly settled: boolean;
78
+ /** The rollback error if the write failed, else `null`. */
79
+ readonly error: unknown;
80
+ /**
81
+ * Resolves when the write settles (persisted or rolled back); read
82
+ * {@link error} (or {@link settled}) for the outcome. Never rejects, so
83
+ * reactive-only consumers never risk an unhandled rejection.
84
+ */
85
+ readonly done: Promise<void>;
86
+ }
87
+ /** Options for {@link liveCollection}. Reserved for forward-compatible growth. */
88
+ export interface LiveCollectionOptions {
89
+ /**
90
+ * Trigger the collection's first load eagerly when the binding is created,
91
+ * rather than waiting for the engine's first read. Defaults to `true`.
92
+ */
93
+ preload?: boolean;
94
+ }
95
+ /**
96
+ * Create a runes-reactive live view over a {@link SmrtWebCollection}.
97
+ *
98
+ * MUST be called during Svelte component initialization (it delegates to
99
+ * `@tanstack/svelte-db`'s `useLiveQuery`, which sets up a `$effect`). The live
100
+ * subscription is torn down automatically when the calling component unmounts.
101
+ *
102
+ * @example
103
+ * ```svelte
104
+ * <script lang="ts">
105
+ * import { createSmrtCollection } from '@happyvertical/smrt-web';
106
+ * import { liveCollection } from '@happyvertical/smrt-svelte/web';
107
+ * import { productsDefinition } from '@happyvertical/smrt-virt-web';
108
+ *
109
+ * const products = createSmrtCollection(productsDefinition, {});
110
+ * const view = liveCollection(products);
111
+ *
112
+ * async function add() {
113
+ * const m = view.insert({ id: crypto.randomUUID(), name: 'New' });
114
+ * await m.done.catch(() => {}); // error also reflected on m.error
115
+ * }
116
+ * </script>
117
+ *
118
+ * {#if view.isLoading}
119
+ * <p>Loading…</p>
120
+ * {:else if view.isError}
121
+ * <p>Failed: {String(view.error)}</p>
122
+ * {:else}
123
+ * <ul>
124
+ * {#each view.rows as row (row.id)}
125
+ * <li>{row.name}</li>
126
+ * {/each}
127
+ * </ul>
128
+ * {/if}
129
+ * ```
130
+ */
131
+ export declare function liveCollection<TData extends object>(handle: SmrtWebCollection<TData>, options?: LiveCollectionOptions): LiveCollection<TData>;
132
+ //# sourceMappingURL=live-collection.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-collection.svelte.d.ts","sourceRoot":"","sources":["../../src/web/live-collection.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,UAAU,EAChB,MAAM,yBAAyB,CAAC;AASjC;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK,SAAS,MAAM;IAClD,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;IAChD,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,GAAG,sBAAsB,CAAC;CACxD;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,kFAAkF;AAClF,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAkCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,cAAc,CAAC,KAAK,SAAS,MAAM,EACjD,MAAM,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAChC,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAAC,KAAK,CAAC,CA6DvB"}
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Svelte 5 live-query bindings for `@happyvertical/smrt-web` (#1761, slice A).
3
+ *
4
+ * Turns a SMRT-owned {@link SmrtWebCollection} into runes-reactive live-query
5
+ * state plus mutation helpers, so a consumer writes reactive components WITHOUT
6
+ * importing TanStack. The client-data engine (`@tanstack/db` +
7
+ * `@tanstack/svelte-db`) is used only inside this module — its types never
8
+ * appear on the public surface, mirroring smrt-web's own engine-absorption
9
+ * boundary (ratified condition for #1761).
10
+ *
11
+ * Two engine facts drive the shape of this file:
12
+ *
13
+ * 1. `useLiveQuery` internally uses `$state`/`$derived`/`$effect`, so it (and
14
+ * therefore {@link liveCollection}) MUST be called during Svelte component
15
+ * initialization — exactly like this package's other `use*` composables.
16
+ * Its internal `$effect` binds to the calling component's lifecycle, so the
17
+ * live subscription is torn down automatically when the component unmounts.
18
+ *
19
+ * 2. `@tanstack/svelte-db` live-query rows carry ENUMERABLE engine virtual
20
+ * props (`$key`/`$origin`/`$synced`/`$collectionId`) that would otherwise
21
+ * escape through spread or JSON. Every exposed row is projected to a plain
22
+ * DTO by stripping `$`-prefixed keys (the `$` prefix is reserved for the
23
+ * engine; SMRT columns never begin with it), mirroring smrt-web's internal
24
+ * `toPlainRow`.
25
+ */
26
+ import { getEngineCollection, } from '@happyvertical/smrt-web';
27
+ import { useLiveQuery } from '@tanstack/svelte-db';
28
+ /**
29
+ * Project an engine live-query row to a plain public DTO by dropping every
30
+ * `$`-prefixed key. Mirrors smrt-web's internal `toPlainRow`: the `$` prefix is
31
+ * reserved for the engine's enumerable virtual props (`$key`/`$origin`/
32
+ * `$synced`/`$collectionId`); SMRT columns never begin with it. Char code 36
33
+ * is `'$'`.
34
+ */
35
+ function toPlainRow(row) {
36
+ const plain = {};
37
+ for (const [key, value] of Object.entries(row)) {
38
+ if (key.charCodeAt(0) !== 36)
39
+ plain[key] = value;
40
+ }
41
+ return plain;
42
+ }
43
+ /** Map the engine's fine-grained status to the coarse SMRT-owned status. */
44
+ function toLiveStatus(isReady, isError) {
45
+ if (isError)
46
+ return 'error';
47
+ if (isReady)
48
+ return 'ready';
49
+ return 'loading';
50
+ }
51
+ /**
52
+ * Create a runes-reactive live view over a {@link SmrtWebCollection}.
53
+ *
54
+ * MUST be called during Svelte component initialization (it delegates to
55
+ * `@tanstack/svelte-db`'s `useLiveQuery`, which sets up a `$effect`). The live
56
+ * subscription is torn down automatically when the calling component unmounts.
57
+ *
58
+ * @example
59
+ * ```svelte
60
+ * <script lang="ts">
61
+ * import { createSmrtCollection } from '@happyvertical/smrt-web';
62
+ * import { liveCollection } from '@happyvertical/smrt-svelte/web';
63
+ * import { productsDefinition } from '@happyvertical/smrt-virt-web';
64
+ *
65
+ * const products = createSmrtCollection(productsDefinition, {});
66
+ * const view = liveCollection(products);
67
+ *
68
+ * async function add() {
69
+ * const m = view.insert({ id: crypto.randomUUID(), name: 'New' });
70
+ * await m.done.catch(() => {}); // error also reflected on m.error
71
+ * }
72
+ * </script>
73
+ *
74
+ * {#if view.isLoading}
75
+ * <p>Loading…</p>
76
+ * {:else if view.isError}
77
+ * <p>Failed: {String(view.error)}</p>
78
+ * {:else}
79
+ * <ul>
80
+ * {#each view.rows as row (row.id)}
81
+ * <li>{row.name}</li>
82
+ * {/each}
83
+ * </ul>
84
+ * {/if}
85
+ * ```
86
+ */
87
+ export function liveCollection(handle, options = {}) {
88
+ const { preload = true } = options;
89
+ // Bridge to the engine collection (typed `unknown` at the boundary — cast to
90
+ // the engine `Collection` here, and confine that type to this module).
91
+ const engine = getEngineCollection(handle);
92
+ // Kick the first load without forcing consumers to await preload() — the
93
+ // live query subscribes lazily, so nudge it when requested. Swallow the
94
+ // rejection: a load failure (network error, or an SSR relative-URL
95
+ // `Failed to parse URL` TypeError) already surfaces reactively via the
96
+ // live-query status; this fire-and-forget must not become an unhandled
97
+ // rejection.
98
+ if (preload) {
99
+ void handle.preload().catch(() => { });
100
+ }
101
+ // The live query over the whole base collection. No `.select()` → `data`
102
+ // carries the full row objects (with engine virtual props, stripped below).
103
+ // `useLiveQuery` installs a `$effect`; calling `liveCollection` outside a
104
+ // component surfaces Svelte's own "effect outside component" error, which is
105
+ // the correct signal.
106
+ const query = useLiveQuery((q) => q.from({ row: engine }));
107
+ // Project rows to plain DTOs reactively. `query.data` is a $state-backed
108
+ // reactive array; `$derived.by` recomputes the projection when it changes.
109
+ const rows = $derived.by(() => query.data.map((row) => toPlainRow(row)));
110
+ const status = $derived(toLiveStatus(query.isReady, query.isError));
111
+ // useLiveQuery does not surface an error object, only an error status; expose
112
+ // the status string as the error payload when the read has failed.
113
+ const error = $derived(query.isError ? query.status : null);
114
+ return {
115
+ get rows() {
116
+ return rows;
117
+ },
118
+ get status() {
119
+ return status;
120
+ },
121
+ get error() {
122
+ return error;
123
+ },
124
+ get isLoading() {
125
+ return status === 'loading';
126
+ },
127
+ get isReady() {
128
+ return status === 'ready';
129
+ },
130
+ get isError() {
131
+ return status === 'error';
132
+ },
133
+ insert(row) {
134
+ return createMutation(() => handle.insert(row).isPersisted.promise);
135
+ },
136
+ };
137
+ }
138
+ /**
139
+ * Wrap a transaction's settle promise in a reactive {@link LiveCollectionMutation}.
140
+ * Runs the mutation immediately; tracks pending/error via runes. Kept separate
141
+ * so `insert` (and future `update`/`delete` helpers) share one reactive shape.
142
+ */
143
+ function createMutation(run) {
144
+ let pending = $state(true);
145
+ let settled = $state(false);
146
+ let error = $state(null);
147
+ // `done` RESOLVES on settle whether the write persisted or rolled back — the
148
+ // failure is surfaced only reactively via `error`. Re-throwing here would
149
+ // reject `done`, which a reactive-only consumer (reads `error`/`pending` in
150
+ // markup, never awaits `done`) would leave as an unhandled rejection.
151
+ const done = run().then(() => {
152
+ pending = false;
153
+ settled = true;
154
+ }, (cause) => {
155
+ pending = false;
156
+ settled = true;
157
+ error = cause;
158
+ });
159
+ return {
160
+ get pending() {
161
+ return pending;
162
+ },
163
+ get settled() {
164
+ return settled;
165
+ },
166
+ get error() {
167
+ return error;
168
+ },
169
+ get done() {
170
+ return done;
171
+ },
172
+ };
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-svelte",
3
- "version": "0.37.9",
3
+ "version": "0.37.10",
4
4
  "description": "Svelte 5 components for SMRT user management - auth, users, tenants, roles, permissions, groups",
5
5
  "type": "module",
6
6
  "smrtRawPrimitives": "strict",
@@ -46,6 +46,12 @@
46
46
  "svelte": "./dist/browser-ai/svelte/index.js",
47
47
  "import": "./dist/browser-ai/svelte/index.js",
48
48
  "default": "./dist/browser-ai/svelte/index.js"
49
+ },
50
+ "./web": {
51
+ "types": "./dist/web/index.d.ts",
52
+ "svelte": "./dist/web/index.js",
53
+ "import": "./dist/web/index.js",
54
+ "default": "./dist/web/index.js"
49
55
  }
50
56
  },
51
57
  "files": [
@@ -77,10 +83,13 @@
77
83
  },
78
84
  "dependencies": {
79
85
  "@happyvertical/logger": "^0.74.11",
86
+ "@tanstack/db": "^0.6.14",
87
+ "@tanstack/svelte-db": "^0.1.91",
80
88
  "esm-env": "^1.2.2",
81
- "@happyvertical/smrt-languages": "0.37.9",
82
- "@happyvertical/smrt-ui": "0.37.9",
83
- "@happyvertical/smrt-types": "0.37.9"
89
+ "@happyvertical/smrt-languages": "0.37.10",
90
+ "@happyvertical/smrt-types": "0.37.10",
91
+ "@happyvertical/smrt-ui": "0.37.10",
92
+ "@happyvertical/smrt-web": "0.37.10"
84
93
  },
85
94
  "peerDependencies": {
86
95
  "@huggingface/transformers": ">=3.8.1",