bireactive 0.2.2 → 0.2.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # bi-reactive
2
2
 
3
- [npm](https://www.npmjs.com/package/bireactive) · [GitHub](https://github.com/OrionReed/bireactive) · [site](https://orionreed.github.io/bireactive/)
3
+ [npm](https://www.npmjs.com/package/bireactive) · [GitHub](https://github.com/OrionReed/bireactive) · [site](https://orionreed.github.io/bireactive/) · [API](https://orionreed.github.io/bireactive/api/)
4
4
 
5
5
  A signals-like bidirectional reactive programming system where edges can go both ways. Forward and backward propagation are handled by the engine, with the same set of caveats as regular reactive programming.
6
6
 
package/dist/coll.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { type Cell, type Read, type Writable } from "./core/index.js";
2
+ /** Accessor for an element's writable field cell. Forward reads `.value`;
3
+ * the backward pass writes it. */
4
+ export type Field<E, V> = (e: E) => Writable<Cell<V>>;
5
+ /** A forward test over an element's fields, optionally assertable —
6
+ * `assert(e)` makes the test pass by writing fields. */
7
+ export interface FieldPred<E> {
8
+ (e: E): boolean;
9
+ assert?: (e: E) => void;
10
+ }
11
+ export interface Group<K, E> {
12
+ key: K;
13
+ items: readonly E[];
14
+ }
15
+ export interface GroupOpts<E, K> {
16
+ /** Fixed key order; seeds empty buckets and pins column order. */
17
+ order?: readonly K[];
18
+ /** Order field within each group; enables `move(e, key, index)`. */
19
+ sort?: Field<E, number>;
20
+ }
21
+ /** `field === value`, assertable by writing the field. */
22
+ export declare function is<E, V>(field: Field<E, V>, value: V): FieldPred<E>;
23
+ /** Conjunction; asserts every clause. */
24
+ export declare function allPass<E>(...preds: readonly FieldPred<E>[]): FieldPred<E>;
25
+ /** Read-only projection with chainable structural lenses. */
26
+ export declare class View<E> {
27
+ protected readonly list: Read<readonly E[]>;
28
+ readonly key: (e: E) => unknown;
29
+ protected readonly parent: View<E> | null;
30
+ protected constructor(list: Read<readonly E[]>, key: (e: E) => unknown, parent: View<E> | null);
31
+ /** Current members; tracked when read in an effect/derive. */
32
+ get items(): readonly E[];
33
+ /** The source collection at the head of the chain. */
34
+ get root(): Coll<E>;
35
+ /** Make `e` appear in this view: satisfy own constraint, recursively up. */
36
+ assertContains(e: E): void;
37
+ protected assertSelf(_e: E): void;
38
+ filter(pred: FieldPred<E>): View<E>;
39
+ sortBy(field: Field<E, number>): SortView<E>;
40
+ groupBy<K>(field: Field<E, K>, opts?: GroupOpts<E, K>): GroupView<K, E>;
41
+ map<F>(f: (e: E) => F): Read<readonly F[]>;
42
+ /** Remove `e` from the source. */
43
+ remove(e: E): void;
44
+ }
45
+ /** A writable source collection. */
46
+ export declare class Coll<E> extends View<E> {
47
+ #private;
48
+ constructor(items: readonly E[], key: (e: E) => unknown);
49
+ assertContains(e: E): void;
50
+ insert(e: E, at?: number): void;
51
+ removeFromSource(e: E): void;
52
+ }
53
+ /** Sorted view. `move` writes the order field between the drop neighbours. */
54
+ export declare class SortView<E> extends View<E> {
55
+ #private;
56
+ constructor(parent: View<E>, field: Field<E, number>);
57
+ move(e: E, to: number): void;
58
+ }
59
+ /** Grouped view. `move`/`insert` write the group field (and, with a `sort`
60
+ * field, the order field). Backward composition runs the parent chain. */
61
+ export declare class GroupView<K, E> {
62
+ #private;
63
+ readonly groups: Read<readonly Group<K, E>[]>;
64
+ constructor(parent: View<E>, field: Field<E, K>, opts?: GroupOpts<E, K>);
65
+ get value(): readonly Group<K, E>[];
66
+ map<F>(f: (g: Group<K, E>) => F): Read<readonly F[]>;
67
+ /** Place `e` in group `toKey` at `index`. Inserts it into the source if
68
+ * it isn't there yet, asserts every upstream filter, then writes the
69
+ * group key and order field — all in one batch. */
70
+ move(e: E, toKey: K, index?: number): void;
71
+ insert(e: E, toKey: K, index?: number): void;
72
+ remove(e: E): void;
73
+ }
74
+ export declare function coll<E>(items: readonly E[], key: (e: E) => unknown): Coll<E>;
package/dist/coll.js ADDED
@@ -0,0 +1,208 @@
1
+ // coll.ts — a keyed, ordered, *bidirectional* collection.
2
+ //
3
+ // The structural sibling of `tree.ts`. A `Coll<E>` holds stable element
4
+ // handles (records of cells), and each projection — `filter` / `sortBy` /
5
+ // `groupBy` — is a WRITABLE structural lens. You edit the *view* and the
6
+ // backward pass writes the elements' own fields:
7
+ //
8
+ // sortBy(rank) → move(e, i) writes `rank` (position is a field)
9
+ // groupBy(key) → move(e, k, i) writes `key` (membership is a field)
10
+ // filter(pred) → insert/assert writes whatever pred demands
11
+ //
12
+ // Because position / group / visibility are the elements' own writable
13
+ // cells, these lenses are COMPLEMENT-FREE — the "complement" lives in the
14
+ // field cells, not the lens (contrast `Seq<T>`, where order is imposed and
15
+ // must be remembered). One `move`/`insert` composes the backward passes up
16
+ // the chain in a single `batch`: drop into a filtered + grouped + sorted
17
+ // view and the filter's predicate, the group key, and the rank all get set
18
+ // at once.
19
+ //
20
+ // Two change classes, mirroring the engine's two levels:
21
+ // • value change — a field cell changes; forward derivations re-flow.
22
+ // • structural edit — insert/remove/move; a discrete batched transition.
23
+ // No lens edges are built at runtime, so acyclicity holds as ever.
24
+ import { batch, cell, derive } from "./core/index.js";
25
+ /** `field === value`, assertable by writing the field. */
26
+ export function is(field, value) {
27
+ const p = ((e) => field(e).value === value);
28
+ p.assert = (e) => {
29
+ field(e).value = value;
30
+ };
31
+ return p;
32
+ }
33
+ /** Conjunction; asserts every clause. */
34
+ export function allPass(...preds) {
35
+ const p = ((e) => preds.every(q => q(e)));
36
+ p.assert = (e) => {
37
+ for (const q of preds)
38
+ q.assert?.(e);
39
+ };
40
+ return p;
41
+ }
42
+ /** Read-only projection with chainable structural lenses. */
43
+ export class View {
44
+ list;
45
+ key;
46
+ parent;
47
+ constructor(list, key, parent) {
48
+ this.list = list;
49
+ this.key = key;
50
+ this.parent = parent;
51
+ }
52
+ /** Current members; tracked when read in an effect/derive. */
53
+ get items() {
54
+ return this.list.value;
55
+ }
56
+ /** The source collection at the head of the chain. */
57
+ get root() {
58
+ let v = this;
59
+ while (v.parent)
60
+ v = v.parent;
61
+ return v;
62
+ }
63
+ /** Make `e` appear in this view: satisfy own constraint, recursively up. */
64
+ assertContains(e) {
65
+ this.parent?.assertContains(e);
66
+ this.assertSelf(e);
67
+ }
68
+ assertSelf(_e) { }
69
+ filter(pred) {
70
+ return new FilterView(this, pred);
71
+ }
72
+ sortBy(field) {
73
+ return new SortView(this, field);
74
+ }
75
+ groupBy(field, opts) {
76
+ return new GroupView(this, field, opts);
77
+ }
78
+ map(f) {
79
+ return derive(() => this.list.value.map(f));
80
+ }
81
+ /** Remove `e` from the source. */
82
+ remove(e) {
83
+ this.root.removeFromSource(e);
84
+ }
85
+ }
86
+ /** A writable source collection. */
87
+ export class Coll extends View {
88
+ #src;
89
+ constructor(items, key) {
90
+ const src = cell(items);
91
+ super(src, key, null);
92
+ this.#src = src;
93
+ }
94
+ assertContains(e) {
95
+ if (!this.#src.value.includes(e))
96
+ this.insert(e);
97
+ }
98
+ insert(e, at) {
99
+ const arr = [...this.#src.value];
100
+ if (at == null)
101
+ arr.push(e);
102
+ else
103
+ arr.splice(at, 0, e);
104
+ this.#src.value = arr;
105
+ }
106
+ removeFromSource(e) {
107
+ this.#src.value = this.#src.value.filter(x => x !== e);
108
+ }
109
+ }
110
+ class FilterView extends View {
111
+ #pred;
112
+ constructor(parent, pred) {
113
+ super(derive(() => parent.items.filter(pred)), parent.key, parent);
114
+ this.#pred = pred;
115
+ }
116
+ assertSelf(e) {
117
+ this.#pred.assert?.(e);
118
+ }
119
+ }
120
+ /** Sorted view. `move` writes the order field between the drop neighbours. */
121
+ export class SortView extends View {
122
+ #field;
123
+ constructor(parent, field) {
124
+ super(derive(() => [...parent.items].sort((a, b) => field(a).value - field(b).value)), parent.key, parent);
125
+ this.#field = field;
126
+ }
127
+ move(e, to) {
128
+ const others = this.items.filter(x => x !== e);
129
+ batch(() => {
130
+ this.assertContains(e);
131
+ this.#field(e).value = between(rankAt(others, this.#field, to - 1), rankAt(others, this.#field, to));
132
+ });
133
+ }
134
+ }
135
+ /** Grouped view. `move`/`insert` write the group field (and, with a `sort`
136
+ * field, the order field). Backward composition runs the parent chain. */
137
+ export class GroupView {
138
+ groups;
139
+ #parent;
140
+ #field;
141
+ #order;
142
+ #sort;
143
+ constructor(parent, field, opts = {}) {
144
+ this.#parent = parent;
145
+ this.#field = field;
146
+ this.#order = opts.order ?? [];
147
+ this.#sort = opts.sort;
148
+ this.groups = derive(() => {
149
+ const sort = this.#sort;
150
+ const items = sort ? [...parent.items].sort((a, b) => sort(a).value - sort(b).value) : parent.items;
151
+ return groupItems(items, e => field(e).value, this.#order);
152
+ });
153
+ }
154
+ get value() {
155
+ return this.groups.value;
156
+ }
157
+ map(f) {
158
+ return derive(() => this.value.map(f));
159
+ }
160
+ /** Place `e` in group `toKey` at `index`. Inserts it into the source if
161
+ * it isn't there yet, asserts every upstream filter, then writes the
162
+ * group key and order field — all in one batch. */
163
+ move(e, toKey, index) {
164
+ const target = (this.value.find(g => Object.is(g.key, toKey))?.items ?? []).filter(x => x !== e);
165
+ const sort = this.#sort;
166
+ batch(() => {
167
+ this.#parent.assertContains(e);
168
+ this.#field(e).value = toKey;
169
+ if (sort && index != null)
170
+ sort(e).value = between(rankAt(target, sort, index - 1), rankAt(target, sort, index));
171
+ });
172
+ }
173
+ insert(e, toKey, index) {
174
+ this.move(e, toKey, index);
175
+ }
176
+ remove(e) {
177
+ this.#parent.remove(e);
178
+ }
179
+ }
180
+ function groupItems(items, keyOf, order) {
181
+ const buckets = new Map();
182
+ for (const k of order)
183
+ buckets.set(k, []);
184
+ for (const e of items) {
185
+ const k = keyOf(e);
186
+ const arr = buckets.get(k);
187
+ if (arr)
188
+ arr.push(e);
189
+ else
190
+ buckets.set(k, [e]);
191
+ }
192
+ return [...buckets].map(([key, items]) => ({ key, items }));
193
+ }
194
+ function rankAt(arr, field, i) {
195
+ return i >= 0 && i < arr.length ? field(arr[i]).value : undefined;
196
+ }
197
+ function between(a, b) {
198
+ if (a != null && b != null)
199
+ return (a + b) / 2;
200
+ if (a != null)
201
+ return a + 1;
202
+ if (b != null)
203
+ return b - 1;
204
+ return 0;
205
+ }
206
+ export function coll(items, key) {
207
+ return new Coll(items, key);
208
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./animation/index.js";
2
2
  export * from "./assert/index.js";
3
3
  export { type CodeOpts, CodeShape, code, codeStyles, type Token, tokenize } from "./code/index.js";
4
+ export * from "./coll.js";
4
5
  export * from "./core/index.js";
5
6
  export * from "./ext/index.js";
6
7
  export * from "./shapes/index.js";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./assert/index.js";
3
3
  // `code` and `tex` both export `Part`; re-export `code`'s other symbols
4
4
  // explicitly so the wildcard below lets `tex`'s `Part` win.
5
5
  export { CodeShape, code, codeStyles, tokenize } from "./code/index.js";
6
+ export * from "./coll.js";
6
7
  export * from "./core/index.js";
7
8
  export * from "./ext/index.js";
8
9
  export * from "./shapes/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bireactive",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Bi-directional reactive programming.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -31,7 +31,7 @@
31
31
  ],
32
32
  "scripts": {
33
33
  "dev": "vite --host",
34
- "site": "vite-node site/build.ts && vite build && vite-node site/build.ts",
34
+ "site": "vite-node site/build.ts && vite build && vite-node site/build.ts && typedoc",
35
35
  "preview": "vite preview",
36
36
  "prebuild": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
37
37
  "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths",
@@ -63,6 +63,7 @@
63
63
  "mitata": "^1.0.34",
64
64
  "reactive-framework-test-suite": "^0.0.2",
65
65
  "tsc-alias": "^1.8.17",
66
+ "typedoc": "^0.28.19",
66
67
  "typescript": "^6.0.3",
67
68
  "vite": "^7.3.3",
68
69
  "vite-node": "^5.3.0",