@shivaedev/form 0.13.1 → 0.14.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/CHANGELOG.md +15 -0
- package/README.md +7 -1
- package/dist/adapters/browserDraftStorage.d.ts.map +1 -1
- package/dist/adapters/browserDraftStorage.js +27 -2
- package/dist/adapters/browserDraftStorage.js.map +1 -1
- package/dist/adapters/draftCodec.d.ts +7 -0
- package/dist/adapters/draftCodec.d.ts.map +1 -0
- package/dist/adapters/draftCodec.js +40 -0
- package/dist/adapters/draftCodec.js.map +1 -0
- package/dist/adapters/persistence.d.ts +1 -1
- package/dist/adapters/persistence.d.ts.map +1 -1
- package/dist/adapters/persistence.js +91 -61
- package/dist/adapters/persistence.js.map +1 -1
- package/dist/adapters/react.d.ts.map +1 -1
- package/dist/adapters/react.js +3 -1
- package/dist/adapters/react.js.map +1 -1
- package/dist/draft.d.ts +2 -2
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +67 -45
- package/dist/draft.js.map +1 -1
- package/dist/draftValues.d.ts +13 -0
- package/dist/draftValues.d.ts.map +1 -0
- package/dist/draftValues.js +47 -0
- package/dist/draftValues.js.map +1 -0
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +17 -9
- package/dist/form.js.map +1 -1
- package/dist/persistence.d.ts +15 -11
- package/dist/persistence.d.ts.map +1 -1
- package/dist/persistence.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/browserDraftStorage.ts +30 -2
- package/src/adapters/draftCodec.ts +43 -0
- package/src/adapters/persistence.ts +97 -63
- package/src/adapters/react.ts +3 -1
- package/src/draft.ts +70 -62
- package/src/draftValues.ts +65 -0
- package/src/form.ts +12 -6
- package/src/persistence.ts +17 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Persist drafts through asynchronous native Effect storage operations, with optional observation across editors. Observed changes preserve local edits and adopt untouched fields.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Clear only the submitted draft revision after a successful save, keeping newer stored input from another editor. Serialize asynchronous writes and clears, and condition metadata writes on the owned revision.
|
|
12
|
+
|
|
13
|
+
### Breaking
|
|
14
|
+
|
|
15
|
+
- Custom `DraftStorage.removeItem` implementations must accept `(key, expected)` and atomically remove only when the stored serialized value equals `expected`. Custom `setItem(key, value, expected?)` implementations must also compare atomically when `expected` is supplied, so rebasing accepted fields cannot overwrite newer stored input. Replace unconditional mutations with conditional mutations at the storage owner; writes without `expected` still store new local input. The browser adapter handles this contract; providers can keep synchronous reads and writes or return native Effects.
|
|
16
|
+
- Lower-level `Persistence` integrations use `observe` and `revision` in place of `read`; mount the draft's atoms to own observation. Core form consumers mount `form.dirty` before reading restored input; asynchronous restoration arrives through the existing field refs. Form's React hooks attach this lifetime automatically.
|
|
17
|
+
|
|
3
18
|
## 0.13.1 - 2026-11-03
|
|
4
19
|
|
|
5
20
|
### Changed
|
package/README.md
CHANGED
|
@@ -220,7 +220,11 @@ function OrderEditor({ persistence }: { readonly persistence: Omit<PersistedDraf
|
|
|
220
220
|
|
|
221
221
|
Each stored record defaults to at most 256 KiB of UTF-8 JSON and seven days since its last write. Set `maxBytes` and `maxAgeMs` to finite positive limits to change those bounds. Oversized, expired, malformed or incompatible records are dropped when encountered. Persistence requires JSON-compatible encoded input; invalid input such as an empty required string or an unfinished numeric string still persists. Storage access, serialization and quota failures leave the form usable.
|
|
222
222
|
|
|
223
|
-
Edits and received values update the stored draft. Revert removes
|
|
223
|
+
Edits and received values update the stored draft. Revert conditionally removes the form's owned revision. A successful submit clears only the revision associated with its submitted input, including refreshes of its untouched fields; newer edits made locally or in another editor remain persisted. A failed submit keeps the draft.
|
|
224
|
+
|
|
225
|
+
A custom `DraftStorage` implements `getItem(key)`, `setItem(key, value, expected?)` and `removeItem(key, expected)`. Each operation returns its synchronous result or a native `Effect` with no remaining services; acquire application services when constructing the provider. `expected` is the exact serialized value previously read or written, containing an opaque revision. The storage owner must compare and mutate atomically: remove the key, or replace it when `setItem` supplies `expected`, only if its current value equals `expected`. A `setItem` without `expected` stores a new local edit; a conditional write rebases accepted or refreshed fields without replacing another editor's newer draft. For an Electron bridge, perform these comparisons in the process that owns the shared store. Writes and clears from one form run in order even when storage is asynchronous. Storage failures remain best-effort and do not fail the form's save.
|
|
226
|
+
|
|
227
|
+
Optional `subscribe(key, listener)` reports the current serialized value, or `null` when cleared, and returns an unsubscribe function. Report changes in storage order. Observation starts while the form's atoms are mounted and ends with their lifetime. Form's React hooks mount that lifetime automatically; direct atom consumers mount `form.dirty` or its validation/submission atoms. An untouched editor follows observed changes. Locally edited fields keep their current input while untouched fields adopt the observed draft. Receiving a storage event does not echo a write. The browser adapter observes other windows through storage events and serializes mutations with Web Locks where available; without Web Locks its compare and remove run synchronously.
|
|
224
228
|
|
|
225
229
|
Outside React, pass a `session` from `draftSession(id)` in `@shivaedev/form/draftSession.ts` and call its `drop()` when the application replaces that session. Core forms without a session owner use only the application's key. `DraftSessionContext` supplies the owner to React integrations.
|
|
226
230
|
|
|
@@ -352,6 +356,8 @@ The returned form exposes:
|
|
|
352
356
|
|
|
353
357
|
The lower-level `draft.ts` (`draft`), `refs.ts` (`fromRef`, `propertyRef`) and `statusOf.ts` (`statusOf`) modules hold the machinery the form composes; the examples use the form API instead.
|
|
354
358
|
|
|
359
|
+
For a custom lower-level `Persistence`, `observe(receive)` calls `receive` with decoded stored input (or `undefined` after removal) and returns a cleanup function. The receiver returns `true` only when it adopted the complete incoming input; `false` preserves the local revision when local fields remain. `revision()` captures a revision reader for a submission; `write(..., sameInput)` can keep that captured reader current when only untouched server fields refresh.
|
|
360
|
+
|
|
355
361
|
### Install and limits
|
|
356
362
|
|
|
357
363
|
```sh
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserDraftStorage.d.ts","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"browserDraftStorage.d.ts","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAOvD,eAAO,MAAM,mBAAmB,iDAuB7B,CAAC"}
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
import { Layer } from "effect";
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
2
|
import { DraftStorageProvider } from "#persistence.ts";
|
|
3
|
-
|
|
3
|
+
function mutate(key, run) {
|
|
4
|
+
const locks = globalThis.navigator?.locks;
|
|
5
|
+
return locks ? Effect.promise(() => locks.request(`@shivaedev/form/${key}`, run)) : run();
|
|
6
|
+
}
|
|
7
|
+
export const browserDraftStorage = Layer.succeed(DraftStorageProvider, () => ({
|
|
8
|
+
getItem: (key) => globalThis.localStorage.getItem(key),
|
|
9
|
+
removeItem: (key, expected) => mutate(key, () => {
|
|
10
|
+
if (globalThis.localStorage.getItem(key) === expected) {
|
|
11
|
+
globalThis.localStorage.removeItem(key);
|
|
12
|
+
}
|
|
13
|
+
}),
|
|
14
|
+
setItem: (key, value, expected) => mutate(key, () => {
|
|
15
|
+
if (expected === undefined || globalThis.localStorage.getItem(key) === expected) {
|
|
16
|
+
globalThis.localStorage.setItem(key, value);
|
|
17
|
+
}
|
|
18
|
+
}),
|
|
19
|
+
subscribe: (key, listener) => {
|
|
20
|
+
function changed(event) {
|
|
21
|
+
if (event.storageArea === globalThis.localStorage && (event.key === key || event.key === null)) {
|
|
22
|
+
listener(event.newValue);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
globalThis.addEventListener("storage", changed);
|
|
26
|
+
return () => globalThis.removeEventListener("storage", changed);
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
4
29
|
//# sourceMappingURL=browserDraftStorage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserDraftStorage.js","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"browserDraftStorage.js","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,SAAS,MAAM,CAAC,GAAW,EAAE,GAAe;IAC3C,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,EAAE,KAAK,CAAC;IAC1C,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,mBAAmB,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AAC3F,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7E,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC;IACtD,UAAU,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,CAC7B,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE;QAChB,IAAI,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ,EAAE,CAAC;YACvD,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACzC,CAAC;IACF,CAAC,CAAC;IACH,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CACjC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE;QAChB,IAAI,QAAQ,KAAK,SAAS,IAAI,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ,EAAE,CAAC;YACjF,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC;IACF,CAAC,CAAC;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE;QAC5B,SAAS,OAAO,CAAC,KAAmB;YACnC,IAAI,KAAK,CAAC,WAAW,KAAK,UAAU,CAAC,YAAY,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,EAAE,CAAC;gBAChG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC1B,CAAC;QACF,CAAC;QACD,UAAU,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,mBAAmB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACjE,CAAC;CACD,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { PersistedDraftConfig, StoredValues } from "#persistence.ts";
|
|
3
|
+
export declare function draftCodec<TSchema extends Schema.Constraint>(schema: TSchema, config: PersistedDraftConfig): {
|
|
4
|
+
read: (raw: string) => StoredValues<TSchema["Encoded"]>;
|
|
5
|
+
write: (baseline: TSchema["Encoded"], values: TSchema["Encoded"], edited: readonly string[]) => string;
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=draftCodec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftCodec.d.ts","sourceRoot":"","sources":["../../src/adapters/draftCodec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE1E,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB;gBAcvF,MAAM,KAAG,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;sBAmBzC,OAAO,CAAC,SAAS,CAAC,UAAU,OAAO,CAAC,SAAS,CAAC,UAAU,SAAS,MAAM,EAAE;EAM5F"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Schema, SchemaParser } from "effect";
|
|
2
|
+
export function draftCodec(schema, config) {
|
|
3
|
+
const StoredDraft = Schema.Struct({
|
|
4
|
+
baseline: Schema.Unknown,
|
|
5
|
+
edited: Schema.Array(Schema.String),
|
|
6
|
+
session: Schema.String,
|
|
7
|
+
updatedAt: Schema.Number,
|
|
8
|
+
values: Schema.Unknown,
|
|
9
|
+
version: Schema.Literal(1),
|
|
10
|
+
});
|
|
11
|
+
const valid = Schema.is(StoredDraft);
|
|
12
|
+
const decode = SchemaParser.decodeUnknownSync(Schema.toEncoded(schema), { disableChecks: true });
|
|
13
|
+
const maxAgeMs = config.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
14
|
+
const maxBytes = config.maxBytes ?? 256 * 1024;
|
|
15
|
+
const session = config.session?.id ?? "";
|
|
16
|
+
function read(raw) {
|
|
17
|
+
const value = new TextEncoder().encode(raw).byteLength <= maxBytes ? JSON.parse(raw) : undefined;
|
|
18
|
+
const now = config.now();
|
|
19
|
+
if (!valid(value)
|
|
20
|
+
|| value.session !== session
|
|
21
|
+
|| !Number.isFinite(maxAgeMs)
|
|
22
|
+
|| !Number.isFinite(maxBytes)
|
|
23
|
+
|| maxAgeMs <= 0
|
|
24
|
+
|| maxBytes <= 0
|
|
25
|
+
|| now - value.updatedAt > maxAgeMs
|
|
26
|
+
|| value.updatedAt > now) {
|
|
27
|
+
throw new Error("Invalid stored draft");
|
|
28
|
+
}
|
|
29
|
+
return { baseline: decode(value.baseline), edited: value.edited, values: decode(value.values) };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
read,
|
|
33
|
+
write: (baseline, values, edited) => {
|
|
34
|
+
const raw = JSON.stringify({ baseline, edited, revision: crypto.randomUUID(), session, updatedAt: config.now(), values, version: 1 });
|
|
35
|
+
read(raw);
|
|
36
|
+
return raw;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=draftCodec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftCodec.js","sourceRoot":"","sources":["../../src/adapters/draftCodec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAG9C,MAAM,UAAU,UAAU,CAAoC,MAAe,EAAE,MAA4B;IAC1G,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC,OAAO;QACxB,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;QACnC,OAAO,EAAE,MAAM,CAAC,MAAM;QACtB,SAAS,EAAE,MAAM,CAAC,MAAM;QACxB,MAAM,EAAE,MAAM,CAAC,OAAO;QACtB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;KAC1B,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,YAAY,CAAC,iBAAiB,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACjG,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,GAAG,GAAG,IAAI,CAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC;IACzC,SAAS,IAAI,CAAC,GAAW;QACxB,MAAM,KAAK,GAAY,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1G,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;QACzB,IACC,CAAC,KAAK,CAAC,KAAK,CAAC;eACV,KAAK,CAAC,OAAO,KAAK,OAAO;eACzB,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;eAC1B,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;eAC1B,QAAQ,IAAI,CAAC;eACb,QAAQ,IAAI,CAAC;eACb,GAAG,GAAG,KAAK,CAAC,SAAS,GAAG,QAAQ;eAChC,KAAK,CAAC,SAAS,GAAG,GAAG,EACvB,CAAC;YACF,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;IACjG,CAAC;IACD,OAAO;QACN,IAAI;QACJ,KAAK,EAAE,CAAC,QAA4B,EAAE,MAA0B,EAAE,MAAyB,EAAE,EAAE;YAC9F,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YACtI,IAAI,CAAC,GAAG,CAAC,CAAC;YACV,OAAO,GAAG,CAAC;QACZ,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { type Schema } from "effect";
|
|
2
2
|
import type { PersistedDraftConfig, Persistence } from "#persistence.ts";
|
|
3
3
|
export declare function persistence<TSchema extends Schema.Constraint>(schema: TSchema, config: PersistedDraftConfig): Persistence<TSchema["Encoded"]>;
|
|
4
4
|
//# sourceMappingURL=persistence.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAa,MAAM,QAAQ,CAAC;AAC9D,OAAO,KAAK,EAAkB,oBAAoB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYzF,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CA2G7I"}
|
|
@@ -1,83 +1,113 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Data, Effect, Semaphore } from "effect";
|
|
2
|
+
import { draftCodec } from "./draftCodec.js";
|
|
3
|
+
class DraftStorageUnavailable extends Data.TaggedError("DraftStorageUnavailable") {
|
|
4
|
+
}
|
|
5
|
+
function operation(run) {
|
|
6
|
+
return Effect.suspend(() => {
|
|
7
|
+
const value = run();
|
|
8
|
+
return Effect.isEffect(value) ? value : Effect.succeed(value);
|
|
9
|
+
}).pipe(Effect.catchCause((cause) => Effect.fail(new DraftStorageUnavailable({ cause }))));
|
|
10
|
+
}
|
|
2
11
|
export function persistence(schema, config) {
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
edited: Schema.Array(Schema.String),
|
|
7
|
-
session: Schema.String,
|
|
8
|
-
updatedAt: Schema.Number,
|
|
9
|
-
values: Schema.Unknown,
|
|
10
|
-
version: Schema.Literal(1),
|
|
11
|
-
});
|
|
12
|
-
const valid = Schema.is(StoredDraft);
|
|
13
|
-
const decode = SchemaParser.decodeUnknownSync(encoded, { disableChecks: true });
|
|
14
|
-
const maxAgeMs = config.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
15
|
-
const maxBytes = config.maxBytes ?? 256 * 1024;
|
|
16
|
-
function bytes(value) {
|
|
17
|
-
return new TextEncoder().encode(value).byteLength;
|
|
18
|
-
}
|
|
19
|
-
const storage = config.storage;
|
|
20
|
-
const session = config.session?.id ?? "";
|
|
12
|
+
const codec = draftCodec(schema, config);
|
|
13
|
+
const serial = Semaphore.makeUnsafe(1);
|
|
14
|
+
let revision = { value: undefined };
|
|
21
15
|
function active() {
|
|
22
16
|
return config.session?.active !== false;
|
|
23
17
|
}
|
|
24
|
-
function
|
|
18
|
+
function run(effect) {
|
|
19
|
+
return Effect.runCallback(serial.withPermit(effect).pipe(Effect.catchTag("DraftStorageUnavailable", () => Effect.void)));
|
|
20
|
+
}
|
|
21
|
+
function clear(expected = revision.value) {
|
|
22
|
+
if (expected !== undefined) {
|
|
23
|
+
run(operation(() => config.storage().removeItem(config.key, expected)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function read(raw) {
|
|
25
27
|
try {
|
|
26
|
-
return
|
|
28
|
+
return { values: raw === null ? undefined : codec.read(raw) };
|
|
27
29
|
}
|
|
28
30
|
catch {
|
|
29
|
-
|
|
31
|
+
if (raw !== null) {
|
|
32
|
+
clear(raw);
|
|
33
|
+
}
|
|
30
34
|
return undefined;
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
config.session?.track(config.key, () => run(Effect.gen(function* () {
|
|
38
|
+
const storage = yield* operation(config.storage);
|
|
39
|
+
const raw = yield* operation(() => storage.getItem(config.key));
|
|
40
|
+
if (raw !== null) {
|
|
41
|
+
yield* operation(() => codec.read(raw));
|
|
42
|
+
yield* operation(() => storage.removeItem(config.key, raw));
|
|
43
|
+
}
|
|
44
|
+
})));
|
|
37
45
|
return {
|
|
38
|
-
clear: () => {
|
|
46
|
+
clear: (expected = revision.value) => {
|
|
39
47
|
if (active()) {
|
|
40
|
-
clear();
|
|
48
|
+
clear(expected);
|
|
41
49
|
}
|
|
42
50
|
},
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
&&
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
&& maxAgeMs > 0
|
|
58
|
-
&& maxBytes > 0
|
|
59
|
-
&& now - value.updatedAt <= maxAgeMs
|
|
60
|
-
&& value.updatedAt <= now) {
|
|
61
|
-
return { baseline: decode(value.baseline), edited: value.edited, values: decode(value.values) };
|
|
51
|
+
observe: (receive) => {
|
|
52
|
+
let stopped = false;
|
|
53
|
+
let changes = 0;
|
|
54
|
+
function changed(raw, loaded = false) {
|
|
55
|
+
if (stopped || !active()) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!loaded && revision.value === (raw ?? undefined)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const parsed = read(raw);
|
|
62
|
+
if (parsed && receive(parsed.values)) {
|
|
63
|
+
revision = { value: raw ?? undefined };
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
let unsubscribe;
|
|
67
|
+
const stop = Effect.runCallback(Effect.gen(function* () {
|
|
68
|
+
if (!active()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const storage = yield* operation(config.storage);
|
|
72
|
+
unsubscribe = yield* operation(() => storage.subscribe?.(config.key, (incoming) => {
|
|
73
|
+
changes += 1;
|
|
74
|
+
changed(incoming);
|
|
75
|
+
}));
|
|
76
|
+
const before = changes;
|
|
77
|
+
const captured = revision;
|
|
78
|
+
const raw = yield* operation(() => storage.getItem(config.key));
|
|
79
|
+
if (before === changes && captured === revision) {
|
|
80
|
+
changed(raw, true);
|
|
81
|
+
}
|
|
82
|
+
}).pipe(Effect.catchTag("DraftStorageUnavailable", () => Effect.void)));
|
|
83
|
+
return () => {
|
|
84
|
+
stopped = true;
|
|
85
|
+
unsubscribe?.();
|
|
86
|
+
stop();
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
revision: () => {
|
|
90
|
+
const captured = revision;
|
|
91
|
+
return () => captured.value;
|
|
92
|
+
},
|
|
93
|
+
write: (baseline, values, edited, sameInput) => {
|
|
67
94
|
if (!active()) {
|
|
68
95
|
return;
|
|
69
96
|
}
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
try {
|
|
98
|
+
const expected = sameInput ? revision.value : undefined;
|
|
99
|
+
const raw = codec.write(baseline, values, edited);
|
|
100
|
+
if (sameInput) {
|
|
101
|
+
revision.value = raw;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
revision = { value: raw };
|
|
76
105
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
run(operation(() => (active() ? config.storage().setItem(config.key, raw, expected) : undefined)));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
clear();
|
|
110
|
+
}
|
|
81
111
|
},
|
|
82
112
|
};
|
|
83
113
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/adapters/react.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpF,eAAO,MAAM,mBAAmB,mDAAqD,CAAC;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAAC;AAK9G,MAAM,WAAW,KAAK,CAAC,OAAO,SAAS,MAAM,EAAE,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;CACvC;AACD,wBAAgB,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC,EAC/F,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EACxC,IAAI,EAAE,IAAI,GACR,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/adapters/react.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAElE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpF,eAAO,MAAM,mBAAmB,mDAAqD,CAAC;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAAC;AAK9G,MAAM,WAAW,KAAK,CAAC,OAAO,SAAS,MAAM,EAAE,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;CACvC;AACD,wBAAgB,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC,EAC/F,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EACxC,IAAI,EAAE,IAAI,GACR,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAQtB;AACD,MAAM,WAAW,MAAM,CAAC,CAAC,EAAE,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/C,QAAQ,CAAC,GAAG,EAAE,MAAM,IAAI,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC7B;AACD,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EACpE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,GACtC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,aAAa,GAAG,YAAY,GAAG,OAAO,CAAC,CAMvD;AACD,wBAAgB,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,GAAG,OAAO,CAEvH"}
|
package/dist/adapters/react.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useAtomRef, useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
1
|
+
import { useAtomMount, useAtomRef, useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
2
2
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
3
3
|
export const DraftSessionContext = createContext(undefined);
|
|
4
4
|
export function usePersistedDraft(config) {
|
|
@@ -6,6 +6,7 @@ export function usePersistedDraft(config) {
|
|
|
6
6
|
return config ? { ...config, session: session ?? config.session } : undefined;
|
|
7
7
|
}
|
|
8
8
|
export function useField(form, name) {
|
|
9
|
+
useAtomMount(form.dirty);
|
|
9
10
|
const value = useAtomRef(form.field(name));
|
|
10
11
|
const error = useAtomValue(form.error(name));
|
|
11
12
|
const onChange = useCallback((next) => form.change(name, next), [form, name]);
|
|
@@ -14,6 +15,7 @@ export function useField(form, name) {
|
|
|
14
15
|
return { choices, error, name, onBlur, onChange, value };
|
|
15
16
|
}
|
|
16
17
|
export function useSubmit(form) {
|
|
18
|
+
useAtomMount(form.dirty);
|
|
17
19
|
const result = useAtomValue(form.submit);
|
|
18
20
|
const set = useAtomSet(form.submit);
|
|
19
21
|
const run = useCallback(() => set(), [set]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/adapters/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/adapters/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAExF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAIxE,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAA2B,SAAS,CAAC,CAAC;AAGtF,MAAM,UAAU,iBAAiB,CAAC,MAAwC;IACzE,MAAM,OAAO,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAC;IAChD,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/E,CAAC;AASD,MAAM,UAAU,QAAQ,CACvB,IAAwC,EACxC,IAAU;IAEV,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,IAA4B,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACtG,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAChE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC1D,CAAC;AAMD,MAAM,UAAU,SAAS,CACxB,IAAwC;IAExC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;AACpD,CAAC;AACD,MAAM,UAAU,QAAQ,CAA8C,IAAwC;IAC7G,OAAO,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC"}
|
package/dist/draft.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as Atom from "effect/reactivity/Atom";
|
|
2
2
|
import * as AtomRef from "effect/reactivity/AtomRef";
|
|
3
|
+
import { type Entries } from "./draftValues.ts";
|
|
3
4
|
import type { Persistence } from "./persistence.ts";
|
|
4
|
-
type Entries = Readonly<Record<string, unknown>>;
|
|
5
5
|
export declare function draft<TValues extends Entries>(initial: TValues, persistence?: Persistence<TValues>): {
|
|
6
6
|
dirty: Atom.Atom<boolean>;
|
|
7
|
+
mounted: Atom.Atom<void>;
|
|
7
8
|
receive: (incoming: TValues) => void;
|
|
8
9
|
revert: () => void;
|
|
9
10
|
submission: () => {
|
|
@@ -12,5 +13,4 @@ export declare function draft<TValues extends Entries>(initial: TValues, persist
|
|
|
12
13
|
};
|
|
13
14
|
values: AtomRef.AtomRef<TValues>;
|
|
14
15
|
};
|
|
15
|
-
export {};
|
|
16
16
|
//# sourceMappingURL=draft.d.ts.map
|
package/dist/draft.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAC/C,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAgD,KAAK,OAAO,EAAoC,MAAM,kBAAkB,CAAC;AAChI,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,kBAAkB,CAAC;AAElE,wBAAgB,KAAK,CAAC,OAAO,SAAS,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC;;;wBAsE7E,OAAO,KAAG,IAAI;kBAUtB,IAAI;;QAWd,MAAM,cAAc,OAAO,KAAG,IAAI;QAYlC,MAAM;;;EAKT"}
|
package/dist/draft.js
CHANGED
|
@@ -1,45 +1,61 @@
|
|
|
1
|
-
import { Equal } from "effect";
|
|
2
1
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
2
|
import * as AtomRef from "effect/reactivity/AtomRef";
|
|
4
|
-
|
|
5
|
-
function untouched({ baseline, current, settled }, name) {
|
|
6
|
-
return Equal.equals(current[name], baseline[name]) || (Object.hasOwn(settled, name) && Equal.equals(current[name], settled[name]));
|
|
7
|
-
}
|
|
8
|
-
function changed(held) {
|
|
9
|
-
return Object.keys({ ...held.baseline, ...held.current }).some((name) => !untouched(held, name));
|
|
10
|
-
}
|
|
11
|
-
function merged(held, incoming) {
|
|
12
|
-
const result = {};
|
|
13
|
-
for (const name of Object.keys({ ...held.current, ...incoming })) {
|
|
14
|
-
const source = untouched(held, name) ? incoming : held.current;
|
|
15
|
-
if (Object.hasOwn(source, name)) {
|
|
16
|
-
result[name] = source[name];
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return result;
|
|
20
|
-
}
|
|
21
|
-
function matching(current, submitted) {
|
|
22
|
-
return Object.fromEntries(Object.keys({ ...current, ...submitted })
|
|
23
|
-
.filter((name) => Equal.equals(current[name], submitted[name]))
|
|
24
|
-
.map((name) => [name, submitted[name]]));
|
|
25
|
-
}
|
|
3
|
+
import { accepted, changed, changedFields, merged, mergeObserved, untouched } from "./draftValues.js";
|
|
26
4
|
export function draft(initial, persistence) {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
5
|
+
const none = {};
|
|
6
|
+
const state = AtomRef.make({ baseline: initial, current: initial, settled: none });
|
|
7
|
+
const local = new Set();
|
|
8
|
+
let previous = initial;
|
|
9
|
+
let receiving = false;
|
|
10
|
+
let refreshing = false;
|
|
11
|
+
function withoutWriting(run) {
|
|
12
|
+
receiving = true;
|
|
13
|
+
run();
|
|
14
|
+
receiving = false;
|
|
15
|
+
}
|
|
16
|
+
function restore(restored) {
|
|
17
|
+
const held = state.value;
|
|
18
|
+
const incoming = restored
|
|
19
|
+
? merged({
|
|
20
|
+
baseline: restored.baseline,
|
|
21
|
+
current: restored.values,
|
|
22
|
+
settled: Object.fromEntries(Object.entries(restored.values).filter(([name]) => !restored.edited.includes(name))),
|
|
23
|
+
}, held.baseline)
|
|
24
|
+
: held.baseline;
|
|
25
|
+
withoutWriting(() => state.update((value) => ({ ...value, current: mergeObserved(value.current, incoming, local) })));
|
|
26
|
+
return local.size === 0;
|
|
27
|
+
}
|
|
28
|
+
const mounted = Atom.readable((get) => {
|
|
29
|
+
if (persistence) {
|
|
30
|
+
get.addFinalizer(persistence.observe(restore));
|
|
31
|
+
}
|
|
37
32
|
});
|
|
33
|
+
function releaseCleanFields() {
|
|
34
|
+
for (const name of local) {
|
|
35
|
+
if (untouched(state.value, name)) {
|
|
36
|
+
local.delete(name);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function store(value, sameInput = refreshing) {
|
|
41
|
+
const edited = Object.keys({ ...value.baseline, ...value.current }).filter((name) => !untouched(value, name));
|
|
42
|
+
persistence?.write({ ...value.baseline, ...value.settled }, value.current, edited, sameInput);
|
|
43
|
+
}
|
|
38
44
|
if (persistence) {
|
|
39
45
|
state.subscribe((value) => {
|
|
46
|
+
const before = previous;
|
|
47
|
+
previous = value.current;
|
|
48
|
+
if (receiving) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!refreshing) {
|
|
52
|
+
for (const name of changedFields(before, value.current)) {
|
|
53
|
+
local.add(name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
releaseCleanFields();
|
|
40
57
|
if (changed(value)) {
|
|
41
|
-
|
|
42
|
-
persistence.write({ ...value.baseline, ...value.settled }, value.current, edited);
|
|
58
|
+
store(value);
|
|
43
59
|
}
|
|
44
60
|
else {
|
|
45
61
|
persistence.clear();
|
|
@@ -48,15 +64,20 @@ export function draft(initial, persistence) {
|
|
|
48
64
|
}
|
|
49
65
|
return {
|
|
50
66
|
dirty: Atom.readable((get) => {
|
|
67
|
+
get(mounted);
|
|
51
68
|
get.addFinalizer(state.subscribe((value) => get.setSelf(changed(value))));
|
|
52
69
|
return changed(state.value);
|
|
53
70
|
}),
|
|
71
|
+
mounted,
|
|
54
72
|
receive: (incoming) => {
|
|
73
|
+
refreshing = true;
|
|
55
74
|
state.update((held) => ({
|
|
56
75
|
baseline: incoming,
|
|
57
76
|
current: changed(held) ? merged(held, incoming) : incoming,
|
|
58
77
|
settled: none,
|
|
59
78
|
}));
|
|
79
|
+
refreshing = false;
|
|
80
|
+
releaseCleanFields();
|
|
60
81
|
},
|
|
61
82
|
revert: () => {
|
|
62
83
|
state.update((held) => ({
|
|
@@ -67,19 +88,20 @@ export function draft(initial, persistence) {
|
|
|
67
88
|
},
|
|
68
89
|
submission: () => {
|
|
69
90
|
const { baseline, current: submitted } = state.value;
|
|
91
|
+
const revision = persistence?.revision();
|
|
70
92
|
return {
|
|
71
93
|
accept: (incoming) => {
|
|
72
|
-
state.update((held) =>
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
withoutWriting(() => state.update((held) => accepted(held, baseline, submitted, incoming)));
|
|
95
|
+
releaseCleanFields();
|
|
96
|
+
if (changed(state.value)) {
|
|
97
|
+
store(state.value, true);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const expected = revision?.();
|
|
101
|
+
if (expected !== undefined) {
|
|
102
|
+
persistence?.clear(expected);
|
|
75
103
|
}
|
|
76
|
-
|
|
77
|
-
...held,
|
|
78
|
-
baseline: incoming ?? submitted,
|
|
79
|
-
current: incoming === undefined ? held.current : merged({ ...held, baseline: submitted, settled: none }, incoming),
|
|
80
|
-
settled: none,
|
|
81
|
-
};
|
|
82
|
-
});
|
|
104
|
+
}
|
|
83
105
|
},
|
|
84
106
|
values: submitted,
|
|
85
107
|
};
|