Skip to content

Commit 4e0e081

Browse files
authored
Merge pull request #119 from x0k/async-binding
Async binding
2 parents eb55e26 + a775ccd commit 4e0e081

File tree

4 files changed

+89
-155
lines changed

4 files changed

+89
-155
lines changed

.changeset/free-adults-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sjsf/form": major
3+
---
4+
5+
Replace `asyncProxy` with `createAsyncBinding`

packages/form/src/fields/extra-fields/file.svelte

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
</script>
88

99
<script lang="ts">
10-
import { untrack } from "svelte";
11-
1210
import { fileToDataURL } from "@/lib/file.js";
13-
import { abortPrevious, createAction } from "@/lib/action.svelte.js";
11+
import { createAsyncBinding } from "@/lib/svelte.svelte.js";
1412
import {
1513
makeEventHandlers,
1614
getErrors,
@@ -40,48 +38,24 @@
4038
validateField(ctx, config, value)
4139
);
4240
43-
let lastValueUpdate: string | undefined;
44-
const toValue = createAction({
45-
combinator: abortPrevious,
46-
async execute(
47-
signal,
48-
files: FileList | undefined
49-
) {
50-
return files === undefined || files.length === 0
51-
? undefined
52-
: fileToDataURL(signal, files[0]!);
53-
},
54-
onSuccess(result: string | undefined) {
55-
lastValueUpdate = result;
56-
value = result;
57-
},
58-
});
59-
60-
let files = $state.raw<FileList>();
61-
const toFiles = createAction({
62-
combinator: abortPrevious,
63-
async execute(signal, value: string | undefined) {
41+
const files = createAsyncBinding({
42+
initialOutput: undefined,
43+
getInput: () => value,
44+
setInput: (v) => (value = v),
45+
async toOutput(signal, value) {
6446
const data = new DataTransfer();
6547
if (value !== undefined) {
6648
await addFile(ctx, signal, data, value);
6749
}
6850
return data.files;
6951
},
70-
onSuccess(list: FileList) {
71-
files = list;
52+
async toInput(signal, files) {
53+
return files === undefined || files.length === 0
54+
? undefined
55+
: fileToDataURL(signal, files[0]!);
7256
},
7357
});
7458
75-
$effect(() => {
76-
if (value === lastValueUpdate) {
77-
return;
78-
}
79-
untrack(() => {
80-
toValue.abort();
81-
toFiles.run(value);
82-
});
83-
});
84-
8559
const errors = $derived(getErrors(ctx, config.id));
8660
</script>
8761

@@ -97,15 +71,9 @@
9771
>
9872
<Widget
9973
type="widget"
100-
bind:value={
101-
() => files,
102-
(files) => {
103-
toFiles.abort();
104-
toValue.run(files);
105-
}
106-
}
107-
processing={toValue.isProcessed}
108-
loading={toFiles.isProcessed}
74+
bind:value={files.current}
75+
processing={files.inputProcessing}
76+
loading={files.outputProcessing}
10977
{uiOption}
11078
{handlers}
11179
{errors}

packages/form/src/fields/extra-fields/files.svelte

Lines changed: 21 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
</script>
88

99
<script lang="ts">
10-
import { untrack } from "svelte";
11-
1210
import { fileToDataURL } from "@/lib/file.js";
13-
import { abortPrevious, createAction } from "@/lib/action.svelte.js";
11+
import { createAsyncBinding } from "@/lib/svelte.svelte.js";
1412
import {
1513
makeEventHandlers,
1614
getErrors,
@@ -40,53 +38,32 @@
4038
validateField(ctx, config, value)
4139
);
4240
43-
let lastValueUpdate: string[] | undefined;
44-
const toValue = createAction({
45-
combinator: abortPrevious,
46-
async execute(signal, files: FileList | undefined) {
47-
if (files === undefined) {
48-
return undefined;
49-
}
50-
return Promise.all(
51-
Array.from(files).map((f) => fileToDataURL(signal, f))
52-
);
53-
},
54-
onSuccess(result: string[] | undefined) {
55-
lastValueUpdate = result;
56-
value = result;
57-
},
58-
});
59-
60-
let files = $state.raw<FileList>();
61-
const toFiles = createAction({
62-
combinator: abortPrevious,
63-
async execute(signal, value: string[] | undefined) {
41+
const files = createAsyncBinding({
42+
initialOutput: undefined,
43+
getInput: () => value,
44+
setInput: (v) => (value = v),
45+
isEqual: (a, b) =>
46+
// WARN: Do not optimize, avoid svelte reactive value equality warning
47+
(a === undefined && b === undefined) ||
48+
(Array.isArray(a) &&
49+
Array.isArray(b) &&
50+
a.length === b.length &&
51+
a.every((v, i) => v === b[i])),
52+
async toOutput(signal, value) {
6453
const data = new DataTransfer();
6554
if (value !== undefined) {
6655
await addFiles(ctx, signal, data, value);
6756
}
6857
return data.files;
6958
},
70-
onSuccess(list: FileList) {
71-
files = list;
59+
async toInput(signal, files) {
60+
return (
61+
files &&
62+
Promise.all(Array.from(files).map((f) => fileToDataURL(signal, f)))
63+
);
7264
},
7365
});
7466
75-
$effect(() => {
76-
if (
77-
Array.isArray(value) &&
78-
Array.isArray(lastValueUpdate) &&
79-
value.length === lastValueUpdate.length &&
80-
value.every((v, i) => v === lastValueUpdate![i])
81-
) {
82-
return;
83-
}
84-
untrack(() => {
85-
toValue.abort();
86-
toFiles.run(value);
87-
});
88-
});
89-
9067
const errors = $derived(getErrors(ctx, config.id));
9168
</script>
9269

@@ -102,15 +79,9 @@
10279
>
10380
<Widget
10481
type="widget"
105-
bind:value={
106-
() => files,
107-
(files) => {
108-
toFiles.abort();
109-
toValue.run(files);
110-
}
111-
}
112-
processing={toValue.isProcessed}
113-
loading={toFiles.isProcessed}
82+
bind:value={files.current}
83+
processing={files.inputProcessing}
84+
loading={files.outputProcessing}
11485
{uiOption}
11586
{handlers}
11687
{errors}
Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,67 @@
11
import { untrack } from "svelte";
22

3-
export interface AsyncInput<V> {
4-
/**
5-
* @param isDependencyRegistrationOnlyCall - when `true`, indicates that function is called only for dependency registration and result will be ignored
6-
*/
7-
(isDependencyRegistrationOnlyCall: false, signal: AbortSignal): Promise<V>;
8-
/**
9-
* @param signal used here to simplify typing
10-
*/
11-
(isDependencyRegistrationOnlyCall: true, signal: AbortSignal): void;
3+
import { abortPrevious, createAction } from "./action.svelte.js";
4+
5+
export interface AsyncBindingOptions<I, O> {
6+
initialOutput: O;
7+
toOutput: (signal: AbortSignal, input: I) => Promise<O>;
8+
toInput: (signal: AbortSignal, output: O) => Promise<I>;
9+
setInput: (v: I) => void;
10+
getInput: () => I;
11+
isEqual?: (a: I, b: I | undefined) => boolean;
1212
}
1313

14-
export function asyncProxy<V>(
15-
asyncInput: AsyncInput<V>,
16-
asyncOutput: (value: V, signal: AbortSignal) => Promise<void>,
17-
defaultValue: (prev: V | undefined) => V
18-
) {
19-
let inputsInProcess = $state.raw(0);
20-
let outputsInProcess = $state.raw(0);
21-
let proxyValue = $state.raw<V>();
22-
let inputController = new AbortController();
23-
let outputController = new AbortController();
24-
let ignoreProxyUpdate = true;
25-
const derivation = $derived.by(() => {
26-
const proxyVal = proxyValue;
27-
if (ignoreProxyUpdate) {
28-
ignoreProxyUpdate = false;
29-
asyncInput(true, inputController.signal);
30-
return proxyVal as V;
14+
export function createAsyncBinding<I, O>({
15+
initialOutput,
16+
getInput,
17+
setInput,
18+
toInput,
19+
toOutput,
20+
isEqual = Object.is,
21+
}: AsyncBindingOptions<I, O>) {
22+
let lastInputUpdate: I | undefined;
23+
const toInputAction = createAction({
24+
combinator: abortPrevious,
25+
execute: toInput,
26+
onSuccess(result: I) {
27+
lastInputUpdate = result;
28+
setInput(result);
29+
},
30+
});
31+
32+
let output = $state.raw(initialOutput);
33+
const toOutputAction = createAction({
34+
combinator: abortPrevious,
35+
execute: toOutput,
36+
onSuccess(result: O) {
37+
output = result;
38+
},
39+
});
40+
41+
$effect(() => {
42+
const input = getInput();
43+
if (isEqual(input, lastInputUpdate)) {
44+
return;
3145
}
32-
inputController.abort();
33-
inputController = new AbortController();
34-
outputController.abort();
35-
outputController = new AbortController();
3646
untrack(() => {
37-
inputsInProcess++;
47+
toInputAction.abort();
48+
toOutputAction.run(input);
3849
});
39-
asyncInput(false, inputController.signal)
40-
.then((v) => {
41-
ignoreProxyUpdate = true;
42-
proxyValue = v;
43-
})
44-
.finally(() => {
45-
untrack(() => {
46-
inputsInProcess--;
47-
});
48-
});
49-
return defaultValue(proxyVal);
5050
});
51+
5152
return {
52-
get value() {
53-
return derivation;
53+
get current() {
54+
return output;
5455
},
55-
set value(v) {
56-
if (Object.is(proxyValue, v)) {
57-
return;
58-
}
59-
outputController.abort();
60-
outputController = new AbortController();
61-
outputsInProcess++;
62-
asyncOutput(v, outputController.signal)
63-
.then(() => {
64-
ignoreProxyUpdate = true;
65-
})
66-
.finally(() => {
67-
outputsInProcess--;
68-
});
56+
set current(v) {
57+
toOutputAction.abort();
58+
toInputAction.run(v);
6959
},
7060
get inputProcessing() {
71-
return inputsInProcess > 0;
61+
return toInputAction.isProcessed;
7262
},
7363
get outputProcessing() {
74-
return outputsInProcess > 0;
64+
return toOutputAction.isProcessed;
7565
},
7666
};
7767
}

0 commit comments

Comments
 (0)