@happyvertical/smrt-svelte 0.38.6 → 0.38.7
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/AGENTS.md +23 -0
- package/dist/web/__tests__/update-available-harness.svelte +43 -0
- package/dist/web/__tests__/update-available-harness.svelte.d.ts +22 -0
- package/dist/web/__tests__/update-available-harness.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/update-available.svelte.test.js +131 -0
- package/dist/web/index.d.ts +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +1 -0
- package/dist/web/update-available.svelte.d.ts +92 -0
- package/dist/web/update-available.svelte.d.ts.map +1 -0
- package/dist/web/update-available.svelte.js +94 -0
- package/package.json +5 -5
package/AGENTS.md
CHANGED
|
@@ -280,6 +280,29 @@ AdminShell core (`./workspace`) stays transport-agnostic and TanStack-free (epic
|
|
|
280
280
|
#1766). The pure diff core is `ActivityFeedReconciler` (engine-free, unit-tested
|
|
281
281
|
against a real `ShellState`). Demo: `playground/.../admin-shell-activity-feed`.
|
|
282
282
|
|
|
283
|
+
### `updateAvailable` binding (`./web`, #1764)
|
|
284
|
+
|
|
285
|
+
`useUpdateAvailable({ state, updated? })` (from `@happyvertical/smrt-svelte/web`)
|
|
286
|
+
is the Svelte 5 reactive wrapper over smrt-web's framework-free `UpdateState`
|
|
287
|
+
(from `createUpdateState()`). It surfaces `updateAvailable` / `bundle` /
|
|
288
|
+
`contract` reactively (`$state`/`$derived`) for a toast or reload prompt, and
|
|
289
|
+
wires SvelteKit's native `updated` store as the **bundle** signal. The
|
|
290
|
+
**contract** signal (a manifest-hash change across loads, detected inside the
|
|
291
|
+
smrt-web primitive under the build-time-inject decision) is surfaced as-is.
|
|
292
|
+
|
|
293
|
+
`updated` is passed IN as a reactive accessor (`() => updated.current` on modern
|
|
294
|
+
`$app/state`, or a `$derived` over the legacy `$updated` store) rather than
|
|
295
|
+
imported here — `$app/*` only resolves inside a SvelteKit app, so a library that
|
|
296
|
+
imported it could not build/test standalone; the other `./web` adapters take
|
|
297
|
+
runtime input the same way. Must be called during component init (installs
|
|
298
|
+
`$effect`s that subscribe to the primitive and watch `updated`); both tear down
|
|
299
|
+
on unmount. Construct ONE `createUpdateState()` per app (it owns the durable
|
|
300
|
+
last-seen-hash bookkeeping) and pass its `manifestHash` from
|
|
301
|
+
`@happyvertical/smrt-virt-web`. Browser-safe, engine-free (no `@tanstack/*`
|
|
302
|
+
type). NOTE: `.svelte.ts` runes tests may not run under the local Darwin/vite8
|
|
303
|
+
toolchain (CI is the gate) — the binding is covered by typecheck + svelte-check +
|
|
304
|
+
a light unit test.
|
|
305
|
+
|
|
283
306
|
### Dock availability gates (server-side)
|
|
284
307
|
|
|
285
308
|
`ToolDef.gates?: string[]` declares the gates a tool must pass to be visible.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Test harness — invokes useUpdateAvailable() inside a component init scope so
|
|
4
|
+
* its `$effect`s (the state subscription + the `updated` watcher) wire up
|
|
5
|
+
* correctly, then hands the reactive view back to the test via `onReady`.
|
|
6
|
+
*
|
|
7
|
+
* The bundle-signal source (`updated`) is a genuinely REACTIVE `$state` owned by
|
|
8
|
+
* this harness, mutated by the test through the `setUpdated` control handed back
|
|
9
|
+
* via `onReady`. This mirrors production, where the caller passes
|
|
10
|
+
* `() => updated.current` over SvelteKit's reactive `updated` store — a plain
|
|
11
|
+
* closure variable would not be tracked by the rune's `$effect`.
|
|
12
|
+
*/
|
|
13
|
+
import type { UpdateState } from '@happyvertical/smrt-web';
|
|
14
|
+
import {
|
|
15
|
+
type UpdateAvailableView,
|
|
16
|
+
useUpdateAvailable,
|
|
17
|
+
} from '../update-available.svelte.js';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
state: UpdateState;
|
|
21
|
+
initialUpdated?: boolean;
|
|
22
|
+
onReady: (view: UpdateAvailableView, setUpdated: (v: boolean) => void) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props: Props = $props();
|
|
26
|
+
|
|
27
|
+
// A reactive bundle-updated source the test drives via setUpdated. Seeds from
|
|
28
|
+
// the prop once at init (intentional snapshot).
|
|
29
|
+
// svelte-ignore state_referenced_locally
|
|
30
|
+
let updated = $state(props.initialUpdated ?? false);
|
|
31
|
+
|
|
32
|
+
// The harness intentionally snapshots the props during setup.
|
|
33
|
+
// svelte-ignore state_referenced_locally
|
|
34
|
+
const view = useUpdateAvailable({
|
|
35
|
+
state: props.state,
|
|
36
|
+
updated: () => updated,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// svelte-ignore state_referenced_locally
|
|
40
|
+
props.onReady(view, (v: boolean) => {
|
|
41
|
+
updated = v;
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test harness — invokes useUpdateAvailable() inside a component init scope so
|
|
3
|
+
* its `$effect`s (the state subscription + the `updated` watcher) wire up
|
|
4
|
+
* correctly, then hands the reactive view back to the test via `onReady`.
|
|
5
|
+
*
|
|
6
|
+
* The bundle-signal source (`updated`) is a genuinely REACTIVE `$state` owned by
|
|
7
|
+
* this harness, mutated by the test through the `setUpdated` control handed back
|
|
8
|
+
* via `onReady`. This mirrors production, where the caller passes
|
|
9
|
+
* `() => updated.current` over SvelteKit's reactive `updated` store — a plain
|
|
10
|
+
* closure variable would not be tracked by the rune's `$effect`.
|
|
11
|
+
*/
|
|
12
|
+
import type { UpdateState } from '@happyvertical/smrt-web';
|
|
13
|
+
import { type UpdateAvailableView } from '../update-available.svelte.js';
|
|
14
|
+
interface Props {
|
|
15
|
+
state: UpdateState;
|
|
16
|
+
initialUpdated?: boolean;
|
|
17
|
+
onReady: (view: UpdateAvailableView, setUpdated: (v: boolean) => void) => void;
|
|
18
|
+
}
|
|
19
|
+
declare const UpdateAvailableHarness: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type UpdateAvailableHarness = ReturnType<typeof UpdateAvailableHarness>;
|
|
21
|
+
export default UpdateAvailableHarness;
|
|
22
|
+
//# sourceMappingURL=update-available-harness.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-available-harness.svelte.d.ts","sourceRoot":"","sources":["../../../src/web/__tests__/update-available-harness.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,+BAA+B,CAAC;AAGvC,UAAU,KAAK;IACb,KAAK,EAAE,WAAW,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE,CAAC,IAAI,EAAE,mBAAmB,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;CAChF;AA6BD,QAAA,MAAM,sBAAsB,2CAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior tests for the Svelte 5 `useUpdateAvailable` binding (#1764).
|
|
3
|
+
*
|
|
4
|
+
* The binding wires SvelteKit's `updated` boolean (the bundle signal) into
|
|
5
|
+
* smrt-web's framework-free `UpdateState`, and surfaces both signals reactively.
|
|
6
|
+
* Here the `UpdateState` is a small in-memory stub (a real one would open
|
|
7
|
+
* IndexedDB; the RUNE's reactivity — subscription sync + `updated` watcher — is
|
|
8
|
+
* what these tests pin), and the binding runs inside a host component so its
|
|
9
|
+
* `$effect`s have a genuine component-init scope, matching this package's other
|
|
10
|
+
* runes-composable tests.
|
|
11
|
+
*
|
|
12
|
+
* NOTE: Svelte-package tests may not run under the local Darwin/vite8 toolchain
|
|
13
|
+
* (CI is the gate). The binding is additionally covered by typecheck.
|
|
14
|
+
*/
|
|
15
|
+
import { flushSync, mount, unmount } from 'svelte';
|
|
16
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
17
|
+
import Harness from './update-available-harness.svelte';
|
|
18
|
+
/**
|
|
19
|
+
* A minimal in-memory UpdateState: drives `bundle`/`contract` and fans out to
|
|
20
|
+
* subscribers, exactly like the real primitive but with no IndexedDB. Lets a
|
|
21
|
+
* test flip the contract signal and push the bundle signal deterministically.
|
|
22
|
+
*/
|
|
23
|
+
function makeStubState() {
|
|
24
|
+
let bundle = false;
|
|
25
|
+
let contract = false;
|
|
26
|
+
const subs = new Set();
|
|
27
|
+
const snapshot = () => ({
|
|
28
|
+
bundle,
|
|
29
|
+
contract,
|
|
30
|
+
updateAvailable: bundle || contract,
|
|
31
|
+
});
|
|
32
|
+
const notify = () => {
|
|
33
|
+
for (const cb of [...subs])
|
|
34
|
+
cb(snapshot());
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
get: snapshot,
|
|
38
|
+
subscribe(cb) {
|
|
39
|
+
subs.add(cb);
|
|
40
|
+
cb(snapshot());
|
|
41
|
+
return () => {
|
|
42
|
+
subs.delete(cb);
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
notifyBundleUpdated() {
|
|
46
|
+
if (bundle)
|
|
47
|
+
return;
|
|
48
|
+
bundle = true;
|
|
49
|
+
notify();
|
|
50
|
+
},
|
|
51
|
+
ready: Promise.resolve(),
|
|
52
|
+
dispose() {
|
|
53
|
+
subs.clear();
|
|
54
|
+
},
|
|
55
|
+
fireContract() {
|
|
56
|
+
if (contract)
|
|
57
|
+
return;
|
|
58
|
+
contract = true;
|
|
59
|
+
notify();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const mounted = [];
|
|
64
|
+
function render(state, initialUpdated = false) {
|
|
65
|
+
let view;
|
|
66
|
+
let setUpdated;
|
|
67
|
+
const target = document.createElement('div');
|
|
68
|
+
const component = mount(Harness, {
|
|
69
|
+
target,
|
|
70
|
+
props: {
|
|
71
|
+
state,
|
|
72
|
+
initialUpdated,
|
|
73
|
+
onReady: (v, set) => {
|
|
74
|
+
view = v;
|
|
75
|
+
setUpdated = set;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
mounted.push(component);
|
|
80
|
+
flushSync();
|
|
81
|
+
return { view, setUpdated };
|
|
82
|
+
}
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
for (const c of mounted) {
|
|
85
|
+
void unmount(c);
|
|
86
|
+
}
|
|
87
|
+
mounted.length = 0;
|
|
88
|
+
});
|
|
89
|
+
describe('useUpdateAvailable (#1764)', () => {
|
|
90
|
+
it('starts with no update available and reflects the initial state', () => {
|
|
91
|
+
const state = makeStubState();
|
|
92
|
+
const { view } = render(state);
|
|
93
|
+
expect(view.bundle).toBe(false);
|
|
94
|
+
expect(view.contract).toBe(false);
|
|
95
|
+
expect(view.updateAvailable).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
it('reacts to the CONTRACT signal fired by the primitive', () => {
|
|
98
|
+
const state = makeStubState();
|
|
99
|
+
const { view } = render(state);
|
|
100
|
+
state.fireContract();
|
|
101
|
+
flushSync();
|
|
102
|
+
expect(view.contract).toBe(true);
|
|
103
|
+
expect(view.updateAvailable).toBe(true);
|
|
104
|
+
expect(view.bundle).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
it('pushes the BUNDLE signal when SvelteKit `updated` flips true', () => {
|
|
107
|
+
const state = makeStubState();
|
|
108
|
+
// `setUpdated` mutates a REACTIVE $state in the harness (mirrors SvelteKit's
|
|
109
|
+
// reactive `updated` store) — a plain closure variable would not be tracked
|
|
110
|
+
// by the rune's $effect, so the bundle signal would never fire.
|
|
111
|
+
const { view, setUpdated } = render(state);
|
|
112
|
+
expect(view.updateAvailable).toBe(false);
|
|
113
|
+
// Simulate SvelteKit detecting a new deployment.
|
|
114
|
+
setUpdated(true);
|
|
115
|
+
flushSync();
|
|
116
|
+
expect(view.bundle).toBe(true);
|
|
117
|
+
expect(view.updateAvailable).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it('updateAvailable is the union: either signal alone makes it true', () => {
|
|
120
|
+
const stateA = makeStubState();
|
|
121
|
+
const a = render(stateA);
|
|
122
|
+
stateA.fireContract();
|
|
123
|
+
flushSync();
|
|
124
|
+
expect(a.view.updateAvailable).toBe(true);
|
|
125
|
+
const stateB = makeStubState();
|
|
126
|
+
const b = render(stateB);
|
|
127
|
+
b.setUpdated(true);
|
|
128
|
+
flushSync();
|
|
129
|
+
expect(b.view.updateAvailable).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/dist/web/index.d.ts
CHANGED
|
@@ -16,4 +16,5 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { type ActivityFeedHandle, type ActivityFeedMap, type ActivityFeedOptions, activityFeed, type ShellActivityInput, } from './activity-feed.svelte.js';
|
|
18
18
|
export { type LiveCollection, type LiveCollectionMutation, type LiveCollectionOptions, type LiveCollectionStatus, liveCollection, } from './live-collection.svelte.js';
|
|
19
|
+
export { type UpdateAvailableView, type UseUpdateAvailableOptions, useUpdateAvailable, } from './update-available.svelte.js';
|
|
19
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/web/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,YAAY,EACZ,KAAK,kBAAkB,GACxB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,cAAc,GACf,MAAM,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,YAAY,EACZ,KAAK,kBAAkB,GACxB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,cAAc,GACf,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,kBAAkB,GACnB,MAAM,8BAA8B,CAAC"}
|
package/dist/web/index.js
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte 5 reactive binding for smrt-web's `updateAvailable` primitive (#1764).
|
|
3
|
+
*
|
|
4
|
+
* Turns the framework-free {@link UpdateState} from `@happyvertical/smrt-web`
|
|
5
|
+
* into runes-reactive state a component can read directly to drive a toast /
|
|
6
|
+
* reload prompt, and wires SvelteKit's native `updated` store (the BUNDLE
|
|
7
|
+
* signal) into it. The contract signal (a manifest-hash change across loads) is
|
|
8
|
+
* detected inside the smrt-web primitive under the build-time-inject decision;
|
|
9
|
+
* this binding just surfaces both, plus the derived `updateAvailable = bundle ||
|
|
10
|
+
* contract`.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the caller passes `updated` in
|
|
13
|
+
*
|
|
14
|
+
* SvelteKit's `updated` store lives in `$app/state` / `$app/stores`, which only
|
|
15
|
+
* resolve inside a SvelteKit app — a library that imported it could not build or
|
|
16
|
+
* test standalone. So the consumer passes the CURRENT bundle-updated boolean via
|
|
17
|
+
* {@link UseUpdateAvailableOptions.updated} (e.g. `() => updated.current` on
|
|
18
|
+
* modern SvelteKit, or a `$derived` over the legacy `$updated` store). This
|
|
19
|
+
* keeps the binding browser-safe and SvelteKit-version-agnostic, matching how
|
|
20
|
+
* this package's other `/web` adapters take runtime input as parameters rather
|
|
21
|
+
* than importing app-only modules.
|
|
22
|
+
*
|
|
23
|
+
* MUST be called during Svelte component initialization (it installs a
|
|
24
|
+
* `$effect`), like this package's other `use*` composables. The subscription to
|
|
25
|
+
* the primitive and the `updated` watcher are torn down automatically when the
|
|
26
|
+
* component unmounts.
|
|
27
|
+
*
|
|
28
|
+
* Engine-free / TanStack-free: this binding names no `@tanstack/*` type — it
|
|
29
|
+
* only touches the SMRT-owned `UpdateState` surface.
|
|
30
|
+
*/
|
|
31
|
+
import type { UpdateState } from '@happyvertical/smrt-web';
|
|
32
|
+
/**
|
|
33
|
+
* A runes-reactive view of the two update signals. Read the fields directly in
|
|
34
|
+
* markup — they update as either signal fires. Do NOT destructure (Svelte 5
|
|
35
|
+
* reactivity is lost on destructure); read through the object, e.g.
|
|
36
|
+
* `update.updateAvailable`.
|
|
37
|
+
*/
|
|
38
|
+
export interface UpdateAvailableView {
|
|
39
|
+
/** True once the client bundle changed on the server (SvelteKit `updated`). */
|
|
40
|
+
readonly bundle: boolean;
|
|
41
|
+
/** True once the API contract (manifest hash) changed across this load. */
|
|
42
|
+
readonly contract: boolean;
|
|
43
|
+
/** `bundle || contract` — either means an update is available. */
|
|
44
|
+
readonly updateAvailable: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Options for {@link useUpdateAvailable}. */
|
|
47
|
+
export interface UseUpdateAvailableOptions {
|
|
48
|
+
/**
|
|
49
|
+
* The smrt-web primitive holding the two signals, from `createUpdateState()`.
|
|
50
|
+
* Construct ONE per app (it owns the durable last-seen-hash bookkeeping) and
|
|
51
|
+
* pass it here.
|
|
52
|
+
*/
|
|
53
|
+
state: UpdateState;
|
|
54
|
+
/**
|
|
55
|
+
* A reactive accessor for SvelteKit's bundle-updated boolean — call it inside
|
|
56
|
+
* this function so the returned `$effect` tracks it. Typically `() =>
|
|
57
|
+
* updated.current` (modern `$app/state`) or a `$derived` over the legacy
|
|
58
|
+
* `$updated` store. When it flips `true`, the BUNDLE signal is pushed into the
|
|
59
|
+
* primitive. Omit to wire no bundle source (contract-only).
|
|
60
|
+
*/
|
|
61
|
+
updated?: () => boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a runes-reactive {@link UpdateAvailableView} over a smrt-web
|
|
65
|
+
* {@link UpdateState}, wiring SvelteKit's `updated` store as the bundle signal.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```svelte
|
|
69
|
+
* <script lang="ts">
|
|
70
|
+
* import { updated } from '$app/state';
|
|
71
|
+
* import { createUpdateState } from '@happyvertical/smrt-web';
|
|
72
|
+
* import { useUpdateAvailable } from '@happyvertical/smrt-svelte/web';
|
|
73
|
+
* import { manifestHash } from '@happyvertical/smrt-virt-web';
|
|
74
|
+
*
|
|
75
|
+
* // One primitive per app — owns the durable last-seen-hash compare.
|
|
76
|
+
* const state = createUpdateState({
|
|
77
|
+
* manifestHash,
|
|
78
|
+
* namespace: { apiBase: '/api/v1', manifestHash: 'apiv1' },
|
|
79
|
+
* });
|
|
80
|
+
* const update = useUpdateAvailable({ state, updated: () => updated.current });
|
|
81
|
+
* </script>
|
|
82
|
+
*
|
|
83
|
+
* {#if update.updateAvailable}
|
|
84
|
+
* <div role="alert">
|
|
85
|
+
* A new version is available.
|
|
86
|
+
* <button onclick={() => location.reload()}>Reload</button>
|
|
87
|
+
* </div>
|
|
88
|
+
* {/if}
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare function useUpdateAvailable(options: UseUpdateAvailableOptions): UpdateAvailableView;
|
|
92
|
+
//# sourceMappingURL=update-available.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-available.svelte.d.ts","sourceRoot":"","sources":["../../src/web/update-available.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,8CAA8C;AAC9C,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,KAAK,EAAE,WAAW,CAAC;IACnB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,yBAAyB,GACjC,mBAAmB,CAuCrB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte 5 reactive binding for smrt-web's `updateAvailable` primitive (#1764).
|
|
3
|
+
*
|
|
4
|
+
* Turns the framework-free {@link UpdateState} from `@happyvertical/smrt-web`
|
|
5
|
+
* into runes-reactive state a component can read directly to drive a toast /
|
|
6
|
+
* reload prompt, and wires SvelteKit's native `updated` store (the BUNDLE
|
|
7
|
+
* signal) into it. The contract signal (a manifest-hash change across loads) is
|
|
8
|
+
* detected inside the smrt-web primitive under the build-time-inject decision;
|
|
9
|
+
* this binding just surfaces both, plus the derived `updateAvailable = bundle ||
|
|
10
|
+
* contract`.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the caller passes `updated` in
|
|
13
|
+
*
|
|
14
|
+
* SvelteKit's `updated` store lives in `$app/state` / `$app/stores`, which only
|
|
15
|
+
* resolve inside a SvelteKit app — a library that imported it could not build or
|
|
16
|
+
* test standalone. So the consumer passes the CURRENT bundle-updated boolean via
|
|
17
|
+
* {@link UseUpdateAvailableOptions.updated} (e.g. `() => updated.current` on
|
|
18
|
+
* modern SvelteKit, or a `$derived` over the legacy `$updated` store). This
|
|
19
|
+
* keeps the binding browser-safe and SvelteKit-version-agnostic, matching how
|
|
20
|
+
* this package's other `/web` adapters take runtime input as parameters rather
|
|
21
|
+
* than importing app-only modules.
|
|
22
|
+
*
|
|
23
|
+
* MUST be called during Svelte component initialization (it installs a
|
|
24
|
+
* `$effect`), like this package's other `use*` composables. The subscription to
|
|
25
|
+
* the primitive and the `updated` watcher are torn down automatically when the
|
|
26
|
+
* component unmounts.
|
|
27
|
+
*
|
|
28
|
+
* Engine-free / TanStack-free: this binding names no `@tanstack/*` type — it
|
|
29
|
+
* only touches the SMRT-owned `UpdateState` surface.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Create a runes-reactive {@link UpdateAvailableView} over a smrt-web
|
|
33
|
+
* {@link UpdateState}, wiring SvelteKit's `updated` store as the bundle signal.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```svelte
|
|
37
|
+
* <script lang="ts">
|
|
38
|
+
* import { updated } from '$app/state';
|
|
39
|
+
* import { createUpdateState } from '@happyvertical/smrt-web';
|
|
40
|
+
* import { useUpdateAvailable } from '@happyvertical/smrt-svelte/web';
|
|
41
|
+
* import { manifestHash } from '@happyvertical/smrt-virt-web';
|
|
42
|
+
*
|
|
43
|
+
* // One primitive per app — owns the durable last-seen-hash compare.
|
|
44
|
+
* const state = createUpdateState({
|
|
45
|
+
* manifestHash,
|
|
46
|
+
* namespace: { apiBase: '/api/v1', manifestHash: 'apiv1' },
|
|
47
|
+
* });
|
|
48
|
+
* const update = useUpdateAvailable({ state, updated: () => updated.current });
|
|
49
|
+
* </script>
|
|
50
|
+
*
|
|
51
|
+
* {#if update.updateAvailable}
|
|
52
|
+
* <div role="alert">
|
|
53
|
+
* A new version is available.
|
|
54
|
+
* <button onclick={() => location.reload()}>Reload</button>
|
|
55
|
+
* </div>
|
|
56
|
+
* {/if}
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function useUpdateAvailable(options) {
|
|
60
|
+
const { state, updated } = options;
|
|
61
|
+
// Seed from the primitive's current state, then keep it in sync via its
|
|
62
|
+
// subscription (fires immediately + on every transition).
|
|
63
|
+
let bundle = $state(state.get().bundle);
|
|
64
|
+
let contract = $state(state.get().contract);
|
|
65
|
+
$effect(() => {
|
|
66
|
+
const unsubscribe = state.subscribe((s) => {
|
|
67
|
+
bundle = s.bundle;
|
|
68
|
+
contract = s.contract;
|
|
69
|
+
});
|
|
70
|
+
return unsubscribe;
|
|
71
|
+
});
|
|
72
|
+
// Push the bundle signal when SvelteKit reports a new deployment. Reading
|
|
73
|
+
// `updated()` inside the effect makes it a reactive dependency, so a flip to
|
|
74
|
+
// true re-runs and forwards it. The primitive's bundle signal is idempotent,
|
|
75
|
+
// so re-forwarding is harmless.
|
|
76
|
+
if (updated) {
|
|
77
|
+
$effect(() => {
|
|
78
|
+
if (updated())
|
|
79
|
+
state.notifyBundleUpdated();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const updateAvailable = $derived(bundle || contract);
|
|
83
|
+
return {
|
|
84
|
+
get bundle() {
|
|
85
|
+
return bundle;
|
|
86
|
+
},
|
|
87
|
+
get contract() {
|
|
88
|
+
return contract;
|
|
89
|
+
},
|
|
90
|
+
get updateAvailable() {
|
|
91
|
+
return updateAvailable;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-svelte",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.7",
|
|
4
4
|
"description": "Svelte 5 components for SMRT user management - auth, users, tenants, roles, permissions, groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"smrtRawPrimitives": "strict",
|
|
@@ -92,10 +92,10 @@
|
|
|
92
92
|
"@tanstack/db": "^0.6.14",
|
|
93
93
|
"@tanstack/svelte-db": "^0.1.91",
|
|
94
94
|
"esm-env": "^1.2.2",
|
|
95
|
-
"@happyvertical/smrt-
|
|
96
|
-
"@happyvertical/smrt-
|
|
97
|
-
"@happyvertical/smrt-ui": "0.38.
|
|
98
|
-
"@happyvertical/smrt-web": "0.38.
|
|
95
|
+
"@happyvertical/smrt-languages": "0.38.7",
|
|
96
|
+
"@happyvertical/smrt-types": "0.38.7",
|
|
97
|
+
"@happyvertical/smrt-ui": "0.38.7",
|
|
98
|
+
"@happyvertical/smrt-web": "0.38.7"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
101
|
"@huggingface/transformers": ">=3.8.1",
|