@wentools/simmer-svelte 0.1.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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/package.json +31 -0
- package/src/eventual_state/eventual_state.svelte.ts +108 -0
- package/src/eventual_state/mod.ts +2 -0
- package/src/mod.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wentools
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @wentools/simmer-svelte
|
|
2
|
+
|
|
3
|
+
Svelte 5 adapters for [@wentools/simmer](https://jsr.io/@wentools/simmer).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# JSR
|
|
9
|
+
deno add jsr:@wentools/simmer-svelte
|
|
10
|
+
|
|
11
|
+
# npm
|
|
12
|
+
npx jsr add @wentools/simmer-svelte
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### `createEventualState`
|
|
18
|
+
|
|
19
|
+
Bridges simmer's `EventualResult` to Svelte 5 reactive state. Returns the
|
|
20
|
+
current (cached) value immediately, then automatically updates when fresh data
|
|
21
|
+
arrives.
|
|
22
|
+
|
|
23
|
+
```svelte
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
import { createEventualState } from '@wentools/simmer-svelte'
|
|
26
|
+
|
|
27
|
+
let { data } = $props()
|
|
28
|
+
|
|
29
|
+
// data.patronShowcase is EventualResult<ShowcaseData>
|
|
30
|
+
const showcase = createEventualState(() => data.patronShowcase)
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<!-- Renders immediately with cached data -->
|
|
34
|
+
<PatronShowcase data={showcase.value} />
|
|
35
|
+
|
|
36
|
+
{#if showcase.isRefreshing}
|
|
37
|
+
<LoadingSpinner />
|
|
38
|
+
{/if}
|
|
39
|
+
|
|
40
|
+
{#if showcase.error}
|
|
41
|
+
<p>Failed to refresh: {showcase.error.message}</p>
|
|
42
|
+
{/if}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### How it works
|
|
46
|
+
|
|
47
|
+
`EventualResult<T>` contains `{ current: T, fresh: Promise<T> | null }`.
|
|
48
|
+
|
|
49
|
+
- `current` is served immediately (from cache or SSR)
|
|
50
|
+
- `fresh` resolves when updated data arrives from the API
|
|
51
|
+
|
|
52
|
+
`createEventualState` wraps this in a reactive rune that:
|
|
53
|
+
|
|
54
|
+
1. Returns `current` as `value` immediately
|
|
55
|
+
2. Sets `isRefreshing: true` while `fresh` is pending
|
|
56
|
+
3. Swaps `value` to the resolved fresh data automatically
|
|
57
|
+
4. Handles race conditions when navigation triggers new requests
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wentools/simmer-svelte",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Svelte 5 adapters for @wentools/simmer",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://gitlab.com/wentools/simmer-svelte"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"svelte",
|
|
12
|
+
"runes",
|
|
13
|
+
"eventual",
|
|
14
|
+
"simmer",
|
|
15
|
+
"reactive",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"svelte": "./src/mod.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"svelte": "./src/mod.ts",
|
|
23
|
+
"types": "./src/mod.ts",
|
|
24
|
+
"default": "./src/mod.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"svelte": ">=5.0.0",
|
|
29
|
+
"@wentools/simmer": ">=0.1.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte 5 rune for handling EventualResult on the client side.
|
|
3
|
+
*
|
|
4
|
+
* Automatically switches from current to fresh data when the promise resolves,
|
|
5
|
+
* with proper handling of race conditions during navigation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EventualResult } from '@wentools/simmer'
|
|
9
|
+
|
|
10
|
+
type EventualInput<TData> = EventualResult<TData>
|
|
11
|
+
|
|
12
|
+
type EventualState<TData> = {
|
|
13
|
+
/** The current value - starts with cached data, updates when fresh arrives */
|
|
14
|
+
readonly value: TData
|
|
15
|
+
/** True while waiting for fresh data */
|
|
16
|
+
readonly isRefreshing: boolean
|
|
17
|
+
/** Error if the fresh promise rejected */
|
|
18
|
+
readonly error: Error | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates reactive state from an EventualResult.
|
|
23
|
+
*
|
|
24
|
+
* Returns the current value immediately, then automatically updates
|
|
25
|
+
* when fresh data arrives. Handles race conditions from navigation
|
|
26
|
+
* and errors gracefully.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```svelte
|
|
30
|
+
* <script lang="ts">
|
|
31
|
+
* import { createEventualState } from '@wentools/simmer-svelte'
|
|
32
|
+
*
|
|
33
|
+
* let { data } = $props()
|
|
34
|
+
*
|
|
35
|
+
* const showcase = createEventualState(() => data.patronShowcase)
|
|
36
|
+
* </script>
|
|
37
|
+
*
|
|
38
|
+
* <Footer patronShowcase={showcase.value} />
|
|
39
|
+
*
|
|
40
|
+
* {#if showcase.isRefreshing}
|
|
41
|
+
* <span class="loading-indicator" />
|
|
42
|
+
* {/if}
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @param getEventual - Getter returning EventualResult (or compatible { current, fresh })
|
|
46
|
+
* @returns Reactive state with value, isRefreshing, and error
|
|
47
|
+
*/
|
|
48
|
+
const createEventualState = <TData>(
|
|
49
|
+
getEventual: () => EventualInput<TData>,
|
|
50
|
+
): EventualState<TData> => {
|
|
51
|
+
// Initialize with current value
|
|
52
|
+
const initial = getEventual()
|
|
53
|
+
let currentValue = $state<TData>(initial.current)
|
|
54
|
+
let freshValue = $state<TData | undefined>(undefined)
|
|
55
|
+
let hasFreshValue = $state(false)
|
|
56
|
+
let hasFreshPromise = $state(initial.fresh !== null)
|
|
57
|
+
let error = $state<Error | null>(null)
|
|
58
|
+
|
|
59
|
+
// Counter to handle race conditions - stale promises are ignored
|
|
60
|
+
let promiseCounter = 0
|
|
61
|
+
|
|
62
|
+
$effect(() => {
|
|
63
|
+
const { current, fresh } = getEventual()
|
|
64
|
+
|
|
65
|
+
// Update tracked state
|
|
66
|
+
currentValue = current
|
|
67
|
+
hasFreshPromise = fresh !== null
|
|
68
|
+
|
|
69
|
+
// Reset fresh state for new input
|
|
70
|
+
const myCounter = ++promiseCounter
|
|
71
|
+
freshValue = undefined
|
|
72
|
+
hasFreshValue = false
|
|
73
|
+
error = null
|
|
74
|
+
|
|
75
|
+
if (fresh) {
|
|
76
|
+
fresh
|
|
77
|
+
.then((result) => {
|
|
78
|
+
// Only update if this is still the active promise
|
|
79
|
+
if (myCounter === promiseCounter) {
|
|
80
|
+
freshValue = result
|
|
81
|
+
hasFreshValue = true
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch((err) => {
|
|
85
|
+
if (myCounter === promiseCounter) {
|
|
86
|
+
error = err instanceof Error ? err : new Error(String(err))
|
|
87
|
+
hasFreshValue = true // Mark as done even on error
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
get value() {
|
|
95
|
+
// Return fresh value if available and no error, otherwise current
|
|
96
|
+
return hasFreshValue && !error ? (freshValue as TData) : currentValue
|
|
97
|
+
},
|
|
98
|
+
get isRefreshing() {
|
|
99
|
+
return hasFreshPromise && !hasFreshValue
|
|
100
|
+
},
|
|
101
|
+
get error() {
|
|
102
|
+
return error
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { createEventualState }
|
|
108
|
+
export type { EventualInput, EventualState }
|
package/src/mod.ts
ADDED