@shivaedev/form 0.11.0 → 0.12.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 +10 -0
- package/README.md +57 -21
- package/dist/adapters/browserDraftStorage.d.ts +4 -0
- package/dist/adapters/browserDraftStorage.d.ts.map +1 -0
- package/dist/adapters/browserDraftStorage.js +4 -0
- package/dist/adapters/browserDraftStorage.js.map +1 -0
- package/dist/adapters/draftPersistence.d.ts +7 -0
- package/dist/adapters/draftPersistence.d.ts.map +1 -0
- package/dist/adapters/draftPersistence.js +8 -0
- package/dist/adapters/draftPersistence.js.map +1 -0
- package/dist/adapters/persistence.d.ts +4 -0
- package/dist/adapters/persistence.d.ts.map +1 -0
- package/dist/adapters/persistence.js +84 -0
- package/dist/adapters/persistence.js.map +1 -0
- package/dist/{client → adapters}/react.d.ts +3 -6
- package/dist/adapters/react.d.ts.map +1 -0
- package/dist/{client → adapters}/react.js +1 -4
- package/dist/adapters/react.js.map +1 -0
- package/dist/fieldRejection.d.ts +22 -0
- package/dist/fieldRejection.d.ts.map +1 -0
- package/dist/fieldRejection.js +21 -0
- package/dist/fieldRejection.js.map +1 -0
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +11 -5
- package/dist/form.js.map +1 -1
- package/dist/persistence.d.ts +5 -2
- package/dist/persistence.d.ts.map +1 -1
- package/dist/persistence.js +2 -82
- package/dist/persistence.js.map +1 -1
- package/dist/shape.d.ts +5 -0
- package/dist/shape.d.ts.map +1 -1
- package/dist/validation.d.ts +7 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +11 -0
- package/dist/validation.js.map +1 -0
- package/package.json +7 -4
- package/src/adapters/browserDraftStorage.ts +4 -0
- package/src/adapters/draftPersistence.ts +8 -0
- package/src/adapters/persistence.ts +87 -0
- package/src/{client → adapters}/react.ts +5 -11
- package/src/fieldRejection.ts +47 -0
- package/src/form.ts +12 -18
- package/src/persistence.ts +3 -86
- package/src/shape.ts +6 -0
- package/src/validation.ts +18 -0
- package/dist/client/react.d.ts.map +0 -1
- package/dist/client/react.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 - 2026-11-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Form save rejection mappings pass nested paths to the submitter, so feedback appears on the rejected value. Automatic recognition uses the shared `@shivaedev/errors` decoder.
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- Import `FieldRejection`, `RejectField`, `FieldRejectionMapping`, `fieldRejectionOf` and `rejecting` from `@shivaedev/form/fieldRejection.ts` instead of `@shivaedev/react/fieldRejection.ts`. The React module is removed. `FieldRejection.field` now accepts a form path tuple as well as a top-level name; consumers that read it must handle both. Return `{ field: ["profile", "name"] as const, message }` from `rejectField` to attach a save rejection to a nested value.
|
|
12
|
+
|
|
3
13
|
## 0.11.0 - 2026-11-02
|
|
4
14
|
|
|
5
15
|
### Added
|
package/README.md
CHANGED
|
@@ -81,6 +81,17 @@ An empty name shows that message after blur or submission, including through `er
|
|
|
81
81
|
|
|
82
82
|
The handler has three arguments: decoded values, the submitter, and the submitted encoded snapshot. Use the third argument when coordinating work against what was submitted, rather than reading live fields after an asynchronous decode. The `react` create adapter uses that snapshot to preserve input typed while decoding.
|
|
83
83
|
|
|
84
|
+
### Validate without saving
|
|
85
|
+
|
|
86
|
+
Write the `validate` atom to reveal validation for every field and whole-form refinement without calling `onSubmit`:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
registry.set(order.validate, undefined);
|
|
90
|
+
const snapshot = await Effect.runPromise(AtomRegistry.getResult(registry, order.validate, { suspendOnWaiting: true }));
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The result is a `Snapshot<F>` with `encoded` input captured before decoding and `decoded` domain values. Validation uses the form's runtime, including asynchronous and service-based decoding. Invalid input fails with `Invalid`, carrying the same field and whole-form feedback as submit. Validation leaves the input, dirty baseline and persisted draft intact; it does not save or accept changes.
|
|
94
|
+
|
|
84
95
|
### In the UI: bind the same form
|
|
85
96
|
|
|
86
97
|
```tsx
|
|
@@ -90,7 +101,7 @@ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
|
90
101
|
import { useState } from "react";
|
|
91
102
|
import { createRoot } from "react-dom/client";
|
|
92
103
|
import { form } from "@shivaedev/form/form.ts";
|
|
93
|
-
import { useDirty, useField, useSubmit } from "@shivaedev/form/
|
|
104
|
+
import { useDirty, useField, useSubmit } from "@shivaedev/form/adapters/react.ts";
|
|
94
105
|
|
|
95
106
|
const fields = Schema.Struct({ name: Schema.NonEmptyString, quantity: Schema.NumberFromString });
|
|
96
107
|
|
|
@@ -170,26 +181,30 @@ By default, the form does not replace submitted encoded input with its decoded r
|
|
|
170
181
|
|
|
171
182
|
Opt in with `persistedDraft`. The application chooses a key for one logical editing session. Keep that key stable across server revisions so a refresh can find the same draft; choose a new key when the encoded field shape changes. Stored input includes its baseline and edited field names, so edited fields survive reload while untouched fields adopt the latest received values.
|
|
172
183
|
|
|
173
|
-
|
|
184
|
+
Acquire draft persistence during the application's Effect setup. `draftPersistence` reads `DraftStorageProvider` and `Clock.Clock` from the environment, returning the storage provider and a callback that reads that clock for every timestamp and expiry check. The browser adapter supplies `localStorage`; storage lookup stays lazy so a browser refusal remains best-effort. Supply a different provider with `Layer.succeed(DraftStorageProvider, () => storage)`.
|
|
174
185
|
|
|
175
186
|
```ts
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
import { Effect } from "effect";
|
|
188
|
+
import { browserDraftStorage } from "@shivaedev/form/adapters/browserDraftStorage.ts";
|
|
189
|
+
import { draftPersistence } from "@shivaedev/form/adapters/draftPersistence.ts";
|
|
190
|
+
|
|
191
|
+
const persistence = yield* draftPersistence.pipe(Effect.provide(browserDraftStorage));
|
|
178
192
|
const order = form(fields, {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
initialValues: { name: "Paper", quantity: "300" },
|
|
194
|
+
onSubmit: Effect.succeed,
|
|
195
|
+
persistedDraft: { key: "order:123", ...persistence },
|
|
182
196
|
});
|
|
183
197
|
```
|
|
184
198
|
|
|
185
|
-
For a React form,
|
|
199
|
+
For a React form, pass the acquired persistence into `usePersistedDraft` before creating the form. The hook attaches the current `SessionBoundary` draft owner:
|
|
186
200
|
|
|
187
201
|
```tsx
|
|
188
202
|
import { useState } from "react";
|
|
189
|
-
import { usePersistedDraft } from "@shivaedev/form/
|
|
203
|
+
import { usePersistedDraft } from "@shivaedev/form/adapters/react.ts";
|
|
204
|
+
import type { PersistedDraftConfig } from "@shivaedev/form/persistence.ts";
|
|
190
205
|
|
|
191
|
-
function OrderEditor({
|
|
192
|
-
const persistedDraft = usePersistedDraft({ key: "order:123",
|
|
206
|
+
function OrderEditor({ persistence }: { readonly persistence: Omit<PersistedDraftConfig, "key"> }) {
|
|
207
|
+
const persistedDraft = usePersistedDraft({ key: "order:123", ...persistence });
|
|
193
208
|
const [order] = useState(() =>
|
|
194
209
|
form(fields, {
|
|
195
210
|
initialValues: { name: "Paper", quantity: "300" },
|
|
@@ -201,7 +216,7 @@ function OrderEditor({ now }: { readonly now: () => number }) {
|
|
|
201
216
|
}
|
|
202
217
|
```
|
|
203
218
|
|
|
204
|
-
`OrderFields` is the application's field UI. Keep this component mounted for one record; remount it when its identity changes. `useEditor` and `useCreate` from `react` accept `persistedDraft: { key,
|
|
219
|
+
`OrderFields` is the application's field UI. Keep this component mounted for one record; remount it when its identity changes. `useEditor` and `useCreate` from `react` accept `persistedDraft: { key, ...persistence }` directly and attach the same owner. A committed `SessionBoundary` session replacement or sign-out drops its tracked drafts, including screens already unmounted, and disables writes from its old forms. A suspended replacement that is canceled preserves the visible session's stored draft and future persistence. Ordinary unmounts and React effect replay preserve stored drafts. A stored draft with a different session generation is discarded when read.
|
|
205
220
|
|
|
206
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.
|
|
207
222
|
|
|
@@ -232,6 +247,25 @@ For a nested value held in one top-level field, `errorAt(["answers", index, "tex
|
|
|
232
247
|
|
|
233
248
|
Whole-form Schema refinements are available through `errorAt([])` after submission. This root path selects only a whole-form message; field errors stay on their field paths. In React, render it with `useAtomValue(form.errorAt([]))`. A failed submission also keeps that message in `Invalid.formMessage`. Matching the refinement clears the feedback. Schema feedback is hidden while a new asynchronous decode is pending.
|
|
234
249
|
|
|
250
|
+
### Map save rejections to fields
|
|
251
|
+
|
|
252
|
+
`rejecting` from `@shivaedev/form/fieldRejection.ts` wraps a save with field feedback. It takes the form schema, the save function and a `rejectField` callback or `undefined`. With `undefined`, `fieldRejectionOf(schema)` uses `@shivaedev/errors` to recognize tagged string-field rejections and selects names in that schema. Other failures keep their original error.
|
|
253
|
+
|
|
254
|
+
For a service whose rejection names a nested value, return a path tuple from the callback:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { rejecting } from "@shivaedev/form/fieldRejection.ts";
|
|
258
|
+
|
|
259
|
+
const profile = form(Profile, {
|
|
260
|
+
initialValues: { profile: { email: "alice@example.com", name: "Taken" } },
|
|
261
|
+
onSubmit: rejecting(Profile, saveProfile, (error) =>
|
|
262
|
+
error.field === "name" ? { field: ["profile", "name"] as const, message: error.message } : undefined,
|
|
263
|
+
),
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`Profile` is the application's struct with a nested `profile` value, and `saveProfile` is its Effect save function. The mapped message appears at `errorAt(["profile", "name"])`; the email sibling stays clear. Returning `undefined` leaves the save failure unchanged.
|
|
268
|
+
|
|
235
269
|
### Errors on array elements
|
|
236
270
|
|
|
237
271
|
Pass a path tuple to `submitter.fail` to reject one element or a property inside it. Paths use indices in the submitted encoded snapshot. Optional `arrayKeys` selectors receive encoded elements of top-level array fields and return their stable, unique keys:
|
|
@@ -273,13 +307,14 @@ For a form made with these fields, `order.choices("visibility")` returns the enc
|
|
|
273
307
|
|
|
274
308
|
Import the module that defines a name; there is no root entry.
|
|
275
309
|
|
|
276
|
-
| Module
|
|
277
|
-
|
|
|
278
|
-
| `@shivaedev/form/form.ts`
|
|
279
|
-
| `@shivaedev/form/shape.ts`
|
|
280
|
-
| `@shivaedev/form/
|
|
281
|
-
| `@shivaedev/form/
|
|
282
|
-
| `@shivaedev/form/
|
|
310
|
+
| Module | Application-facing exports |
|
|
311
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
312
|
+
| `@shivaedev/form/form.ts` | `form(schema, config)` |
|
|
313
|
+
| `@shivaedev/form/shape.ts` | `Form`, `Submitter`, `FieldFailure`, `Invalid` and the field types they use |
|
|
314
|
+
| `@shivaedev/form/fieldRejection.ts` | `rejecting`, `fieldRejectionOf`, `FieldRejection`, `RejectField`, `FieldRejectionMapping` |
|
|
315
|
+
| `@shivaedev/form/adapters/react.ts` | `useField`, `useSubmit`, `useDirty`, `usePersistedDraft`, `DraftSessionContext` |
|
|
316
|
+
| `@shivaedev/form/draftSession.ts` | `draftSession(id)` |
|
|
317
|
+
| `@shivaedev/form/emptyAsNull.ts` | `emptyAsNull` |
|
|
283
318
|
|
|
284
319
|
`form` takes a `Schema.Struct` and this configuration:
|
|
285
320
|
|
|
@@ -292,7 +327,7 @@ Import the module that defines a name; there is no root entry.
|
|
|
292
327
|
| `checks` | Optional functions keyed by field, from encoded input to an Effect of `string \| undefined` |
|
|
293
328
|
| `arrayKeys` | Optional `ArrayKeys<F>` selectors keyed by top-level array field, from encoded element to stable `PropertyKey`; retains nested feedback across reordering |
|
|
294
329
|
| `debounce` | Optional Effect `Duration.Input` for checks |
|
|
295
|
-
| `persistedDraft` | Optional `{ key, storage, maxBytes?, maxAgeMs?, session? }`; React's `usePersistedDraft` supplies
|
|
330
|
+
| `persistedDraft` | Optional `{ key, now, storage, maxBytes?, maxAgeMs?, session? }`; React's `usePersistedDraft` supplies session ownership |
|
|
296
331
|
|
|
297
332
|
The returned form exposes:
|
|
298
333
|
|
|
@@ -304,11 +339,12 @@ The returned form exposes:
|
|
|
304
339
|
| `blur(name)` | Field blur operation, supplied as `useField(...).onBlur` |
|
|
305
340
|
| `error(name)` | Atom of `string \| undefined` |
|
|
306
341
|
| `errorAt([name, ...path])` | Atom of nested Schema feedback and handler rejections; visibility follows the top-level field |
|
|
307
|
-
| `errorAt([])` | Atom of whole-form Schema feedback, visible after submission
|
|
342
|
+
| `errorAt([])` | Atom of whole-form Schema feedback, visible after validation or submission |
|
|
308
343
|
| `choices(name)` | Encoded choices or `undefined` |
|
|
309
344
|
| `dirty` | Atom of `boolean` |
|
|
310
345
|
| `receive(values)` | Merge received encoded values |
|
|
311
346
|
| `revert()` | Restore the baseline |
|
|
347
|
+
| `validate` | `AtomResultFn<void, Snapshot<F>, ER \| Invalid>`; reveal feedback and decode without saving |
|
|
312
348
|
| `submit` | `AtomResultFn<void, A, E \| ER \| FieldFailure \| Invalid>` |
|
|
313
349
|
| `submitting` | Atom of `boolean` for submission waiting state |
|
|
314
350
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserDraftStorage.d.ts","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,eAAO,MAAM,mBAAmB,iDAAqE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserDraftStorage.js","sourceRoot":"","sources":["../../src/adapters/browserDraftStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { DraftStorageProvider } from "#persistence.ts";
|
|
3
|
+
export declare const draftPersistence: Effect.Effect<{
|
|
4
|
+
now: () => number;
|
|
5
|
+
storage: () => import("#persistence.ts").DraftStorage;
|
|
6
|
+
}, never, DraftStorageProvider>;
|
|
7
|
+
//# sourceMappingURL=draftPersistence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftPersistence.d.ts","sourceRoot":"","sources":["../../src/adapters/draftPersistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,eAAO,MAAM,gBAAgB;;;+BAI3B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
import { DraftStorageProvider } from "#persistence.ts";
|
|
3
|
+
export const draftPersistence = Effect.gen(function* () {
|
|
4
|
+
const storage = yield* DraftStorageProvider;
|
|
5
|
+
const clock = yield* Clock.Clock;
|
|
6
|
+
return { now: () => clock.currentTimeMillisUnsafe(), storage };
|
|
7
|
+
});
|
|
8
|
+
//# sourceMappingURL=draftPersistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftPersistence.js","sourceRoot":"","sources":["../../src/adapters/draftPersistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACnD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,oBAAoB,CAAC;IAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IACjC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,uBAAuB,EAAE,EAAE,OAAO,EAAE,CAAC;AAChE,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { PersistedDraftConfig, Persistence } from "#persistence.ts";
|
|
3
|
+
export declare function persistence<TSchema extends Schema.Constraint>(schema: TSchema, config: PersistedDraftConfig): Persistence<TSchema["Encoded"]>;
|
|
4
|
+
//# sourceMappingURL=persistence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEzE,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAmF7I"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Schema, SchemaParser } from "effect";
|
|
2
|
+
export function persistence(schema, config) {
|
|
3
|
+
const encoded = Schema.toEncoded(schema);
|
|
4
|
+
const StoredDraft = Schema.Struct({
|
|
5
|
+
baseline: Schema.Unknown,
|
|
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 ?? "";
|
|
21
|
+
function active() {
|
|
22
|
+
return config.session?.active !== false;
|
|
23
|
+
}
|
|
24
|
+
function attempt(run, failed) {
|
|
25
|
+
try {
|
|
26
|
+
return run();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
failed?.();
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function clear() {
|
|
34
|
+
attempt(() => storage().removeItem(config.key));
|
|
35
|
+
}
|
|
36
|
+
config.session?.track(config.key, clear);
|
|
37
|
+
return {
|
|
38
|
+
clear: () => {
|
|
39
|
+
if (active()) {
|
|
40
|
+
clear();
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
read: () => attempt(() => {
|
|
44
|
+
if (!active()) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const raw = storage().getItem(config.key);
|
|
48
|
+
if (raw === null) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const value = bytes(raw) <= maxBytes ? JSON.parse(raw) : undefined;
|
|
52
|
+
const now = config.now();
|
|
53
|
+
if (valid(value)
|
|
54
|
+
&& value.session === session
|
|
55
|
+
&& Number.isFinite(maxAgeMs)
|
|
56
|
+
&& Number.isFinite(maxBytes)
|
|
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) };
|
|
62
|
+
}
|
|
63
|
+
clear();
|
|
64
|
+
return undefined;
|
|
65
|
+
}, clear),
|
|
66
|
+
write: (baseline, values, edited) => {
|
|
67
|
+
if (!active()) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
attempt(() => {
|
|
71
|
+
const raw = JSON.stringify({ baseline, edited, session, updatedAt: config.now(), values, version: 1 });
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
if (!(maxAgeMs > 0 && Number.isFinite(maxAgeMs) && Number.isFinite(maxBytes) && bytes(raw) <= maxBytes && valid(parsed))) {
|
|
74
|
+
clear();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
decode(parsed.baseline);
|
|
78
|
+
decode(parsed.values);
|
|
79
|
+
storage().setItem(config.key, raw);
|
|
80
|
+
}, clear);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/adapters/persistence.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
2
2
|
import type { DraftSession } from "#draftSession.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { PersistedDraftConfig } from "#persistence.ts";
|
|
4
4
|
import type { Encoded, FieldFailure, Fields, Form, Invalid, Name } from "#shape.ts";
|
|
5
5
|
export declare const DraftSessionContext: import("react").Context<DraftSession | undefined>;
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
export declare function usePersistedDraft(config: BrowserDraftConfig): PersistedDraftConfig;
|
|
10
|
-
export declare function usePersistedDraft(config: BrowserDraftConfig | undefined): PersistedDraftConfig | undefined;
|
|
6
|
+
export declare function usePersistedDraft(config: PersistedDraftConfig): PersistedDraftConfig;
|
|
7
|
+
export declare function usePersistedDraft(config: PersistedDraftConfig | undefined): PersistedDraftConfig | undefined;
|
|
11
8
|
export interface Field<TFields extends Fields, TKey extends Name<TFields>> {
|
|
12
9
|
readonly choices: readonly Encoded<TFields>[TKey][] | undefined;
|
|
13
10
|
readonly error: string | undefined;
|
|
@@ -0,0 +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,CAOtB;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,CAKvD;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"}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { useAtomRef, useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
2
2
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
3
3
|
export const DraftSessionContext = createContext(undefined);
|
|
4
|
-
function browserStorage() {
|
|
5
|
-
return globalThis.localStorage;
|
|
6
|
-
}
|
|
7
4
|
export function usePersistedDraft(config) {
|
|
8
5
|
const session = useContext(DraftSessionContext);
|
|
9
|
-
return config ? { ...config, session: session ?? config.session
|
|
6
|
+
return config ? { ...config, session: session ?? config.session } : undefined;
|
|
10
7
|
}
|
|
11
8
|
export function useField(form, name) {
|
|
12
9
|
const value = useAtomRef(form.field(name));
|
|
@@ -0,0 +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;AAE1E,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,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,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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Effect, type Schema } from "effect";
|
|
2
|
+
import type { FieldFailure, Fields, Name, Path, Submitter } from "#shape.ts";
|
|
3
|
+
export interface FieldRejection<TName extends string> {
|
|
4
|
+
readonly field: TName | Path<TName>;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
export type RejectField<TFields extends Fields, TError> = (error: TError) => FieldRejection<Name<TFields>> | undefined;
|
|
8
|
+
type Unmatched<TFields extends Fields, TError> = TError extends {
|
|
9
|
+
readonly _tag: string;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
} ? "field" extends keyof TError ? Exclude<TError["field"], Name<TFields> | undefined> : never : never;
|
|
12
|
+
export type FieldRejectionMapping<TFields extends Fields, TError> = {
|
|
13
|
+
readonly rejectField?: RejectField<TFields, TError>;
|
|
14
|
+
} & ([
|
|
15
|
+
Unmatched<TFields, TError>
|
|
16
|
+
] extends [never] ? unknown : {
|
|
17
|
+
readonly rejectField: RejectField<TFields, TError>;
|
|
18
|
+
});
|
|
19
|
+
export declare function fieldRejectionOf<TFields extends Fields>(schema: Schema.Struct<TFields>): (error: unknown) => FieldRejection<Name<TFields>> | undefined;
|
|
20
|
+
export declare function rejecting<TFields extends Fields, TValues, TResult, TError, TRequirements>(schema: Schema.Struct<TFields>, save: (values: TValues) => Effect.Effect<TResult, TError, TRequirements>, reject: RejectField<TFields, TError> | undefined): (values: TValues, submitter: Submitter<Name<TFields>>) => Effect.Effect<TResult, TError | FieldFailure, TRequirements>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=fieldRejection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldRejection.d.ts","sourceRoot":"","sources":["../src/fieldRejection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE7E,MAAM,WAAW,cAAc,CAAC,KAAK,SAAS,MAAM;IACnD,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,MAAM,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;AAEvH,KAAK,SAAS,CAAC,OAAO,SAAS,MAAM,EAAE,MAAM,IAAI,MAAM,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChH,OAAO,SAAS,MAAM,MAAM,GAC3B,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,GACnD,KAAK,GACN,KAAK,CAAC;AAET,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,EAAE,MAAM,IAAI;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC;IAC9H,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,SAAS,CAAC,KAAK,CAAC,GACd,OAAO,GACP;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,CAAC;AAE3D,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WACvE,OAAO,KAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CASlE;AAED,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EACxF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAC9B,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EACxE,MAAM,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,SAAS,YAGhC,OAAO,aAAa,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,aAAa,CAAC,CAK3H"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { rejectedField } from "@shivaedev/errors/rejectedField.ts";
|
|
3
|
+
export function fieldRejectionOf(schema) {
|
|
4
|
+
return (error) => {
|
|
5
|
+
function isName(name) {
|
|
6
|
+
return Object.hasOwn(schema.fields, name);
|
|
7
|
+
}
|
|
8
|
+
return Option.match(rejectedField(error), {
|
|
9
|
+
onNone: () => undefined,
|
|
10
|
+
onSome: ({ field, message }) => (isName(field) ? { field, message } : undefined),
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function rejecting(schema, save, reject) {
|
|
15
|
+
const rejection = reject ?? fieldRejectionOf(schema);
|
|
16
|
+
return (values, submitter) => Effect.catch(save(values), (error) => {
|
|
17
|
+
const found = rejection(error);
|
|
18
|
+
return found === undefined ? Effect.fail(error) : submitter.fail(found.field, found.message);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=fieldRejection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldRejection.js","sourceRoot":"","sources":["../src/fieldRejection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAe,MAAM,QAAQ,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAsBnE,MAAM,UAAU,gBAAgB,CAAyB,MAA8B;IACtF,OAAO,CAAC,KAAc,EAA6C,EAAE;QACpE,SAAS,MAAM,CAAC,IAAY;YAC3B,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE;YACzC,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS;YACvB,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;SAChF,CAAC,CAAC;IACJ,CAAC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CACxB,MAA8B,EAC9B,IAAwE,EACxE,MAAgD;IAEhD,MAAM,SAAS,GAAG,MAAM,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACrD,OAAO,CAAC,MAAe,EAAE,SAAmC,EAAgE,EAAE,CAC7H,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAA+C,EAAE;QACjF,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QAC/B,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/form.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,MAAM,EAAkC,MAAM,QAAQ,CAAC;AAUvF,OAAO,
|
|
1
|
+
{"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,MAAM,EAAkC,MAAM,QAAQ,CAAC;AAUvF,OAAO,EAAE,KAAK,MAAM,EAA4C,KAAK,MAAM,EAAE,KAAK,IAAI,EAA4C,MAAM,YAAY,CAAC;AAcrJ,wBAAgB,IAAI,CAAC,OAAO,SAAS,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,KAAK,EAAE,aAAa,GAAG,KAAK,EACpG,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,CAAC,GAC/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,CAqG9C"}
|
package/dist/form.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Effect, Layer, Result, Schema, SchemaParser } from "effect";
|
|
2
2
|
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
3
3
|
import * as Atom from "effect/reactivity/Atom";
|
|
4
|
+
import { persistence } from "#adapters/persistence.ts";
|
|
4
5
|
import { checkFeedback } from "./checkFeedback.js";
|
|
5
6
|
import { draft } from "./draft.js";
|
|
6
|
-
import { literalChoices, messageAt,
|
|
7
|
+
import { literalChoices, messageAt, schemaFeedback } from "./messages.js";
|
|
7
8
|
import { pathMessageAt } from "./paths.js";
|
|
8
|
-
import { persistence } from "./persistence.js";
|
|
9
9
|
import { fromRef, propertyRef } from "./refs.js";
|
|
10
|
-
import { FieldFailure
|
|
10
|
+
import { FieldFailure } from "./shape.js";
|
|
11
11
|
import { statusOf } from "./statusOf.js";
|
|
12
12
|
import { trackRejections } from "./trackRejections.js";
|
|
13
|
+
import { validation } from "./validation.js";
|
|
13
14
|
const emptyRuntime = Atom.runtime(Layer.empty);
|
|
14
15
|
function configuredRuntime(config) {
|
|
15
16
|
return config.runtime ?? emptyRuntime;
|
|
@@ -19,7 +20,7 @@ export function form(schema, config) {
|
|
|
19
20
|
const runtime = configuredRuntime(config);
|
|
20
21
|
const debounce = config.debounce ?? "300 millis";
|
|
21
22
|
const checks = new Map(Object.entries(config.checks ?? {}));
|
|
22
|
-
const decode =
|
|
23
|
+
const { decode, snapshot } = validation(schema, config.requiredMessages);
|
|
23
24
|
const editing = draft(initialValues, config.persistedDraft ? persistence(schema, config.persistedDraft) : undefined);
|
|
24
25
|
const { values } = editing;
|
|
25
26
|
const status = statusOf();
|
|
@@ -50,11 +51,15 @@ export function form(schema, config) {
|
|
|
50
51
|
const submitter = {
|
|
51
52
|
fail: (path, message) => Effect.fail(new FieldFailure(typeof path === "string" ? { message, path } : { message, path: path[0], segments: path })),
|
|
52
53
|
};
|
|
54
|
+
const validate = runtime.fn()(() => {
|
|
55
|
+
status.attempt();
|
|
56
|
+
return snapshot(values.value);
|
|
57
|
+
});
|
|
53
58
|
const submit = runtime.fn()(() => Effect.gen(function* () {
|
|
54
59
|
status.attempt();
|
|
55
60
|
const submission = editing.submission();
|
|
56
61
|
const submitted = submission.values;
|
|
57
|
-
const value = yield*
|
|
62
|
+
const { decoded: value } = yield* snapshot(submitted);
|
|
58
63
|
return yield* onSubmit(value, submitter, submitted).pipe(Effect.tapError(noted(submitted)), Effect.tap((saved) => Effect.sync(() => submission.accept(config.savedValues?.(saved)))));
|
|
59
64
|
}));
|
|
60
65
|
return {
|
|
@@ -83,6 +88,7 @@ export function form(schema, config) {
|
|
|
83
88
|
revert: editing.revert,
|
|
84
89
|
submit,
|
|
85
90
|
submitting: Atom.map(submit, AsyncResult.isWaiting),
|
|
91
|
+
validate,
|
|
86
92
|
values,
|
|
87
93
|
};
|
|
88
94
|
}
|
package/dist/form.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.js","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAoB,YAAY,EAAE,MAAM,QAAQ,CAAC;AACvF,OAAO,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"form.js","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"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"}
|
package/dist/persistence.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context } from "effect";
|
|
2
2
|
import type { DraftSession } from "./draftSession.ts";
|
|
3
3
|
export interface DraftStorage {
|
|
4
4
|
readonly getItem: (key: string) => string | null;
|
|
5
5
|
readonly removeItem: (key: string) => void;
|
|
6
6
|
readonly setItem: (key: string, value: string) => void;
|
|
7
7
|
}
|
|
8
|
+
declare const DraftStorageProvider_base: Context.ServiceClass<DraftStorageProvider, "@shivaedev/form/DraftStorageProvider", () => DraftStorage>;
|
|
9
|
+
export declare class DraftStorageProvider extends DraftStorageProvider_base {
|
|
10
|
+
}
|
|
8
11
|
export interface PersistedDraftConfig {
|
|
9
12
|
readonly key: string;
|
|
10
13
|
readonly maxAgeMs?: number;
|
|
@@ -22,5 +25,5 @@ export interface Persistence<TValues> {
|
|
|
22
25
|
} | undefined;
|
|
23
26
|
readonly write: (baseline: TValues, values: TValues, edited: readonly string[]) => void;
|
|
24
27
|
}
|
|
25
|
-
export
|
|
28
|
+
export {};
|
|
26
29
|
//# sourceMappingURL=persistence.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;kIAEqF,YAAY;AAAlG,qBAAa,oBAAqB,SAAQ,yBAAmG;CAAG;AAEhJ,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,YAAY,CAAC;CACrC;AAED,MAAM,WAAW,WAAW,CAAC,OAAO;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM;QAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IAC9H,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;CACxF"}
|
package/dist/persistence.js
CHANGED
|
@@ -1,84 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export
|
|
3
|
-
const encoded = Schema.toEncoded(schema);
|
|
4
|
-
const StoredDraft = Schema.Struct({
|
|
5
|
-
baseline: Schema.Unknown,
|
|
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 ?? "";
|
|
21
|
-
function active() {
|
|
22
|
-
return config.session?.active !== false;
|
|
23
|
-
}
|
|
24
|
-
function attempt(run, failed) {
|
|
25
|
-
try {
|
|
26
|
-
return run();
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
failed?.();
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function clear() {
|
|
34
|
-
attempt(() => storage().removeItem(config.key));
|
|
35
|
-
}
|
|
36
|
-
config.session?.track(config.key, clear);
|
|
37
|
-
return {
|
|
38
|
-
clear: () => {
|
|
39
|
-
if (active()) {
|
|
40
|
-
clear();
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
read: () => attempt(() => {
|
|
44
|
-
if (!active()) {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
const raw = storage().getItem(config.key);
|
|
48
|
-
if (raw === null) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
const value = bytes(raw) <= maxBytes ? JSON.parse(raw) : undefined;
|
|
52
|
-
const now = config.now();
|
|
53
|
-
if (valid(value)
|
|
54
|
-
&& value.session === session
|
|
55
|
-
&& Number.isFinite(maxAgeMs)
|
|
56
|
-
&& Number.isFinite(maxBytes)
|
|
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) };
|
|
62
|
-
}
|
|
63
|
-
clear();
|
|
64
|
-
return undefined;
|
|
65
|
-
}, clear),
|
|
66
|
-
write: (baseline, values, edited) => {
|
|
67
|
-
if (!active()) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
attempt(() => {
|
|
71
|
-
const raw = JSON.stringify({ baseline, edited, session, updatedAt: config.now(), values, version: 1 });
|
|
72
|
-
const parsed = JSON.parse(raw);
|
|
73
|
-
if (!(maxAgeMs > 0 && Number.isFinite(maxAgeMs) && Number.isFinite(maxBytes) && bytes(raw) <= maxBytes && valid(parsed))) {
|
|
74
|
-
clear();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
decode(parsed.baseline);
|
|
78
|
-
decode(parsed.values);
|
|
79
|
-
storage().setItem(config.key, raw);
|
|
80
|
-
}, clear);
|
|
81
|
-
},
|
|
82
|
-
};
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
export class DraftStorageProvider extends Context.Service()("@shivaedev/form/DraftStorageProvider") {
|
|
83
3
|
}
|
|
84
4
|
//# sourceMappingURL=persistence.js.map
|
package/dist/persistence.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AASjC,MAAM,OAAO,oBAAqB,SAAQ,OAAO,CAAC,OAAO,EAA4C,CAAC,sCAAsC,CAAC;CAAG"}
|
package/dist/shape.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export type Config<TFields extends Fields, A, E, R, TRuntimeError> = {
|
|
|
53
53
|
} : {
|
|
54
54
|
readonly runtime: Atom.AtomRuntime<Services<TFields, R>, TRuntimeError>;
|
|
55
55
|
});
|
|
56
|
+
export interface Snapshot<TFields extends Fields> {
|
|
57
|
+
readonly decoded: Decoded<TFields>;
|
|
58
|
+
readonly encoded: Encoded<TFields>;
|
|
59
|
+
}
|
|
56
60
|
export interface Form<TFields extends Fields, A, E, TRuntimeError> {
|
|
57
61
|
readonly blur: (name: Name<TFields>) => void;
|
|
58
62
|
readonly change: <TKey extends Name<TFields>>(name: TKey, value: Encoded<TFields>[TKey]) => void;
|
|
@@ -65,6 +69,7 @@ export interface Form<TFields extends Fields, A, E, TRuntimeError> {
|
|
|
65
69
|
readonly revert: () => void;
|
|
66
70
|
readonly submit: Atom.AtomResultFn<void, A, E | TRuntimeError | FieldFailure | Invalid>;
|
|
67
71
|
readonly submitting: Atom.Atom<boolean>;
|
|
72
|
+
readonly validate: Atom.AtomResultFn<void, Snapshot<TFields>, TRuntimeError | Invalid>;
|
|
68
73
|
readonly values: AtomRef.AtomRef<Encoded<TFields>>;
|
|
69
74
|
}
|
|
70
75
|
export type Holder = AtomRef.ReadonlyRef<Readonly<Record<string, unknown>>>;
|
package/dist/shape.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shape.d.ts","sourceRoot":"","sources":["../src/shape.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"shape.d.ts","sourceRoot":"","sources":["../src/shape.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Effect, type Schema } from "effect";
|
|
2
|
+
import { type Encoded, type Fields, Invalid, type Name, type Snapshot } from "./shape.ts";
|
|
3
|
+
export declare function validation<TFields extends Fields>(schema: Schema.Struct<TFields>, requiredMessages: Readonly<Partial<Record<Name<TFields>, string>>> | undefined): {
|
|
4
|
+
decode: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Effect.Effect<Schema.Struct.View<TFields, "Type", Schema.Struct.TypeOptionalKeys<TFields>, Schema.Struct.TypeMutableKeys<TFields>>, import("effect/SchemaIssue").Issue, Schema.Struct.DecodingServices<TFields>>;
|
|
5
|
+
snapshot: (encoded: Encoded<TFields>) => Effect.Effect<Snapshot<TFields>, Invalid, Schema.Struct.DecodingServices<TFields>>;
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAE3D,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE1F,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAChD,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAC9B,gBAAgB,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS;;wBAKzD,OAAO,CAAC,OAAO,CAAC;EAMrC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect, SchemaParser } from "effect";
|
|
2
|
+
import { messageAtPath, messagesByField } from "./messages.js";
|
|
3
|
+
import { Invalid } from "./shape.js";
|
|
4
|
+
export function validation(schema, requiredMessages) {
|
|
5
|
+
const decode = SchemaParser.decodeUnknownEffect(schema, { errors: "all" });
|
|
6
|
+
return {
|
|
7
|
+
decode,
|
|
8
|
+
snapshot: (encoded) => decode(encoded).pipe(Effect.map((decoded) => ({ decoded, encoded })), Effect.mapError((issue) => new Invalid({ formMessage: messageAtPath(issue, []), messages: messagesByField(issue, requiredMessages) }))),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAe,YAAY,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAA6B,OAAO,EAA4B,MAAM,YAAY,CAAC;AAE1F,MAAM,UAAU,UAAU,CACzB,MAA8B,EAC9B,gBAA8E;IAE9E,MAAM,MAAM,GAAG,YAAY,CAAC,mBAAmB,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3E,OAAO;QACN,MAAM;QACN,QAAQ,EAAE,CAAC,OAAyB,EAAE,EAAE,CACvC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CACnB,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAqB,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,EAClE,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,WAAW,EAAE,aAAa,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC,CAAC,CACtI;KACF,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/form",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Schema-derived Effect atom forms with optional React hooks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -42,12 +42,15 @@
|
|
|
42
42
|
"CHANGELOG.md",
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@shivaedev/errors": "0.5.0"
|
|
47
|
+
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"@effect/atom-react": "4.0.6",
|
|
47
50
|
"@effect/vitest": "4.0.6",
|
|
48
|
-
"@shivaedev/quality": "0.
|
|
49
|
-
"@shivaedev/test": "0.
|
|
50
|
-
"@shivaedev/test-story": "0.
|
|
51
|
+
"@shivaedev/quality": "0.56.0",
|
|
52
|
+
"@shivaedev/test": "0.13.0",
|
|
53
|
+
"@shivaedev/test-story": "0.34.0",
|
|
51
54
|
"@types/node": "24.19.2",
|
|
52
55
|
"@types/react": "19.3.0",
|
|
53
56
|
"effect": "4.0.6",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
import { DraftStorageProvider } from "#persistence.ts";
|
|
3
|
+
|
|
4
|
+
export const draftPersistence = Effect.gen(function* () {
|
|
5
|
+
const storage = yield* DraftStorageProvider;
|
|
6
|
+
const clock = yield* Clock.Clock;
|
|
7
|
+
return { now: () => clock.currentTimeMillisUnsafe(), storage };
|
|
8
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Schema, SchemaParser } from "effect";
|
|
2
|
+
import type { PersistedDraftConfig, Persistence } from "#persistence.ts";
|
|
3
|
+
|
|
4
|
+
export function persistence<TSchema extends Schema.Constraint>(schema: TSchema, config: PersistedDraftConfig): Persistence<TSchema["Encoded"]> {
|
|
5
|
+
const encoded = Schema.toEncoded(schema);
|
|
6
|
+
const StoredDraft = Schema.Struct({
|
|
7
|
+
baseline: Schema.Unknown,
|
|
8
|
+
edited: Schema.Array(Schema.String),
|
|
9
|
+
session: Schema.String,
|
|
10
|
+
updatedAt: Schema.Number,
|
|
11
|
+
values: Schema.Unknown,
|
|
12
|
+
version: Schema.Literal(1),
|
|
13
|
+
});
|
|
14
|
+
const valid = Schema.is(StoredDraft);
|
|
15
|
+
const decode = SchemaParser.decodeUnknownSync(encoded, { disableChecks: true });
|
|
16
|
+
const maxAgeMs = config.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
const maxBytes = config.maxBytes ?? 256 * 1024;
|
|
18
|
+
function bytes(value: string) {
|
|
19
|
+
return new TextEncoder().encode(value).byteLength;
|
|
20
|
+
}
|
|
21
|
+
const storage = config.storage;
|
|
22
|
+
const session = config.session?.id ?? "";
|
|
23
|
+
function active() {
|
|
24
|
+
return config.session?.active !== false;
|
|
25
|
+
}
|
|
26
|
+
function attempt<A>(run: () => A, failed?: () => void): A | undefined {
|
|
27
|
+
try {
|
|
28
|
+
return run();
|
|
29
|
+
} catch {
|
|
30
|
+
failed?.();
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function clear() {
|
|
35
|
+
attempt(() => storage().removeItem(config.key));
|
|
36
|
+
}
|
|
37
|
+
config.session?.track(config.key, clear);
|
|
38
|
+
return {
|
|
39
|
+
clear: () => {
|
|
40
|
+
if (active()) {
|
|
41
|
+
clear();
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
read: () =>
|
|
45
|
+
attempt(() => {
|
|
46
|
+
if (!active()) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const raw = storage().getItem(config.key);
|
|
50
|
+
if (raw === null) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const value: unknown = bytes(raw) <= maxBytes ? JSON.parse(raw) : undefined;
|
|
54
|
+
const now = config.now();
|
|
55
|
+
if (
|
|
56
|
+
valid(value)
|
|
57
|
+
&& value.session === session
|
|
58
|
+
&& Number.isFinite(maxAgeMs)
|
|
59
|
+
&& Number.isFinite(maxBytes)
|
|
60
|
+
&& maxAgeMs > 0
|
|
61
|
+
&& maxBytes > 0
|
|
62
|
+
&& now - value.updatedAt <= maxAgeMs
|
|
63
|
+
&& value.updatedAt <= now
|
|
64
|
+
) {
|
|
65
|
+
return { baseline: decode(value.baseline), edited: value.edited, values: decode(value.values) };
|
|
66
|
+
}
|
|
67
|
+
clear();
|
|
68
|
+
return undefined;
|
|
69
|
+
}, clear),
|
|
70
|
+
write: (baseline, values, edited) => {
|
|
71
|
+
if (!active()) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
attempt(() => {
|
|
75
|
+
const raw = JSON.stringify({ baseline, edited, session, updatedAt: config.now(), values, version: 1 });
|
|
76
|
+
const parsed: unknown = JSON.parse(raw);
|
|
77
|
+
if (!(maxAgeMs > 0 && Number.isFinite(maxAgeMs) && Number.isFinite(maxBytes) && bytes(raw) <= maxBytes && valid(parsed))) {
|
|
78
|
+
clear();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
decode(parsed.baseline);
|
|
82
|
+
decode(parsed.values);
|
|
83
|
+
storage().setItem(config.key, raw);
|
|
84
|
+
}, clear);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -2,20 +2,14 @@ import { useAtomRef, useAtomSet, useAtomValue } from "@effect/atom-react";
|
|
|
2
2
|
import type * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
3
3
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
4
4
|
import type { DraftSession } from "#draftSession.ts";
|
|
5
|
-
import type {
|
|
5
|
+
import type { PersistedDraftConfig } from "#persistence.ts";
|
|
6
6
|
import type { Encoded, FieldFailure, Fields, Form, Invalid, Name } from "#shape.ts";
|
|
7
7
|
export const DraftSessionContext = createContext<DraftSession | undefined>(undefined);
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function browserStorage(): DraftStorage {
|
|
12
|
-
return globalThis.localStorage;
|
|
13
|
-
}
|
|
14
|
-
export function usePersistedDraft(config: BrowserDraftConfig): PersistedDraftConfig;
|
|
15
|
-
export function usePersistedDraft(config: BrowserDraftConfig | undefined): PersistedDraftConfig | undefined;
|
|
16
|
-
export function usePersistedDraft(config: BrowserDraftConfig | undefined): PersistedDraftConfig | undefined {
|
|
8
|
+
export function usePersistedDraft(config: PersistedDraftConfig): PersistedDraftConfig;
|
|
9
|
+
export function usePersistedDraft(config: PersistedDraftConfig | undefined): PersistedDraftConfig | undefined;
|
|
10
|
+
export function usePersistedDraft(config: PersistedDraftConfig | undefined): PersistedDraftConfig | undefined {
|
|
17
11
|
const session = useContext(DraftSessionContext);
|
|
18
|
-
return config ? { ...config, session: session ?? config.session
|
|
12
|
+
return config ? { ...config, session: session ?? config.session } : undefined;
|
|
19
13
|
}
|
|
20
14
|
export interface Field<TFields extends Fields, TKey extends Name<TFields>> {
|
|
21
15
|
readonly choices: readonly Encoded<TFields>[TKey][] | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Effect, Option, type Schema } from "effect";
|
|
2
|
+
import { rejectedField } from "@shivaedev/errors/rejectedField.ts";
|
|
3
|
+
import type { FieldFailure, Fields, Name, Path, Submitter } from "#shape.ts";
|
|
4
|
+
|
|
5
|
+
export interface FieldRejection<TName extends string> {
|
|
6
|
+
readonly field: TName | Path<TName>;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type RejectField<TFields extends Fields, TError> = (error: TError) => FieldRejection<Name<TFields>> | undefined;
|
|
11
|
+
|
|
12
|
+
type Unmatched<TFields extends Fields, TError> = TError extends { readonly _tag: string; readonly message: string }
|
|
13
|
+
? "field" extends keyof TError
|
|
14
|
+
? Exclude<TError["field"], Name<TFields> | undefined>
|
|
15
|
+
: never
|
|
16
|
+
: never;
|
|
17
|
+
|
|
18
|
+
export type FieldRejectionMapping<TFields extends Fields, TError> = { readonly rejectField?: RejectField<TFields, TError> } & ([
|
|
19
|
+
Unmatched<TFields, TError>,
|
|
20
|
+
] extends [never]
|
|
21
|
+
? unknown
|
|
22
|
+
: { readonly rejectField: RejectField<TFields, TError> });
|
|
23
|
+
|
|
24
|
+
export function fieldRejectionOf<TFields extends Fields>(schema: Schema.Struct<TFields>) {
|
|
25
|
+
return (error: unknown): FieldRejection<Name<TFields>> | undefined => {
|
|
26
|
+
function isName(name: string): name is Name<TFields> {
|
|
27
|
+
return Object.hasOwn(schema.fields, name);
|
|
28
|
+
}
|
|
29
|
+
return Option.match(rejectedField(error), {
|
|
30
|
+
onNone: () => undefined,
|
|
31
|
+
onSome: ({ field, message }) => (isName(field) ? { field, message } : undefined),
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function rejecting<TFields extends Fields, TValues, TResult, TError, TRequirements>(
|
|
37
|
+
schema: Schema.Struct<TFields>,
|
|
38
|
+
save: (values: TValues) => Effect.Effect<TResult, TError, TRequirements>,
|
|
39
|
+
reject: RejectField<TFields, TError> | undefined,
|
|
40
|
+
) {
|
|
41
|
+
const rejection = reject ?? fieldRejectionOf(schema);
|
|
42
|
+
return (values: TValues, submitter: Submitter<Name<TFields>>): Effect.Effect<TResult, TError | FieldFailure, TRequirements> =>
|
|
43
|
+
Effect.catch(save(values), (error): Effect.Effect<never, TError | FieldFailure> => {
|
|
44
|
+
const found = rejection(error);
|
|
45
|
+
return found === undefined ? Effect.fail(error) : submitter.fail(found.field, found.message);
|
|
46
|
+
});
|
|
47
|
+
}
|
package/src/form.ts
CHANGED
|
@@ -2,26 +2,16 @@ import { Effect, Layer, Result, Schema, type SchemaIssue, SchemaParser } from "e
|
|
|
2
2
|
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
3
3
|
import * as Atom from "effect/reactivity/Atom";
|
|
4
4
|
import type * as AtomRef from "effect/reactivity/AtomRef";
|
|
5
|
+
import { persistence } from "#adapters/persistence.ts";
|
|
5
6
|
import { checkFeedback } from "./checkFeedback.ts";
|
|
6
7
|
import { draft } from "./draft.ts";
|
|
7
|
-
import { literalChoices, messageAt,
|
|
8
|
+
import { literalChoices, messageAt, schemaFeedback } from "./messages.ts";
|
|
8
9
|
import { pathMessageAt } from "./paths.ts";
|
|
9
|
-
import { persistence } from "./persistence.ts";
|
|
10
10
|
import { fromRef, propertyRef } from "./refs.ts";
|
|
11
|
-
import {
|
|
12
|
-
type Config,
|
|
13
|
-
type Decoded,
|
|
14
|
-
type Encoded,
|
|
15
|
-
FieldFailure,
|
|
16
|
-
type Fields,
|
|
17
|
-
type Form,
|
|
18
|
-
Invalid,
|
|
19
|
-
type Name,
|
|
20
|
-
type Services,
|
|
21
|
-
type Submitter,
|
|
22
|
-
} from "./shape.ts";
|
|
11
|
+
import { type Config, type Decoded, type Encoded, FieldFailure, type Fields, type Form, type Name, type Services, type Submitter } from "./shape.ts";
|
|
23
12
|
import { statusOf } from "./statusOf.ts";
|
|
24
13
|
import { trackRejections } from "./trackRejections.ts";
|
|
14
|
+
import { validation } from "./validation.ts";
|
|
25
15
|
|
|
26
16
|
const emptyRuntime = Atom.runtime(Layer.empty);
|
|
27
17
|
|
|
@@ -42,7 +32,7 @@ export function form<TFields extends Fields, TValue, TError, TServices = never,
|
|
|
42
32
|
const checks: ReadonlyMap<string, (value: Encoded<TFields>[Name<TFields>]) => Effect.Effect<string | undefined, never, TServices>> = new Map(
|
|
43
33
|
Object.entries(config.checks ?? {}),
|
|
44
34
|
);
|
|
45
|
-
const decode =
|
|
35
|
+
const { decode, snapshot } = validation(schema, config.requiredMessages);
|
|
46
36
|
|
|
47
37
|
const editing = draft(initialValues, config.persistedDraft ? persistence(schema, config.persistedDraft) : undefined);
|
|
48
38
|
const { values } = editing;
|
|
@@ -87,14 +77,17 @@ export function form<TFields extends Fields, TValue, TError, TServices = never,
|
|
|
87
77
|
fail: (path, message) => Effect.fail(new FieldFailure(typeof path === "string" ? { message, path } : { message, path: path[0], segments: path })),
|
|
88
78
|
};
|
|
89
79
|
|
|
80
|
+
const validate = runtime.fn<void>()(() => {
|
|
81
|
+
status.attempt();
|
|
82
|
+
return snapshot(values.value);
|
|
83
|
+
});
|
|
84
|
+
|
|
90
85
|
const submit = runtime.fn<void>()(() =>
|
|
91
86
|
Effect.gen(function* () {
|
|
92
87
|
status.attempt();
|
|
93
88
|
const submission = editing.submission();
|
|
94
89
|
const submitted = submission.values;
|
|
95
|
-
const value = yield*
|
|
96
|
-
Effect.mapError((issue) => new Invalid({ formMessage: messageAtPath(issue, []), messages: messagesByField(issue, config.requiredMessages) })),
|
|
97
|
-
);
|
|
90
|
+
const { decoded: value } = yield* snapshot(submitted);
|
|
98
91
|
return yield* onSubmit(value, submitter, submitted).pipe(
|
|
99
92
|
Effect.tapError(noted(submitted)),
|
|
100
93
|
Effect.tap((saved) => Effect.sync(() => submission.accept(config.savedValues?.(saved)))),
|
|
@@ -130,6 +123,7 @@ export function form<TFields extends Fields, TValue, TError, TServices = never,
|
|
|
130
123
|
revert: editing.revert,
|
|
131
124
|
submit,
|
|
132
125
|
submitting: Atom.map(submit, AsyncResult.isWaiting),
|
|
126
|
+
validate,
|
|
133
127
|
values,
|
|
134
128
|
};
|
|
135
129
|
}
|
package/src/persistence.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context } from "effect";
|
|
2
2
|
import type { DraftSession } from "./draftSession.ts";
|
|
3
3
|
|
|
4
4
|
export interface DraftStorage {
|
|
@@ -7,6 +7,8 @@ export interface DraftStorage {
|
|
|
7
7
|
readonly setItem: (key: string, value: string) => void;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export class DraftStorageProvider extends Context.Service<DraftStorageProvider, () => DraftStorage>()("@shivaedev/form/DraftStorageProvider") {}
|
|
11
|
+
|
|
10
12
|
export interface PersistedDraftConfig {
|
|
11
13
|
readonly key: string;
|
|
12
14
|
readonly maxAgeMs?: number;
|
|
@@ -21,88 +23,3 @@ export interface Persistence<TValues> {
|
|
|
21
23
|
readonly read: () => { readonly baseline: TValues; readonly edited: readonly string[]; readonly values: TValues } | undefined;
|
|
22
24
|
readonly write: (baseline: TValues, values: TValues, edited: readonly string[]) => void;
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
export function persistence<TSchema extends Schema.Constraint>(schema: TSchema, config: PersistedDraftConfig): Persistence<TSchema["Encoded"]> {
|
|
26
|
-
const encoded = Schema.toEncoded(schema);
|
|
27
|
-
const StoredDraft = Schema.Struct({
|
|
28
|
-
baseline: Schema.Unknown,
|
|
29
|
-
edited: Schema.Array(Schema.String),
|
|
30
|
-
session: Schema.String,
|
|
31
|
-
updatedAt: Schema.Number,
|
|
32
|
-
values: Schema.Unknown,
|
|
33
|
-
version: Schema.Literal(1),
|
|
34
|
-
});
|
|
35
|
-
const valid = Schema.is(StoredDraft);
|
|
36
|
-
const decode = SchemaParser.decodeUnknownSync(encoded, { disableChecks: true });
|
|
37
|
-
const maxAgeMs = config.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
38
|
-
const maxBytes = config.maxBytes ?? 256 * 1024;
|
|
39
|
-
function bytes(value: string) {
|
|
40
|
-
return new TextEncoder().encode(value).byteLength;
|
|
41
|
-
}
|
|
42
|
-
const storage = config.storage;
|
|
43
|
-
const session = config.session?.id ?? "";
|
|
44
|
-
function active() {
|
|
45
|
-
return config.session?.active !== false;
|
|
46
|
-
}
|
|
47
|
-
function attempt<A>(run: () => A, failed?: () => void): A | undefined {
|
|
48
|
-
try {
|
|
49
|
-
return run();
|
|
50
|
-
} catch {
|
|
51
|
-
failed?.();
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function clear() {
|
|
56
|
-
attempt(() => storage().removeItem(config.key));
|
|
57
|
-
}
|
|
58
|
-
config.session?.track(config.key, clear);
|
|
59
|
-
return {
|
|
60
|
-
clear: () => {
|
|
61
|
-
if (active()) {
|
|
62
|
-
clear();
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
read: () =>
|
|
66
|
-
attempt(() => {
|
|
67
|
-
if (!active()) {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
const raw = storage().getItem(config.key);
|
|
71
|
-
if (raw === null) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
const value: unknown = bytes(raw) <= maxBytes ? JSON.parse(raw) : undefined;
|
|
75
|
-
const now = config.now();
|
|
76
|
-
if (
|
|
77
|
-
valid(value)
|
|
78
|
-
&& value.session === session
|
|
79
|
-
&& Number.isFinite(maxAgeMs)
|
|
80
|
-
&& Number.isFinite(maxBytes)
|
|
81
|
-
&& maxAgeMs > 0
|
|
82
|
-
&& maxBytes > 0
|
|
83
|
-
&& now - value.updatedAt <= maxAgeMs
|
|
84
|
-
&& value.updatedAt <= now
|
|
85
|
-
) {
|
|
86
|
-
return { baseline: decode(value.baseline), edited: value.edited, values: decode(value.values) };
|
|
87
|
-
}
|
|
88
|
-
clear();
|
|
89
|
-
return undefined;
|
|
90
|
-
}, clear),
|
|
91
|
-
write: (baseline, values, edited) => {
|
|
92
|
-
if (!active()) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
attempt(() => {
|
|
96
|
-
const raw = JSON.stringify({ baseline, edited, session, updatedAt: config.now(), values, version: 1 });
|
|
97
|
-
const parsed: unknown = JSON.parse(raw);
|
|
98
|
-
if (!(maxAgeMs > 0 && Number.isFinite(maxAgeMs) && Number.isFinite(maxBytes) && bytes(raw) <= maxBytes && valid(parsed))) {
|
|
99
|
-
clear();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
decode(parsed.baseline);
|
|
103
|
-
decode(parsed.values);
|
|
104
|
-
storage().setItem(config.key, raw);
|
|
105
|
-
}, clear);
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
package/src/shape.ts
CHANGED
|
@@ -60,6 +60,11 @@ export type Config<TFields extends Fields, A, E, R, TRuntimeError> = {
|
|
|
60
60
|
? { readonly runtime?: Atom.AtomRuntime<Services<TFields, R>, TRuntimeError> }
|
|
61
61
|
: { readonly runtime: Atom.AtomRuntime<Services<TFields, R>, TRuntimeError> });
|
|
62
62
|
|
|
63
|
+
export interface Snapshot<TFields extends Fields> {
|
|
64
|
+
readonly decoded: Decoded<TFields>;
|
|
65
|
+
readonly encoded: Encoded<TFields>;
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
export interface Form<TFields extends Fields, A, E, TRuntimeError> {
|
|
64
69
|
readonly blur: (name: Name<TFields>) => void;
|
|
65
70
|
readonly change: <TKey extends Name<TFields>>(name: TKey, value: Encoded<TFields>[TKey]) => void;
|
|
@@ -72,6 +77,7 @@ export interface Form<TFields extends Fields, A, E, TRuntimeError> {
|
|
|
72
77
|
readonly revert: () => void;
|
|
73
78
|
readonly submit: Atom.AtomResultFn<void, A, E | TRuntimeError | FieldFailure | Invalid>;
|
|
74
79
|
readonly submitting: Atom.Atom<boolean>;
|
|
80
|
+
readonly validate: Atom.AtomResultFn<void, Snapshot<TFields>, TRuntimeError | Invalid>;
|
|
75
81
|
readonly values: AtomRef.AtomRef<Encoded<TFields>>;
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Effect, type Schema, SchemaParser } from "effect";
|
|
2
|
+
import { messageAtPath, messagesByField } from "./messages.ts";
|
|
3
|
+
import { type Encoded, type Fields, Invalid, type Name, type Snapshot } from "./shape.ts";
|
|
4
|
+
|
|
5
|
+
export function validation<TFields extends Fields>(
|
|
6
|
+
schema: Schema.Struct<TFields>,
|
|
7
|
+
requiredMessages: Readonly<Partial<Record<Name<TFields>, string>>> | undefined,
|
|
8
|
+
) {
|
|
9
|
+
const decode = SchemaParser.decodeUnknownEffect(schema, { errors: "all" });
|
|
10
|
+
return {
|
|
11
|
+
decode,
|
|
12
|
+
snapshot: (encoded: Encoded<TFields>) =>
|
|
13
|
+
decode(encoded).pipe(
|
|
14
|
+
Effect.map((decoded): Snapshot<TFields> => ({ decoded, encoded })),
|
|
15
|
+
Effect.mapError((issue) => new Invalid({ formMessage: messageAtPath(issue, []), messages: messagesByField(issue, requiredMessages) })),
|
|
16
|
+
),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/client/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,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC1E,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,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC;IAChF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,YAAY,CAAC;CACtC;AAID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,oBAAoB,CAAC;AACpF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAAC;AAK5G,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,CAOtB;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,CAKvD;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/client/react.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/client/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE1E,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAIxE,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAA2B,SAAS,CAAC,CAAC;AAItF,SAAS,cAAc;IACtB,OAAO,UAAU,CAAC,YAAY,CAAC;AAChC,CAAC;AAGD,MAAM,UAAU,iBAAiB,CAAC,MAAsC;IACvE,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,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,cAAc,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1H,CAAC;AASD,MAAM,UAAU,QAAQ,CACvB,IAAwC,EACxC,IAAU;IAEV,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,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"}
|