@lucas-barake/effect-form 0.1.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.
Files changed (64) hide show
  1. package/Form/package.json +6 -0
  2. package/FormAtoms/package.json +6 -0
  3. package/LICENSE +21 -0
  4. package/Mode/package.json +6 -0
  5. package/README.md +5 -0
  6. package/Validation/package.json +6 -0
  7. package/dist/cjs/Form.js +299 -0
  8. package/dist/cjs/Form.js.map +1 -0
  9. package/dist/cjs/FormAtoms.js +266 -0
  10. package/dist/cjs/FormAtoms.js.map +1 -0
  11. package/dist/cjs/Mode.js +64 -0
  12. package/dist/cjs/Mode.js.map +1 -0
  13. package/dist/cjs/Validation.js +69 -0
  14. package/dist/cjs/Validation.js.map +1 -0
  15. package/dist/cjs/index.js +35 -0
  16. package/dist/cjs/index.js.map +1 -0
  17. package/dist/cjs/internal/dirty.js +101 -0
  18. package/dist/cjs/internal/dirty.js.map +1 -0
  19. package/dist/cjs/internal/path.js +96 -0
  20. package/dist/cjs/internal/path.js.map +1 -0
  21. package/dist/cjs/internal/weak-registry.js +52 -0
  22. package/dist/cjs/internal/weak-registry.js.map +1 -0
  23. package/dist/dts/Form.d.ts +317 -0
  24. package/dist/dts/Form.d.ts.map +1 -0
  25. package/dist/dts/FormAtoms.d.ts +145 -0
  26. package/dist/dts/FormAtoms.d.ts.map +1 -0
  27. package/dist/dts/Mode.d.ts +55 -0
  28. package/dist/dts/Mode.d.ts.map +1 -0
  29. package/dist/dts/Validation.d.ts +23 -0
  30. package/dist/dts/Validation.d.ts.map +1 -0
  31. package/dist/dts/index.d.ts +26 -0
  32. package/dist/dts/index.d.ts.map +1 -0
  33. package/dist/dts/internal/dirty.d.ts +13 -0
  34. package/dist/dts/internal/dirty.d.ts.map +1 -0
  35. package/dist/dts/internal/path.d.ts +32 -0
  36. package/dist/dts/internal/path.d.ts.map +1 -0
  37. package/dist/dts/internal/weak-registry.d.ts +7 -0
  38. package/dist/dts/internal/weak-registry.d.ts.map +1 -0
  39. package/dist/esm/Form.js +263 -0
  40. package/dist/esm/Form.js.map +1 -0
  41. package/dist/esm/FormAtoms.js +238 -0
  42. package/dist/esm/FormAtoms.js.map +1 -0
  43. package/dist/esm/Mode.js +36 -0
  44. package/dist/esm/Mode.js.map +1 -0
  45. package/dist/esm/Validation.js +40 -0
  46. package/dist/esm/Validation.js.map +1 -0
  47. package/dist/esm/index.js +26 -0
  48. package/dist/esm/index.js.map +1 -0
  49. package/dist/esm/internal/dirty.js +72 -0
  50. package/dist/esm/internal/dirty.js.map +1 -0
  51. package/dist/esm/internal/path.js +86 -0
  52. package/dist/esm/internal/path.js.map +1 -0
  53. package/dist/esm/internal/weak-registry.js +45 -0
  54. package/dist/esm/internal/weak-registry.js.map +1 -0
  55. package/dist/esm/package.json +4 -0
  56. package/package.json +64 -0
  57. package/src/Form.ts +522 -0
  58. package/src/FormAtoms.ts +485 -0
  59. package/src/Mode.ts +59 -0
  60. package/src/Validation.ts +43 -0
  61. package/src/index.ts +28 -0
  62. package/src/internal/dirty.ts +96 -0
  63. package/src/internal/path.ts +93 -0
  64. package/src/internal/weak-registry.ts +60 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Internal dirty tracking algorithms.
