@ternkit/tern 1.2.1 → 1.4.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/dist/goscript/index.svelte.d.ts +32 -2
- package/dist/goscript/index.svelte.js +257 -31
- package/dist/goscript/patch.d.ts +6 -0
- package/dist/goscript/patch.js +25 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/runtime/validate.d.ts +31 -0
- package/dist/runtime/validate.js +168 -0
- package/dist/server/intl.js +55 -12
- package/dist/ui/Accordion.svelte +84 -0
- package/dist/ui/Accordion.svelte.d.ts +20 -0
- package/dist/ui/Alert.svelte +76 -0
- package/dist/ui/Alert.svelte.d.ts +12 -0
- package/dist/ui/AlertDialog.svelte +79 -0
- package/dist/ui/AlertDialog.svelte.d.ts +24 -0
- package/dist/ui/AreaChart.svelte +9 -0
- package/dist/ui/AreaChart.svelte.d.ts +26 -0
- package/dist/ui/AspectRatio.svelte +33 -0
- package/dist/ui/AspectRatio.svelte.d.ts +10 -0
- package/dist/ui/Avatar.svelte +48 -0
- package/dist/ui/Avatar.svelte.d.ts +10 -0
- package/dist/ui/Badge.svelte +54 -0
- package/dist/ui/Badge.svelte.d.ts +10 -0
- package/dist/ui/BarChart.svelte +170 -0
- package/dist/ui/BarChart.svelte.d.ts +40 -0
- package/dist/ui/Breadcrumb.svelte +92 -0
- package/dist/ui/Breadcrumb.svelte.d.ts +14 -0
- package/dist/ui/Button.svelte +136 -0
- package/dist/ui/Button.svelte.d.ts +18 -0
- package/dist/ui/ButtonGroup.svelte +61 -0
- package/dist/ui/ButtonGroup.svelte.d.ts +12 -0
- package/dist/ui/Calendar.svelte +265 -0
- package/dist/ui/Calendar.svelte.d.ts +28 -0
- package/dist/ui/Card.svelte +74 -0
- package/dist/ui/Card.svelte.d.ts +18 -0
- package/dist/ui/Carousel.svelte +169 -0
- package/dist/ui/Carousel.svelte.d.ts +36 -0
- package/dist/ui/ChartFrame.svelte +174 -0
- package/dist/ui/ChartFrame.svelte.d.ts +24 -0
- package/dist/ui/Checkbox.svelte +112 -0
- package/dist/ui/Checkbox.svelte.d.ts +14 -0
- package/dist/ui/Collapsible.svelte +59 -0
- package/dist/ui/Collapsible.svelte.d.ts +12 -0
- package/dist/ui/Combobox.svelte +350 -0
- package/dist/ui/Combobox.svelte.d.ts +24 -0
- package/dist/ui/Command.svelte +268 -0
- package/dist/ui/Command.svelte.d.ts +28 -0
- package/dist/ui/ConsentScreen.svelte +125 -0
- package/dist/ui/ConsentScreen.svelte.d.ts +21 -0
- package/dist/ui/ContextMenu.svelte +78 -0
- package/dist/ui/ContextMenu.svelte.d.ts +13 -0
- package/dist/ui/DataTable.svelte +277 -0
- package/dist/ui/DataTable.svelte.d.ts +53 -0
- package/dist/ui/DatePicker.svelte +138 -0
- package/dist/ui/DatePicker.svelte.d.ts +25 -0
- package/dist/ui/Dialog.svelte +301 -0
- package/dist/ui/Dialog.svelte.d.ts +30 -0
- package/dist/ui/DonutChart.svelte +115 -0
- package/dist/ui/DonutChart.svelte.d.ts +40 -0
- package/dist/ui/Drawer.svelte +19 -0
- package/dist/ui/Drawer.svelte.d.ts +14 -0
- package/dist/ui/Empty.svelte +62 -0
- package/dist/ui/Empty.svelte.d.ts +15 -0
- package/dist/ui/Field.svelte +97 -0
- package/dist/ui/Field.svelte.d.ts +19 -0
- package/dist/ui/Form.svelte +159 -0
- package/dist/ui/Form.svelte.d.ts +21 -0
- package/dist/ui/HoverCard.svelte +106 -0
- package/dist/ui/HoverCard.svelte.d.ts +26 -0
- package/dist/ui/Input.svelte +62 -0
- package/dist/ui/Input.svelte.d.ts +7 -0
- package/dist/ui/InputGroup.svelte +80 -0
- package/dist/ui/InputGroup.svelte.d.ts +13 -0
- package/dist/ui/InputOTP.svelte +140 -0
- package/dist/ui/InputOTP.svelte.d.ts +18 -0
- package/dist/ui/Item.svelte +86 -0
- package/dist/ui/Item.svelte.d.ts +17 -0
- package/dist/ui/Kbd.svelte +24 -0
- package/dist/ui/Kbd.svelte.d.ts +7 -0
- package/dist/ui/Label.svelte +29 -0
- package/dist/ui/Label.svelte.d.ts +10 -0
- package/dist/ui/LineChart.svelte +171 -0
- package/dist/ui/LineChart.svelte.d.ts +42 -0
- package/dist/ui/Menu.svelte +103 -0
- package/dist/ui/Menu.svelte.d.ts +15 -0
- package/dist/ui/MenuList.svelte +317 -0
- package/dist/ui/MenuList.svelte.d.ts +29 -0
- package/dist/ui/Menubar.svelte +133 -0
- package/dist/ui/Menubar.svelte.d.ts +14 -0
- package/dist/ui/NavigationMenu.svelte +208 -0
- package/dist/ui/NavigationMenu.svelte.d.ts +18 -0
- package/dist/ui/Pagination.svelte +114 -0
- package/dist/ui/Pagination.svelte.d.ts +16 -0
- package/dist/ui/PasswordInput.svelte +134 -0
- package/dist/ui/PasswordInput.svelte.d.ts +11 -0
- package/dist/ui/Popover.svelte +127 -0
- package/dist/ui/Popover.svelte.d.ts +29 -0
- package/dist/ui/Progress.svelte +73 -0
- package/dist/ui/Progress.svelte.d.ts +12 -0
- package/dist/ui/RadioGroup.svelte +105 -0
- package/dist/ui/RadioGroup.svelte.d.ts +21 -0
- package/dist/ui/Resizable.svelte +122 -0
- package/dist/ui/Resizable.svelte.d.ts +16 -0
- package/dist/ui/ScrollArea.svelte +43 -0
- package/dist/ui/ScrollArea.svelte.d.ts +13 -0
- package/dist/ui/Select.svelte +77 -0
- package/dist/ui/Select.svelte.d.ts +18 -0
- package/dist/ui/SelectMenu.svelte +301 -0
- package/dist/ui/SelectMenu.svelte.d.ts +18 -0
- package/dist/ui/Separator.svelte +19 -0
- package/dist/ui/Separator.svelte.d.ts +7 -0
- package/dist/ui/Sheet.svelte +21 -0
- package/dist/ui/Sheet.svelte.d.ts +16 -0
- package/dist/ui/Sidebar.svelte +263 -0
- package/dist/ui/Sidebar.svelte.d.ts +35 -0
- package/dist/ui/SignIn.svelte +182 -0
- package/dist/ui/SignIn.svelte.d.ts +38 -0
- package/dist/ui/SignUp.svelte +106 -0
- package/dist/ui/SignUp.svelte.d.ts +23 -0
- package/dist/ui/Skeleton.svelte +29 -0
- package/dist/ui/Skeleton.svelte.d.ts +9 -0
- package/dist/ui/Slider.svelte +196 -0
- package/dist/ui/Slider.svelte.d.ts +19 -0
- package/dist/ui/Sparkline.svelte +23 -0
- package/dist/ui/Sparkline.svelte.d.ts +11 -0
- package/dist/ui/Spinner.svelte +42 -0
- package/dist/ui/Spinner.svelte.d.ts +8 -0
- package/dist/ui/Stat.svelte +91 -0
- package/dist/ui/Stat.svelte.d.ts +18 -0
- package/dist/ui/Switch.svelte +90 -0
- package/dist/ui/Switch.svelte.d.ts +12 -0
- package/dist/ui/Table.svelte +70 -0
- package/dist/ui/Table.svelte.d.ts +36 -0
- package/dist/ui/Tabs.svelte +112 -0
- package/dist/ui/Tabs.svelte.d.ts +19 -0
- package/dist/ui/Textarea.svelte +52 -0
- package/dist/ui/Textarea.svelte.d.ts +7 -0
- package/dist/ui/Toaster.svelte +146 -0
- package/dist/ui/Toaster.svelte.d.ts +6 -0
- package/dist/ui/Toggle.svelte +82 -0
- package/dist/ui/Toggle.svelte.d.ts +17 -0
- package/dist/ui/ToggleGroup.svelte +143 -0
- package/dist/ui/ToggleGroup.svelte.d.ts +27 -0
- package/dist/ui/Tooltip.svelte +85 -0
- package/dist/ui/Tooltip.svelte.d.ts +16 -0
- package/dist/ui/chart.d.ts +26 -0
- package/dist/ui/chart.js +89 -0
- package/dist/ui/dates.d.ts +13 -0
- package/dist/ui/dates.js +64 -0
- package/dist/ui/field.d.ts +18 -0
- package/dist/ui/field.js +8 -0
- package/dist/ui/floating.d.ts +44 -0
- package/dist/ui/floating.js +97 -0
- package/dist/ui/form.d.ts +12 -0
- package/dist/ui/form.js +8 -0
- package/dist/ui/index.d.ts +78 -0
- package/dist/ui/index.js +70 -0
- package/dist/ui/menu.d.ts +25 -0
- package/dist/ui/menu.js +1 -0
- package/dist/ui/options.d.ts +17 -0
- package/dist/ui/options.js +22 -0
- package/dist/ui/table.d.ts +26 -0
- package/dist/ui/table.js +1 -0
- package/dist/ui/theme.css +176 -0
- package/dist/ui/toast.svelte.d.ts +19 -0
- package/dist/ui/toast.svelte.js +18 -0
- package/dist/vite/goscript.d.ts +14 -0
- package/dist/vite/goscript.js +45 -16
- package/dist/vite/index.js +15 -4
- package/package.json +7 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { applyPatch, type PatchOp } from './patch.js';
|
|
2
|
+
export { applyPatch, type PatchOp };
|
|
1
3
|
type Instance = {
|
|
2
4
|
call(name: string, args: string): string | null;
|
|
3
5
|
destroy(): void;
|
|
@@ -30,9 +32,38 @@ export declare function useGo(id: string, initial: Record<string, unknown>, runt
|
|
|
30
32
|
declare global {
|
|
31
33
|
var __tern_live_initial: ((id: string, props: string) => string) | undefined;
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
/** A Func of a live component, as the markup sees it. */
|
|
36
|
+
export interface LiveFunc<A extends unknown[] = unknown[], R = unknown> {
|
|
37
|
+
(...args: A): Promise<R>;
|
|
38
|
+
/** Whether a call is waiting for the server. */
|
|
39
|
+
readonly pending: boolean;
|
|
40
|
+
/** The last call's error message (from goscript.Fail), or null. */
|
|
41
|
+
readonly error: string | null;
|
|
42
|
+
/** Calls the Func, showing values for these state names until it returns. */
|
|
43
|
+
optimistic(values: Record<string, unknown>, ...args: A): Promise<R>;
|
|
44
|
+
/** A version that waits until calls stop for ms (default 200), for input events. */
|
|
45
|
+
debounce(ms?: number): (...args: A) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface LiveComponent<S extends Record<string, unknown> = Record<string, unknown>> extends GoComponent {
|
|
48
|
+
readonly state: S;
|
|
34
49
|
/** The server-rendered state, embedded in the page for hydration. */
|
|
35
50
|
readonly stateScript: string;
|
|
51
|
+
/** Whether the page is connected to the server. */
|
|
52
|
+
readonly connected: boolean;
|
|
53
|
+
/** Whether any call is waiting for the server. */
|
|
54
|
+
readonly pending: boolean;
|
|
55
|
+
/** Why the component failed to mount (such as goscript Reject's message), or null. */
|
|
56
|
+
readonly error: string | null;
|
|
57
|
+
/** Whether the state has arrived; the markup renders once it has. */
|
|
58
|
+
readonly loaded: boolean;
|
|
59
|
+
/** Everything the parent passed, such as children snippets and onevent handlers. */
|
|
60
|
+
readonly props: Record<string, any>;
|
|
61
|
+
/** Bindable state: bind:value={live.bind.name} for a State declared with goscript.Bind. */
|
|
62
|
+
readonly bind: S;
|
|
63
|
+
/** Sets a State declared with goscript.Bind; the page shows value at once. */
|
|
64
|
+
set<K extends keyof S & string>(name: K, value: S[K]): Promise<void>;
|
|
65
|
+
/** The Func named name. */
|
|
66
|
+
fn<A extends unknown[] = unknown[], R = unknown>(name: string): LiveFunc<A, R>;
|
|
36
67
|
}
|
|
37
68
|
/**
|
|
38
69
|
* Mounts a live Go component; code generated from <script lang="go" live>
|
|
@@ -41,4 +72,3 @@ export interface LiveComponent extends GoComponent {
|
|
|
41
72
|
* /_tern/live, where its Go instance runs.
|
|
42
73
|
*/
|
|
43
74
|
export declare function useLive(id: string, uid: string, props: () => Record<string, unknown>): LiveComponent;
|
|
44
|
-
export {};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// The browser side of the experimental <script lang="go">: loads the app's
|
|
2
2
|
// Go WebAssembly module once, mounts a Go instance per component, and keeps
|
|
3
3
|
// the values Go publishes in Svelte state. See docs/experimental-go-script.md.
|
|
4
|
+
import { untrack } from 'svelte';
|
|
4
5
|
import { getBase } from '../runtime/base.js';
|
|
6
|
+
import { applyPatch } from './patch.js';
|
|
7
|
+
export { applyPatch };
|
|
5
8
|
let loading = null;
|
|
6
9
|
function load(runtime, wasm) {
|
|
7
10
|
loading ??= (async () => {
|
|
@@ -65,6 +68,87 @@ export function useGo(id, initial, runtime, wasm, props) {
|
|
|
65
68
|
}
|
|
66
69
|
};
|
|
67
70
|
}
|
|
71
|
+
function clone(v) {
|
|
72
|
+
return v === undefined ? v : JSON.parse(JSON.stringify(v));
|
|
73
|
+
}
|
|
74
|
+
/** The browser side of one live component instance. */
|
|
75
|
+
class LiveInstance {
|
|
76
|
+
id;
|
|
77
|
+
props;
|
|
78
|
+
/** What the markup shows: the server's values, or optimistic ones while a call is pending. */
|
|
79
|
+
state;
|
|
80
|
+
/** The server's values, as last sent. */
|
|
81
|
+
#server;
|
|
82
|
+
#holds = new Map();
|
|
83
|
+
calls = new Map();
|
|
84
|
+
pendingCalls = $state({});
|
|
85
|
+
errors = $state({});
|
|
86
|
+
error = $state(null);
|
|
87
|
+
/** Whether the component has its state: from the server render, or the first message. */
|
|
88
|
+
loaded = $state(false);
|
|
89
|
+
mounted = false;
|
|
90
|
+
constructor(id, props, initial) {
|
|
91
|
+
this.id = id;
|
|
92
|
+
this.props = props;
|
|
93
|
+
const { __tern_error: error, ...values } = initial ?? {};
|
|
94
|
+
this.state = $state(clone(values));
|
|
95
|
+
this.#server = clone(values);
|
|
96
|
+
this.loaded = initial !== null;
|
|
97
|
+
if (typeof error === 'string')
|
|
98
|
+
this.error = error;
|
|
99
|
+
}
|
|
100
|
+
apply(m) {
|
|
101
|
+
this.loaded = true;
|
|
102
|
+
for (const [name, v] of Object.entries(m.set ?? {})) {
|
|
103
|
+
this.#server[name] = clone(v);
|
|
104
|
+
if (!this.#holds.has(name))
|
|
105
|
+
this.state[name] = v;
|
|
106
|
+
}
|
|
107
|
+
for (const [name, ops] of Object.entries(m.patch ?? {})) {
|
|
108
|
+
this.#server[name] = applyPatch(this.#server[name], clone(ops));
|
|
109
|
+
if (!this.#holds.has(name))
|
|
110
|
+
this.state[name] = applyPatch(this.state[name], ops);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Shows value for name until the operation holding it settles. */
|
|
114
|
+
hold(name, value) {
|
|
115
|
+
this.#holds.set(name, (this.#holds.get(name) ?? 0) + 1);
|
|
116
|
+
this.state[name] = value;
|
|
117
|
+
}
|
|
118
|
+
release(names) {
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
const n = (this.#holds.get(name) ?? 1) - 1;
|
|
121
|
+
if (n > 0) {
|
|
122
|
+
this.#holds.set(name, n);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
this.#holds.delete(name);
|
|
126
|
+
this.state[name] = clone(this.#server[name]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
begin(seq, call) {
|
|
130
|
+
this.calls.set(seq, call);
|
|
131
|
+
this.pendingCalls[call.name] = (this.pendingCalls[call.name] ?? 0) + 1;
|
|
132
|
+
}
|
|
133
|
+
settle(seq, value, error) {
|
|
134
|
+
const call = this.calls.get(seq);
|
|
135
|
+
if (!call)
|
|
136
|
+
return;
|
|
137
|
+
this.calls.delete(seq);
|
|
138
|
+
this.pendingCalls[call.name] = (this.pendingCalls[call.name] ?? 1) - 1;
|
|
139
|
+
this.release(call.holds);
|
|
140
|
+
this.errors[call.name] = error ?? null;
|
|
141
|
+
if (error)
|
|
142
|
+
call.reject(new Error(error));
|
|
143
|
+
else
|
|
144
|
+
call.resolve(value);
|
|
145
|
+
}
|
|
146
|
+
/** The values to restore after reconnecting to a new server process. */
|
|
147
|
+
snapshot() {
|
|
148
|
+
return $state.snapshot(this.state);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const status = $state({ connected: false });
|
|
68
152
|
class LiveConnection {
|
|
69
153
|
#ws = null;
|
|
70
154
|
#refs = new Map();
|
|
@@ -76,6 +160,12 @@ class LiveConnection {
|
|
|
76
160
|
constructor(url) {
|
|
77
161
|
this.#url = url;
|
|
78
162
|
}
|
|
163
|
+
#mountMessage(ref, r) {
|
|
164
|
+
const props = untrack(() => r.props());
|
|
165
|
+
// After a reconnect, the server has forgotten the instance: values
|
|
166
|
+
// declared with Bind or Restore come back from what the page shows.
|
|
167
|
+
return JSON.stringify({ op: 'mount', ref, id: r.id, props, restore: r.mounted ? r.snapshot() : undefined });
|
|
168
|
+
}
|
|
79
169
|
#connect() {
|
|
80
170
|
const u = new URL(this.#url, location.href);
|
|
81
171
|
u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
@@ -83,40 +173,71 @@ class LiveConnection {
|
|
|
83
173
|
this.#ws = ws;
|
|
84
174
|
ws.onopen = () => {
|
|
85
175
|
this.#attempt = 0;
|
|
86
|
-
|
|
87
|
-
for (const [ref, r] of this.#refs)
|
|
88
|
-
ws.send(
|
|
89
|
-
|
|
90
|
-
|
|
176
|
+
status.connected = true;
|
|
177
|
+
for (const [ref, r] of this.#refs) {
|
|
178
|
+
ws.send(this.#mountMessage(ref, r));
|
|
179
|
+
r.mounted = true;
|
|
180
|
+
}
|
|
181
|
+
for (const m of this.#queue.splice(0)) {
|
|
182
|
+
if (m.ref !== undefined && !this.#refs.has(m.ref))
|
|
183
|
+
continue;
|
|
184
|
+
ws.send(m.data);
|
|
185
|
+
if (m.seq !== undefined)
|
|
186
|
+
this.#markSent(m.ref, m.seq);
|
|
187
|
+
}
|
|
91
188
|
};
|
|
92
189
|
ws.onmessage = (e) => {
|
|
93
190
|
const m = JSON.parse(e.data);
|
|
94
191
|
const r = this.#refs.get(m.ref);
|
|
95
192
|
if (!r)
|
|
96
193
|
return;
|
|
97
|
-
if (m.op === 'state'
|
|
98
|
-
r.
|
|
99
|
-
else if (m.op === 'result')
|
|
100
|
-
r.
|
|
101
|
-
|
|
194
|
+
if (m.op === 'state')
|
|
195
|
+
r.apply(m);
|
|
196
|
+
else if (m.op === 'result')
|
|
197
|
+
r.settle(m.seq, m.value, m.error);
|
|
198
|
+
else if (m.op === 'event') {
|
|
199
|
+
const handler = untrack(() => r.props())['on' + m.name];
|
|
200
|
+
if (typeof handler === 'function')
|
|
201
|
+
handler(m.value);
|
|
102
202
|
}
|
|
103
|
-
else if (m.op === 'error')
|
|
203
|
+
else if (m.op === 'error') {
|
|
204
|
+
r.error = m.error ?? 'error';
|
|
205
|
+
r.loaded = true;
|
|
104
206
|
console.error(`[tern] live component ${r.id}: ${m.error}`);
|
|
207
|
+
}
|
|
105
208
|
};
|
|
106
209
|
ws.onclose = () => {
|
|
107
|
-
this.#ws
|
|
210
|
+
if (this.#ws === ws)
|
|
211
|
+
this.#ws = null;
|
|
212
|
+
status.connected = false;
|
|
213
|
+
// Calls that reached the old connection have no answer coming.
|
|
214
|
+
for (const r of this.#refs.values()) {
|
|
215
|
+
for (const [seq, call] of r.calls)
|
|
216
|
+
if (call.sent)
|
|
217
|
+
r.settle(seq, undefined, 'connection lost');
|
|
218
|
+
}
|
|
108
219
|
if (this.#refs.size === 0)
|
|
109
220
|
return;
|
|
110
|
-
const delay = Math.min(10_000,
|
|
111
|
-
setTimeout(() => this.#refs.size && this.#connect(), delay);
|
|
221
|
+
const delay = Math.min(10_000, 250 * 2 ** this.#attempt++) * (0.75 + Math.random() / 2);
|
|
222
|
+
setTimeout(() => this.#refs.size && !this.#ws && this.#connect(), delay);
|
|
112
223
|
};
|
|
113
224
|
}
|
|
225
|
+
#markSent(ref, seq) {
|
|
226
|
+
const call = this.#refs.get(ref)?.calls.get(seq);
|
|
227
|
+
if (call)
|
|
228
|
+
call.sent = true;
|
|
229
|
+
}
|
|
114
230
|
#send(m) {
|
|
115
231
|
const data = JSON.stringify(m);
|
|
116
|
-
|
|
232
|
+
const seq = m.seq;
|
|
233
|
+
const ref = m.ref;
|
|
234
|
+
if (this.#ws?.readyState === WebSocket.OPEN) {
|
|
117
235
|
this.#ws.send(data);
|
|
236
|
+
if (seq !== undefined)
|
|
237
|
+
this.#markSent(ref, seq);
|
|
238
|
+
}
|
|
118
239
|
else {
|
|
119
|
-
this.#queue.push(data);
|
|
240
|
+
this.#queue.push({ data, seq, ref });
|
|
120
241
|
if (!this.#ws)
|
|
121
242
|
this.#connect();
|
|
122
243
|
}
|
|
@@ -124,7 +245,12 @@ class LiveConnection {
|
|
|
124
245
|
mount(r) {
|
|
125
246
|
const ref = this.#nextRef++;
|
|
126
247
|
this.#refs.set(ref, r);
|
|
127
|
-
this.#
|
|
248
|
+
if (this.#ws?.readyState === WebSocket.OPEN) {
|
|
249
|
+
this.#ws.send(this.#mountMessage(ref, r));
|
|
250
|
+
r.mounted = true;
|
|
251
|
+
}
|
|
252
|
+
else if (!this.#ws)
|
|
253
|
+
this.#connect();
|
|
128
254
|
return ref;
|
|
129
255
|
}
|
|
130
256
|
unmount(ref) {
|
|
@@ -132,19 +258,44 @@ class LiveConnection {
|
|
|
132
258
|
if (this.#ws?.readyState === WebSocket.OPEN)
|
|
133
259
|
this.#ws.send(JSON.stringify({ op: 'unmount', ref }));
|
|
134
260
|
if (this.#refs.size === 0) {
|
|
135
|
-
this.#ws
|
|
261
|
+
const ws = this.#ws;
|
|
136
262
|
this.#ws = null;
|
|
263
|
+
this.#queue = [];
|
|
264
|
+
ws?.close(1000);
|
|
137
265
|
}
|
|
138
266
|
}
|
|
139
|
-
|
|
267
|
+
props(ref, props) {
|
|
268
|
+
this.#send({ op: 'props', ref, props });
|
|
269
|
+
}
|
|
270
|
+
call(ref, name, args, optimistic) {
|
|
140
271
|
const r = this.#refs.get(ref);
|
|
141
272
|
if (!r)
|
|
142
|
-
return Promise.
|
|
273
|
+
return Promise.reject(new Error('not mounted'));
|
|
143
274
|
const seq = this.#seq++;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
275
|
+
const holds = Object.keys(optimistic ?? {});
|
|
276
|
+
for (const k of holds)
|
|
277
|
+
r.hold(k, optimistic[k]);
|
|
278
|
+
const p = new Promise((resolve, reject) => {
|
|
279
|
+
r.begin(seq, { resolve, reject, name, holds, sent: false });
|
|
147
280
|
});
|
|
281
|
+
this.#send({ op: 'call', ref, seq, name, args });
|
|
282
|
+
// The error is also in fn.error; don't report unawaited calls as
|
|
283
|
+
// unhandled rejections.
|
|
284
|
+
p.catch(() => { });
|
|
285
|
+
return p;
|
|
286
|
+
}
|
|
287
|
+
set(ref, name, value) {
|
|
288
|
+
const r = this.#refs.get(ref);
|
|
289
|
+
if (!r)
|
|
290
|
+
return Promise.reject(new Error('not mounted'));
|
|
291
|
+
const seq = this.#seq++;
|
|
292
|
+
r.hold(name, value);
|
|
293
|
+
const p = new Promise((resolve, reject) => {
|
|
294
|
+
r.begin(seq, { resolve, reject, name: '$set:' + name, holds: [name], sent: false });
|
|
295
|
+
});
|
|
296
|
+
this.#send({ op: 'set', ref, seq, name, value: $state.snapshot(value) });
|
|
297
|
+
p.catch(() => { });
|
|
298
|
+
return p;
|
|
148
299
|
}
|
|
149
300
|
}
|
|
150
301
|
let connection = null;
|
|
@@ -159,7 +310,9 @@ function escapeJSON(s) {
|
|
|
159
310
|
*/
|
|
160
311
|
export function useLive(id, uid, props) {
|
|
161
312
|
const elementID = `tern-live-${uid}`;
|
|
162
|
-
|
|
313
|
+
// After a client-side navigation there is no server-rendered state: the
|
|
314
|
+
// markup waits for the first message (see the preprocessor).
|
|
315
|
+
let initial = null;
|
|
163
316
|
let stateScript = '';
|
|
164
317
|
if (typeof window === 'undefined') {
|
|
165
318
|
const raw = globalThis.__tern_live_initial?.(id, JSON.stringify(props())) ?? '{}';
|
|
@@ -173,14 +326,13 @@ export function useLive(id, uid, props) {
|
|
|
173
326
|
stateScript = el.outerHTML;
|
|
174
327
|
}
|
|
175
328
|
}
|
|
176
|
-
const
|
|
329
|
+
const inst = new LiveInstance(id, props, initial);
|
|
177
330
|
let ref = 0;
|
|
178
331
|
let ready = $state(false);
|
|
179
332
|
if (typeof window !== 'undefined') {
|
|
180
333
|
$effect(() => {
|
|
181
334
|
connection ??= new LiveConnection(`${getBase()}/_tern/live`);
|
|
182
|
-
|
|
183
|
-
ref = connection.mount(live);
|
|
335
|
+
ref = connection.mount(inst);
|
|
184
336
|
ready = true;
|
|
185
337
|
return () => {
|
|
186
338
|
connection?.unmount(ref);
|
|
@@ -188,17 +340,91 @@ export function useLive(id, uid, props) {
|
|
|
188
340
|
ready = false;
|
|
189
341
|
};
|
|
190
342
|
});
|
|
343
|
+
// New props from the parent go to the server without remounting.
|
|
344
|
+
let sent = JSON.stringify(untrack(props));
|
|
345
|
+
$effect(() => {
|
|
346
|
+
const next = JSON.stringify(props());
|
|
347
|
+
if (next !== sent && ref) {
|
|
348
|
+
sent = next;
|
|
349
|
+
connection.props(ref, JSON.parse(next));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
191
352
|
}
|
|
353
|
+
const call = (name, args, optimistic) => {
|
|
354
|
+
if (!ref || !connection)
|
|
355
|
+
return Promise.reject(new Error('not mounted'));
|
|
356
|
+
return connection.call(ref, name, args.map(toArg), optimistic);
|
|
357
|
+
};
|
|
358
|
+
const funcs = new Map();
|
|
359
|
+
const fn = (name) => {
|
|
360
|
+
let f = funcs.get(name);
|
|
361
|
+
if (f)
|
|
362
|
+
return f;
|
|
363
|
+
const debounced = new Map();
|
|
364
|
+
f = Object.assign((...args) => call(name, args), {
|
|
365
|
+
optimistic: (values, ...args) => call(name, args, values),
|
|
366
|
+
debounce: (ms = 200) => {
|
|
367
|
+
let d = debounced.get(ms);
|
|
368
|
+
if (!d) {
|
|
369
|
+
let timer;
|
|
370
|
+
d = (...args) => {
|
|
371
|
+
const converted = args.map(toArg); // read input values now
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
timer = setTimeout(() => void call(name, converted), ms);
|
|
374
|
+
};
|
|
375
|
+
debounced.set(ms, d);
|
|
376
|
+
}
|
|
377
|
+
return d;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
Object.defineProperties(f, {
|
|
381
|
+
pending: { get: () => (inst.pendingCalls[name] ?? 0) > 0 },
|
|
382
|
+
error: { get: () => inst.errors[name] ?? null }
|
|
383
|
+
});
|
|
384
|
+
funcs.set(name, f);
|
|
385
|
+
return f;
|
|
386
|
+
};
|
|
387
|
+
const set = (name, value) => {
|
|
388
|
+
if (!ref || !connection) {
|
|
389
|
+
inst.state[name] = value;
|
|
390
|
+
return Promise.resolve();
|
|
391
|
+
}
|
|
392
|
+
return connection.set(ref, name, value).then(() => undefined);
|
|
393
|
+
};
|
|
394
|
+
const bind = new Proxy({}, {
|
|
395
|
+
get: (_, name) => (typeof name === 'string' ? inst.state[name] : undefined),
|
|
396
|
+
set: (_, name, value) => {
|
|
397
|
+
if (typeof name === 'string')
|
|
398
|
+
void set(name, value);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
192
402
|
return {
|
|
193
|
-
state,
|
|
403
|
+
state: inst.state,
|
|
194
404
|
stateScript,
|
|
405
|
+
bind,
|
|
406
|
+
fn: fn,
|
|
407
|
+
set: set,
|
|
195
408
|
get ready() {
|
|
196
409
|
return ready;
|
|
197
410
|
},
|
|
411
|
+
get props() {
|
|
412
|
+
return props();
|
|
413
|
+
},
|
|
414
|
+
get connected() {
|
|
415
|
+
return status.connected;
|
|
416
|
+
},
|
|
417
|
+
get pending() {
|
|
418
|
+
return Object.values(inst.pendingCalls).some((n) => n > 0);
|
|
419
|
+
},
|
|
420
|
+
get error() {
|
|
421
|
+
return inst.error;
|
|
422
|
+
},
|
|
423
|
+
get loaded() {
|
|
424
|
+
return inst.loaded;
|
|
425
|
+
},
|
|
198
426
|
call(name, ...args) {
|
|
199
|
-
|
|
200
|
-
return undefined;
|
|
201
|
-
return connection.call(ref, name, args.map(toArg));
|
|
427
|
+
return call(name, args);
|
|
202
428
|
}
|
|
203
429
|
};
|
|
204
430
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type Key = string | number;
|
|
2
|
+
/** One patch operation from the server. */
|
|
3
|
+
export type PatchOp = ['s', Key[], unknown] | ['d', Key[]] | ['a', Key[], number, number, unknown[]];
|
|
4
|
+
/** Applies a patch to root and returns the new root. */
|
|
5
|
+
export declare function applyPatch(root: unknown, ops: PatchOp[]): unknown;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Applying the patches live components send; see internal/jsondiff.
|
|
2
|
+
/** Applies a patch to root and returns the new root. */
|
|
3
|
+
export function applyPatch(root, ops) {
|
|
4
|
+
for (const op of ops) {
|
|
5
|
+
const path = op[1];
|
|
6
|
+
if (path.length === 0) {
|
|
7
|
+
if (op[0] === 's')
|
|
8
|
+
root = op[2];
|
|
9
|
+
else if (op[0] === 'a')
|
|
10
|
+
root.splice(op[2], op[3], ...op[4]);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
let parent = root;
|
|
14
|
+
for (const k of path.slice(0, -1))
|
|
15
|
+
parent = parent[k];
|
|
16
|
+
const last = path[path.length - 1];
|
|
17
|
+
if (op[0] === 's')
|
|
18
|
+
parent[last] = op[2];
|
|
19
|
+
else if (op[0] === 'd')
|
|
20
|
+
delete parent[last];
|
|
21
|
+
else
|
|
22
|
+
parent[last].splice(op[2], op[3], ...op[4]);
|
|
23
|
+
}
|
|
24
|
+
return root;
|
|
25
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { goto, invalidate, invalidateAll, preloadData, applyAction } from './run
|
|
|
12
12
|
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
13
|
export type { BeforeNavigateEvent, OnNavigateEvent } from './runtime/navigation.js';
|
|
14
14
|
export { enhance } from './runtime/forms.js';
|
|
15
|
+
export { validate, checkField, type FormField, type FormErrors } from './runtime/validate.js';
|
|
15
16
|
export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
|
|
16
17
|
export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
|
|
17
18
|
export { ApiError, createApi } from './runtime/api.js';
|
|
@@ -28,4 +29,4 @@ export type { MessageKey } from './runtime/i18n.svelte.js';
|
|
|
28
29
|
export interface Messages {
|
|
29
30
|
}
|
|
30
31
|
/** The version of the Tern client runtime. */
|
|
31
|
-
export declare const VERSION = "1.
|
|
32
|
+
export declare const VERSION = "1.4.0";
|
package/dist/index.js
CHANGED
|
@@ -11,9 +11,10 @@ export { page, navigating, env } from './runtime/state.svelte.js';
|
|
|
11
11
|
export { goto, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
12
|
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
13
|
export { enhance } from './runtime/forms.js';
|
|
14
|
+
export { validate, checkField } from './runtime/validate.js';
|
|
14
15
|
export { ApiError, createApi } from './runtime/api.js';
|
|
15
16
|
export { stream, socket, EventStreamState, SocketState } from './runtime/realtime.svelte.js';
|
|
16
17
|
export { default as Image } from './image/Image.svelte';
|
|
17
18
|
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
18
19
|
/** The version of the Tern client runtime. */
|
|
19
|
-
export const VERSION = '1.
|
|
20
|
+
export const VERSION = '1.4.0';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** A form field's rules, as tern gen writes them. */
|
|
2
|
+
export interface FormField<Name extends string = string> {
|
|
3
|
+
name: Name;
|
|
4
|
+
label: string;
|
|
5
|
+
kind: 'string' | 'int' | 'float' | 'bool' | 'date' | 'datetime' | 'strings';
|
|
6
|
+
required?: boolean;
|
|
7
|
+
rules?: {
|
|
8
|
+
name: string;
|
|
9
|
+
arg?: string;
|
|
10
|
+
}[];
|
|
11
|
+
message?: string;
|
|
12
|
+
secret?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Errors by field name; empty when the form is valid. */
|
|
15
|
+
export type FormErrors<Name extends string = string> = Partial<Record<Name, string>>;
|
|
16
|
+
/** Reads a date or a date and time as Go does, or null. Times without a zone are UTC. */
|
|
17
|
+
export declare function parseFormTime(kind: 'date' | 'datetime', s: string): number | null;
|
|
18
|
+
/**
|
|
19
|
+
* Returns why a field's values are invalid, or "". values are its posted
|
|
20
|
+
* values; other reads another field's first value, for same=.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkField(f: FormField, values: string[], other?: (name: string) => string, labelOf?: (name: string) => string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Checks a form against its rules, as kit.FormAction will on the server:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { forms } from './$forms';
|
|
28
|
+
* const errors = validate(forms.create, new FormData(formElement));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function validate<Name extends string>(fields: FormField<Name>[], data: FormData | Record<string, unknown>): FormErrors<Name>;
|