@hitslop/svelte 0.2.0 → 0.3.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/README.md +28 -1
- package/dist/json-store.svelte.d.ts +6 -0
- package/dist/json-store.svelte.js +76 -22
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -37,7 +37,34 @@ validates initial, loaded, externally changed, and outgoing values using the
|
|
|
37
37
|
TypeBox interpreter; it does not coerce, insert defaults, strip unknown fields,
|
|
38
38
|
or write browser storage.
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Once loaded, JSON edits save after 150 ms without another change, or after one
|
|
41
|
+
second of continuous editing. Only one write runs at a time; edits during a write
|
|
42
|
+
coalesce into the newest follow-up snapshot. `await document.flush()` captures
|
|
43
|
+
current state immediately, bypasses the delay, and waits for pending writes.
|
|
44
|
+
Concurrent flushes share the same drain. Schema validation runs at I/O boundaries,
|
|
45
|
+
so invalid edits remain in memory and are never written.
|
|
46
|
+
|
|
47
|
+
`isDirty` stays true while changes are waiting, saving, or failed; `isSaving` is
|
|
48
|
+
true during a write. `error` contains the display message and `errorCode` contains
|
|
49
|
+
the `SlopError` code when available, including `validation_failed`,
|
|
50
|
+
`revision_conflict`, and `storage_error`. A failed save retains the latest edits.
|
|
51
|
+
Call `flush()` to retry, or make a new edit to resume automatic saving. Repeated
|
|
52
|
+
observations of an identical value do not retry a failed save.
|
|
53
|
+
|
|
54
|
+
External file changes load while the store is clean. While dirty, local edits
|
|
55
|
+
win: a revision conflict reads the new revision and retries the latest full
|
|
56
|
+
local snapshot once. This does not merge another writer's fields. Another
|
|
57
|
+
conflict stops saving and surfaces an error. After loading, explicit `reload()`
|
|
58
|
+
discards edits made before the call and adopts the file; edits made during the
|
|
59
|
+
read survive. Retrying a failed initial load also preserves local edits.
|
|
60
|
+
|
|
61
|
+
Call `destroy()` on teardown to stop observation and immediately drain a detached
|
|
62
|
+
final snapshot. It is synchronous and safe to call twice. Pending or failed saves
|
|
63
|
+
stay registered with the runtime flush barrier until a flush succeeds. For a
|
|
64
|
+
user-initiated teardown where errors should prevent navigation, await `flush()`
|
|
65
|
+
before removing the component. A destroyed store cannot reload or resume editing.
|
|
66
|
+
|
|
67
|
+
See the [Svelte authoring examples](https://github.com/hitslop/hitslop/tree/master/examples/slops).
|
|
41
68
|
|
|
42
69
|
MIT © 2026 hitSlop contributors.
|
|
43
70
|
|
|
@@ -10,6 +10,7 @@ export declare class JsonStore<S extends TSchema> {
|
|
|
10
10
|
isDirty: boolean;
|
|
11
11
|
isSaving: boolean;
|
|
12
12
|
error: string | null;
|
|
13
|
+
errorCode: "invalid_request" | "unsupported" | "revision_conflict" | "validation_failed" | "storage_error" | "limit_exceeded" | "closed" | null;
|
|
13
14
|
revision: string | null;
|
|
14
15
|
lastChangeSource: string;
|
|
15
16
|
private persister;
|
|
@@ -17,10 +18,15 @@ export declare class JsonStore<S extends TSchema> {
|
|
|
17
18
|
private unwatch;
|
|
18
19
|
private stopEffect;
|
|
19
20
|
private unregisterFlush;
|
|
21
|
+
private destroyed;
|
|
22
|
+
private finalSnapshot;
|
|
23
|
+
private finalError;
|
|
20
24
|
constructor(options: JsonStoreOptions<S>);
|
|
21
25
|
reload(): Promise<void>;
|
|
22
26
|
destroy(): void;
|
|
23
27
|
flush(): Promise<void>;
|
|
28
|
+
private getLocal;
|
|
29
|
+
private setError;
|
|
24
30
|
private parse;
|
|
25
31
|
}
|
|
26
32
|
export declare function jsonStore<S extends TSchema>(options: JsonStoreOptions<S>): JsonStore<S>;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { slop } from "@hitslop/runtime";
|
|
1
|
+
import { slop, SlopError } from "@hitslop/runtime";
|
|
2
2
|
import { JsonPersister, registerFlush } from "@hitslop/runtime/adapter";
|
|
3
3
|
import { untrack } from "svelte";
|
|
4
4
|
import { validate } from "@hitslop/schema/validation";
|
|
5
5
|
import { assertJSON } from "@hitslop/schema/json";
|
|
6
6
|
const snapshot = (value) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
try {
|
|
8
|
+
const detached = $state.snapshot(value);
|
|
9
|
+
assertJSON(detached);
|
|
10
|
+
return { json: JSON.stringify(detached), value: detached };
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new SlopError("validation_failed", error instanceof Error ? error.message : String(error));
|
|
14
|
+
}
|
|
11
15
|
};
|
|
12
16
|
export class JsonStore {
|
|
13
17
|
current = $state();
|
|
@@ -16,6 +20,7 @@ export class JsonStore {
|
|
|
16
20
|
isDirty = $state(false);
|
|
17
21
|
isSaving = $state(false);
|
|
18
22
|
error = $state(null);
|
|
23
|
+
errorCode = $state(null);
|
|
19
24
|
revision = $state(null);
|
|
20
25
|
lastChangeSource = $state("package");
|
|
21
26
|
persister;
|
|
@@ -23,6 +28,9 @@ export class JsonStore {
|
|
|
23
28
|
unwatch = null;
|
|
24
29
|
stopEffect = null;
|
|
25
30
|
unregisterFlush = null;
|
|
31
|
+
destroyed = false;
|
|
32
|
+
finalSnapshot = null;
|
|
33
|
+
finalError = null;
|
|
26
34
|
constructor(options) {
|
|
27
35
|
this.schema = options.schema;
|
|
28
36
|
const fallbackSnapshot = snapshot(this.parse(options.initial));
|
|
@@ -38,13 +46,18 @@ export class JsonStore {
|
|
|
38
46
|
const result = await slop.json.read();
|
|
39
47
|
return { ...result, value: this.parse(result.value) };
|
|
40
48
|
},
|
|
41
|
-
write: (value, revision) => slop.json.write(this.parse(value), revision),
|
|
49
|
+
write: (value, revision) => slop.json.write(this.parse(value, false), revision),
|
|
50
|
+
},
|
|
51
|
+
getLocal: () => this.getLocal(),
|
|
52
|
+
onAdopt: (value, source) => {
|
|
53
|
+
this.current = value;
|
|
54
|
+
this.lastChangeSource = source;
|
|
55
|
+
if (this.destroyed)
|
|
56
|
+
this.finalSnapshot = snapshot(value);
|
|
42
57
|
},
|
|
43
|
-
getLocal: () => snapshot(this.parse(this.current)),
|
|
44
|
-
onAdopt: (value, source) => { this.current = value; this.lastChangeSource = source; },
|
|
45
58
|
onRevision: (revision) => { this.revision = revision; },
|
|
46
59
|
onSource: (source) => { this.lastChangeSource = source; },
|
|
47
|
-
onError: (
|
|
60
|
+
onError: (error) => this.setError(error),
|
|
48
61
|
onStatus: ({ isDirty, isSaving }) => { this.isDirty = isDirty; this.isSaving = isSaving; },
|
|
49
62
|
});
|
|
50
63
|
// Snapshotting reads the complete proxy tree, so one effect run observes all
|
|
@@ -54,10 +67,10 @@ export class JsonStore {
|
|
|
54
67
|
$effect(() => {
|
|
55
68
|
let local;
|
|
56
69
|
try {
|
|
57
|
-
local = snapshot(this.
|
|
70
|
+
local = snapshot(this.current);
|
|
58
71
|
}
|
|
59
72
|
catch (error) {
|
|
60
|
-
untrack(() =>
|
|
73
|
+
untrack(() => this.persister.localInvalid(error));
|
|
61
74
|
return;
|
|
62
75
|
}
|
|
63
76
|
untrack(() => this.persister.localChanged(local.json, local.value));
|
|
@@ -68,36 +81,77 @@ export class JsonStore {
|
|
|
68
81
|
this.unwatch = slop.json.onChange((event) => this.persister.externalChanged(event.revision));
|
|
69
82
|
}
|
|
70
83
|
async reload() {
|
|
84
|
+
if (this.destroyed)
|
|
85
|
+
throw new SlopError("closed", "JSON store is destroyed");
|
|
71
86
|
this.isLoading = true;
|
|
72
87
|
try {
|
|
73
88
|
await this.persister.reload();
|
|
74
89
|
this.isReady = true;
|
|
75
90
|
}
|
|
76
91
|
catch (error) {
|
|
77
|
-
this.
|
|
92
|
+
this.setError(error);
|
|
78
93
|
}
|
|
79
94
|
finally {
|
|
80
95
|
this.isLoading = false;
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
98
|
destroy() {
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
if (this.destroyed)
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
this.finalSnapshot = snapshot(this.current);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
this.finalError = error;
|
|
106
|
+
}
|
|
107
|
+
this.destroyed = true;
|
|
88
108
|
this.unwatch?.();
|
|
89
109
|
this.unwatch = null;
|
|
90
110
|
this.stopEffect?.();
|
|
91
111
|
this.stopEffect = null;
|
|
112
|
+
// Keep failed writes registered so the host barrier can report/retry them.
|
|
113
|
+
void this.flush().catch(() => undefined);
|
|
92
114
|
}
|
|
93
115
|
async flush() {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
try {
|
|
117
|
+
let local;
|
|
118
|
+
try {
|
|
119
|
+
local = this.getLocal();
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
this.persister.localInvalid(error);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
this.persister.localChanged(local.json, local.value);
|
|
126
|
+
await this.persister.flush();
|
|
127
|
+
if (this.destroyed) {
|
|
128
|
+
this.unregisterFlush?.();
|
|
129
|
+
this.unregisterFlush = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
this.setError(error);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
getLocal() {
|
|
138
|
+
if (this.finalError)
|
|
139
|
+
throw this.finalError;
|
|
140
|
+
return this.finalSnapshot ?? snapshot(this.current);
|
|
141
|
+
}
|
|
142
|
+
setError(error) {
|
|
143
|
+
this.error = error == null ? null : error instanceof Error ? error.message : String(error);
|
|
144
|
+
this.errorCode = error instanceof SlopError ? error.code : null;
|
|
97
145
|
}
|
|
98
|
-
parse(value) {
|
|
99
|
-
|
|
100
|
-
|
|
146
|
+
parse(value, checkJSON = true) {
|
|
147
|
+
try {
|
|
148
|
+
if (checkJSON)
|
|
149
|
+
assertJSON(value);
|
|
150
|
+
return validate(this.schema, value);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
throw new SlopError("validation_failed", error instanceof Error ? error.message : String(error));
|
|
154
|
+
}
|
|
101
155
|
}
|
|
102
156
|
}
|
|
103
157
|
export function jsonStore(options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hitslop/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Svelte 5 state adapters for the hitSlop runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"test": "bun run --cwd ../runtime build && bun test"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@hitslop/runtime": "^0.
|
|
42
|
-
"@hitslop/schema": "^0.
|
|
41
|
+
"@hitslop/runtime": "^0.3.0",
|
|
42
|
+
"@hitslop/schema": "^0.3.0",
|
|
43
43
|
"typebox": "^1.3.26"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|