3
+ *
4
+ * @internal
5
+ */
6
+ import * as Equal from "effect/Equal"
7
+ import * as Utils from "effect/Utils"
8
+ import { getNestedValue } from "./path.js"
9
+
10
+ /**
11
+ * Recalculates dirty fields for an array after mutation.
12
+ * Clears all paths under the array and re-evaluates each item.
13
+ */
14
+ export const recalculateDirtyFieldsForArray = (
15
+ dirtyFields: ReadonlySet<string>,
16
+ initialValues: unknown,
17
+ arrayPath: string,
18
+ newItems: ReadonlyArray<unknown>,
19
+ ): ReadonlySet<string> => {
20
+ const nextDirty = new Set(
21
+ Array.from(dirtyFields).filter(
22
+ (path) => path !== arrayPath && !path.startsWith(arrayPath + ".") && !path.startsWith(arrayPath + "["),
23
+ ),
24
+ )
25
+
26
+ const initialItems = (getNestedValue(initialValues, arrayPath) ?? []) as ReadonlyArray<unknown>
27
+
28
+ const loopLength = Math.max(newItems.length, initialItems.length)
29
+ for (let i = 0; i < loopLength; i++) {
30
+ const itemPath = `${arrayPath}[${i}]`
31
+ const newItem = newItems[i]
32
+ const initialItem = initialItems[i]
33
+
34
+ const isEqual = Utils.structuralRegion(() => Equal.equals(newItem, initialItem))
35
+ if (!isEqual) {
36
+ nextDirty.add(itemPath)
37
+ }
38
+ }
39
+
40
+ if (newItems.length !== initialItems.length) {
41
+ nextDirty.add(arrayPath)
42
+ } else {
43
+ nextDirty.delete(arrayPath)
44
+ }
45
+
46
+ return nextDirty
47
+ }
48
+
49
+ /**
50
+ * Recalculates dirty fields for a subtree after value change.
51
+ * Clears the rootPath and all children, then re-evaluates recursively.
52
+ *
53
+ * @param rootPath - Empty string for full form, or a specific path for targeted update
54
+ */
55
+ export const recalculateDirtySubtree = (
56
+ currentDirty: ReadonlySet<string>,
57
+ allInitial: unknown,
58
+ allValues: unknown,
59
+ rootPath: string = "",
60
+ ): ReadonlySet<string> => {
61
+ const nextDirty = new Set(currentDirty)
62
+
63
+ if (rootPath === "") {
64
+ nextDirty.clear()
65
+ } else {
66
+ for (const path of nextDirty) {
67
+ if (path === rootPath || path.startsWith(rootPath + ".") || path.startsWith(rootPath + "[")) {
68
+ nextDirty.delete(path)
69
+ }
70
+ }
71
+ }
72
+
73
+ const targetValue = rootPath ? getNestedValue(allValues, rootPath) : allValues
74
+ const targetInitial = rootPath ? getNestedValue(allInitial, rootPath) : allInitial
75
+
76
+ const recurse = (current: unknown, initial: unknown, path: string): void => {
77
+ if (Array.isArray(current)) {
78
+ const initialArr = (initial ?? []) as ReadonlyArray<unknown>
79
+ for (let i = 0; i < Math.max(current.length, initialArr.length); i++) {
80
+ recurse(current[i], initialArr[i], path ? `${path}[${i}]` : `[${i}]`)
81
+ }
82
+ } else if (current !== null && typeof current === "object") {
83
+ const initialObj = (initial ?? {}) as Record<string, unknown>
84
+ const allKeys = new Set([...Object.keys(current as object), ...Object.keys(initialObj)])
85
+ for (const key of allKeys) {
86
+ recurse((current as Record<string, unknown>)[key], initialObj[key], path ? `${path}.${key}` : key)
87
+ }
88
+ } else {
89
+ const isEqual = Utils.structuralRegion(() => Equal.equals(current, initial))
90
+ if (!isEqual && path) nextDirty.add(path)
91
+ }
92
+ }
93
+
94
+ recurse(targetValue, targetInitial, rootPath)
95
+ return nextDirty
96
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Internal path utilities for form field operations.
3
+ *
4
+ * @internal
5
+ */
6
+
7
+ const BRACKET_NOTATION_REGEX = /\[(\d+)\]/g
8
+
9
+ /**
10
+ * Converts a schema path array to a dot/bracket notation string.
11
+ *
12
+ * @example
13
+ * schemaPathToFieldPath(["items", 0, "name"]) // "items[0].name"
14
+ */
15
+ export const schemaPathToFieldPath = (path: ReadonlyArray<PropertyKey>): string => {
16
+ if (path.length === 0) return ""
17
+
18
+ let result = String(path[0])
19
+ for (let i = 1; i < path.length; i++) {
20
+ const segment = path[i]
21
+ if (typeof segment === "number") {
22
+ result += `[${segment}]`
23
+ } else {
24
+ result += `.${String(segment)}`
25
+ }
26
+ }
27
+ return result
28
+ }
29
+
30
+ /**
31
+ * Checks if a field path or any of its parent paths are in the dirty set.
32
+ */
33
+ export const isPathOrParentDirty = (dirtyFields: ReadonlySet<string>, path: string): boolean => {
34
+ if (dirtyFields.has(path)) return true
35
+
36
+ let parent = path
37
+ while (true) {
38
+ const lastDot = parent.lastIndexOf(".")
39
+ const lastBracket = parent.lastIndexOf("[")
40
+ const splitIndex = Math.max(lastDot, lastBracket)
41
+
42
+ if (splitIndex === -1) break
43
+
44
+ parent = parent.substring(0, splitIndex)
45
+ if (dirtyFields.has(parent)) return true
46
+ }
47
+
48
+ return false
49
+ }
50
+
51
+ /**
52
+ * Gets a nested value from an object using dot/bracket notation path.
53
+ *
54
+ * @example
55
+ * getNestedValue({ items: [{ name: "A" }] }, "items[0].name") // "A"
56
+ */
57
+ export const getNestedValue = (obj: unknown, path: string): unknown => {
58
+ if (path === "") return obj
59
+ const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".")
60
+ let current: unknown = obj
61
+ for (const part of parts) {
62
+ if (current == null || typeof current !== "object") return undefined
63
+ current = (current as Record<string, unknown>)[part]
64
+ }
65
+ return current
66
+ }
67
+
68
+ /**
69
+ * Sets a nested value in an object immutably using dot/bracket notation path.
70
+ *
71
+ * @example
72
+ * setNestedValue({ items: [{ name: "A" }] }, "items[0].name", "B")
73
+ * // { items: [{ name: "B" }] }
74
+ */
75
+ export const setNestedValue = <T>(obj: T, path: string, value: unknown): T => {
76
+ if (path === "") return value as T
77
+ const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".")
78
+ const result = { ...obj } as Record<string, unknown>
79
+
80
+ let current = result
81
+ for (let i = 0; i < parts.length - 1; i++) {
82
+ const part = parts[i]
83
+ if (Array.isArray(current[part])) {
84
+ current[part] = [...(current[part] as Array<unknown>)]
85
+ } else {
86
+ current[part] = { ...(current[part] as Record<string, unknown>) }
87
+ }
88
+ current = current[part] as Record<string, unknown>
89
+ }
90
+
91
+ current[parts[parts.length - 1]] = value
92
+ return result as T
93
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Internal WeakRef-based registry for caching atoms.
3
+ *
4
+ * @internal
5
+ */
6
+
7
+ /**
8
+ * A registry that uses WeakRef to allow garbage collection of cached values.
9
+ *
10
+ * @internal
11
+ */
12
+ export interface WeakRegistry<V extends object> {
13
+ readonly get: (key: string) => V | undefined
14
+ readonly set: (key: string, value: V) => void
15
+ readonly delete: (key: string) => boolean
16
+ readonly clear: () => void
17
+ readonly values: () => IterableIterator<V>
18
+ }
19
+
20
+ /**
21
+ * Creates a WeakRef-based registry with automatic cleanup via FinalizationRegistry.
22
+ * Falls back to a regular Map in environments without WeakRef support.
23
+ *
24
+ * @internal
25
+ */
26
+ export const createWeakRegistry = <V extends object>(): WeakRegistry<V> => {
27
+ if (typeof WeakRef === "undefined" || typeof FinalizationRegistry === "undefined") {
28
+ const map = new Map<string, V>()
29
+ return {
30
+ get: (key) => map.get(key),
31
+ set: (key, value) => {
32
+ map.set(key, value)
33
+ },
34
+ delete: (key) => map.delete(key),
35
+ clear: () => map.clear(),
36
+ values: () => map.values(),
37
+ }
38
+ }
39
+
40
+ const map = new Map<string, WeakRef<V>>()
41
+ const registry = new FinalizationRegistry<string>((key) => {
42
+ map.delete(key)
43
+ })
44
+
45
+ return {
46
+ get: (key) => map.get(key)?.deref(),
47
+ set: (key, value) => {
48
+ map.set(key, new WeakRef(value))
49
+ registry.register(value, key)
50
+ },
51
+ delete: (key) => map.delete(key),
52
+ clear: () => map.clear(),
53
+ *values() {
54
+ for (const ref of map.values()) {
55
+ const value = ref.deref()
56
+ if (value !== undefined) yield value
57
+ }
58
+ },
59
+ }
60
+ }