creo 0.0.3-dev → 0.0.4-dev

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 (48) hide show
  1. package/.env.development +1 -0
  2. package/.github/workflows/main.yml +24 -0
  3. package/README.md +1 -1
  4. package/TODOS.md +2 -0
  5. package/index.ts +1 -0
  6. package/package.json +7 -2
  7. package/src/DOM/Context.ts +36 -0
  8. package/src/DOM/DomEngine.ts +106 -0
  9. package/src/DOM/IRenderCycle.ts +9 -0
  10. package/src/DOM/Key.ts +1 -0
  11. package/src/DOM/Node.ts +472 -0
  12. package/src/DOM/Registry.ts +53 -0
  13. package/src/creo.ts +134 -0
  14. package/src/data-structures/assert/assert.ts +12 -0
  15. package/src/data-structures/indexed-map/IndexedMap.ts +281 -0
  16. package/src/data-structures/linked-map/LinkedMap.spec.ts +67 -0
  17. package/src/data-structures/linked-map/LinkedMap.ts +198 -0
  18. package/src/data-structures/list/List.spec.ts +181 -0
  19. package/src/data-structures/list/List.ts +195 -0
  20. package/src/data-structures/maybe/Maybe.ts +25 -0
  21. package/src/data-structures/null/null.ts +3 -0
  22. package/src/{tools/isRecordLike.spec.ts → data-structures/record/IsRecordLike.spec.ts} +1 -1
  23. package/src/{tools/isRecordLike.ts → data-structures/record/IsRecordLike.ts} +1 -1
  24. package/src/{record/record.spec.ts → data-structures/record/Record.spec.ts} +96 -2
  25. package/src/data-structures/record/Record.ts +145 -0
  26. package/src/data-structures/shalllowEqual/shallowEqual.ts +26 -0
  27. package/src/data-structures/simpleKey/simpleKey.ts +8 -0
  28. package/src/data-structures/wildcard/wildcard.ts +1 -0
  29. package/src/examples/SimpleTodoList/SimpleTodoList.ts +53 -0
  30. package/src/globals.d.ts +1 -0
  31. package/src/main.ts +22 -11
  32. package/src/style.css +24 -79
  33. package/src/ui/html/Block.ts +10 -0
  34. package/src/ui/html/Button.ts +12 -0
  35. package/src/ui/html/HStack.ts +10 -0
  36. package/src/ui/html/Inline.ts +12 -0
  37. package/src/ui/html/List.ts +10 -0
  38. package/src/ui/html/Text.ts +9 -0
  39. package/src/ui/html/VStack.ts +11 -0
  40. package/tsconfig.json +2 -2
  41. package/vite.config.js +10 -0
  42. package/bun.lockb +0 -0
  43. package/src/record/record.ts +0 -101
  44. package/src/tools/optional.ts +0 -25
  45. package/src/ui/component.ts +0 -1
  46. package/src/ui/prop.ts +0 -13
  47. package/src/ui/state.ts +0 -0
  48. /package/src/{ui/index.ts → examples/simple.ts} +0 -0
@@ -0,0 +1,53 @@
1
+ import { LinkedMap } from "../data-structures/linked-map/LinkedMap";
2
+ import { Maybe } from "../data-structures/maybe/Maybe";
3
+ import { IRenderCycle } from "./IRenderCycle";
4
+ import { Node } from "./Node";
5
+
6
+ export class Registry implements IRenderCycle {
7
+ protected needRender: LinkedMap<Node, "key"> = new LinkedMap("key");
8
+ protected rendering: LinkedMap<Node, "key"> = new LinkedMap("key");
9
+ protected register: LinkedMap<Node, "key"> = new LinkedMap("key");
10
+ protected mounting: LinkedMap<Node, "key"> = new LinkedMap("key");
11
+
12
+ public newNode(node: Node) {
13
+ this.register.put(node);
14
+ this.needRender.put(node);
15
+ this.mounting.put(node);
16
+ }
17
+
18
+ public willRender(node: Node) {
19
+ this.needRender.putFirst(node);
20
+ this.rendering.delete(node.key);
21
+ }
22
+
23
+ public isRendering(node: Node) {
24
+ this.needRender.delete(node.key);
25
+ this.rendering.put(node);
26
+ }
27
+
28
+ public didRender(node: Node): { justMounted: boolean } {
29
+ this.rendering.delete(node.key);
30
+ return {
31
+ justMounted: this.mounting.delete(node.key),
32
+ };
33
+ }
34
+
35
+ public shouldUpdateNode(node: Node): boolean {
36
+ return this.needRender.has(node);
37
+ }
38
+
39
+ public dispose(node: Node) {
40
+ this.register.delete(node.key);
41
+ this.rendering.delete(node.key);
42
+ this.mounting.delete(node.key);
43
+ this.needRender.delete(node.key);
44
+ }
45
+
46
+ public getNextToRender(): Maybe<Node> {
47
+ return this.needRender.at(0);
48
+ }
49
+
50
+ public getNextRendering(): Maybe<Node> {
51
+ return this.rendering.at(-1);
52
+ }
53
+ }
package/src/creo.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { assertJust } from "./data-structures/assert/assert";
2
+ import { Maybe } from "./data-structures/maybe/Maybe";
3
+ import { Wildcard } from "./data-structures/wildcard/wildcard";
4
+ import { Context } from "./DOM/Context";
5
+ import { DomEngine, getActiveEngine } from "./DOM/DomEngine";
6
+ import { Key } from "./DOM/Key";
7
+ import { UINode } from "./DOM/Node";
8
+
9
+ export type NodeMethods<P = void, A = void> = {
10
+ render(): void;
11
+ didMount?(): void;
12
+ didUpdate?(): void;
13
+ dispose?(): void;
14
+ // Return true if the component should get updated with new params
15
+ shouldUpdate?(pendingParams: P): boolean;
16
+ ext?: A extends void ? undefined : A;
17
+ };
18
+
19
+ export type NodeBuilder<P = void, A = void> = (
20
+ c: Context<P>,
21
+ ) => NodeMethods<P, A>;
22
+
23
+ type NodeBuilderFn<P = void, A = void> = (
24
+ p?: P,
25
+ slot?: () => void,
26
+ ) => { ext: A };
27
+
28
+ export function creo<P = void, A = void>(
29
+ ctor: NodeBuilder<P, A>,
30
+ ): NodeBuilderFn<P, A> {
31
+ return (params?: P, slot?: () => void) => {
32
+ // Get Potential pre-existing instance of the component:
33
+ const maybeLayout = getActiveEngine();
34
+ assertJust(
35
+ maybeLayout,
36
+ "Component can be initialised only inside creo rendering cycle",
37
+ );
38
+ const engine: DomEngine = maybeLayout;
39
+
40
+ // 2. Check if there is an existing component
41
+ let userKey: Maybe<Key>;
42
+ // If key is provided separately, use provided key:
43
+ if (
44
+ params != null &&
45
+ typeof params === "object" &&
46
+ "key" in params &&
47
+ params.key != null &&
48
+ (typeof params.key === "string" || typeof params.key === "number")
49
+ ) {
50
+ userKey = params.key;
51
+ }
52
+
53
+ // 3. Get component's parent
54
+ const maybeParent = engine.getParent();
55
+ assertJust(
56
+ maybeParent,
57
+ "There is no rendering context for currently rendering component",
58
+ );
59
+ const parent = maybeParent;
60
+ const node = parent.renderChild(userKey, ctor, params, slot, null);
61
+
62
+ // 4. Public component contains all the required methods
63
+ return node.publicApi;
64
+ };
65
+ }
66
+
67
+ // UI renderers
68
+ type UINodeBuilderFn<P = void> = (
69
+ p?: P,
70
+ slot?: Maybe<() => void>,
71
+ ) => () => Maybe<HTMLElement>;
72
+
73
+ type UITextNodeBuilderFn<P = void> = (
74
+ p?: P,
75
+ slot?: Maybe<() => void>,
76
+ ) => () => Maybe<Text>;
77
+
78
+ function creoUI<P = void, Tag extends Maybe<string> = string>(
79
+ tag: Tag,
80
+ ): Tag extends "Text" ? UITextNodeBuilderFn<P> : UINodeBuilderFn<P> {
81
+ // @ts-ignore
82
+ return (params?: P, slot?: Maybe<() => void>) => {
83
+ // Get Potential pre-existing instance of the component:
84
+ const maybeLayout = getActiveEngine();
85
+ assertJust(
86
+ maybeLayout,
87
+ "Component can be initialised only inside creo rendering cycle",
88
+ );
89
+ const engine: DomEngine = maybeLayout;
90
+
91
+ // 2. Check if there is an existing component
92
+ let userKey: Maybe<Key>;
93
+ // If key is provided separately, use provided key:
94
+ if (
95
+ params != null &&
96
+ typeof params === "object" &&
97
+ "key" in params &&
98
+ params.key != null &&
99
+ (typeof params.key === "string" || typeof params.key === "number")
100
+ ) {
101
+ userKey = params.key;
102
+ }
103
+
104
+ // 3. Get component's parent
105
+ const maybeParent = engine.getParent();
106
+ assertJust(
107
+ maybeParent,
108
+ "There is no rendering context for currently rendering component",
109
+ );
110
+ const parent = maybeParent;
111
+ // TODO: Use separate methods to render Text and UI components
112
+ const node: UINode = parent.renderChild(
113
+ userKey,
114
+ uiCtor,
115
+ params,
116
+ slot,
117
+ tag,
118
+ ) as UINode;
119
+ // 4. Public component contains all the required methods
120
+ return node.publicNode;
121
+ };
122
+ }
123
+
124
+ const uiCtor = <P>(c: Context<P>) => ({
125
+ render() {
126
+ c.slot?.();
127
+ },
128
+ });
129
+
130
+ export const ui = creoUI;
131
+ export const Button = ui<Wildcard>("button");
132
+ export const Block = ui<Wildcard>("div");
133
+ export const Inline = ui<Wildcard>("span");
134
+ export const Text = ui<string>("text");
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Assert helpers for ts
3
+ *
4
+ */
5
+
6
+ import { isNone, Just, Maybe } from "../maybe/Maybe";
7
+
8
+ export function assertJust<T>(maybe: Maybe<T>, errorMessage?: string): asserts maybe is Just<T> {
9
+ if (isNone(maybe)) {
10
+ throw new Error(errorMessage ?? 'Expected Just, received None as Maybe');
11
+ }
12
+ }
@@ -0,0 +1,281 @@
1
+ import { Maybe } from "../maybe/Maybe";
2
+
3
+ type LinkNode<T, K> = {
4
+ value: T;
5
+ cachedIndexValues: Map<keyof T, T[keyof T]>;
6
+ prev: Maybe<LinkNode<T, K>>;
7
+ next: Maybe<LinkNode<T, K>>;
8
+ };
9
+
10
+ // Notifies when item gets added to a particular index
11
+ type IndexAddedSubscriber<T> = (val: T[keyof T]) => void;
12
+
13
+ export class IndexedMap<T extends object, K extends keyof T>
14
+ implements Iterable<T>
15
+ {
16
+ private head: Maybe<LinkNode<T, K>>;
17
+ private tail: Maybe<LinkNode<T, K>>;
18
+ private mapSize = 0;
19
+ private pk: K;
20
+ private map = new Map<T[K], LinkNode<T, K>>();
21
+ private indexes: Map<keyof T, Map<T[keyof T], Set<LinkNode<T, K>>>> =
22
+ new Map();
23
+ private indexAddedSubscribers: Map<keyof T, Set<IndexAddedSubscriber<T>>> =
24
+ new Map();
25
+
26
+ constructor(pk: K, indexFields: Array<keyof T> = []) {
27
+ this.pk = pk;
28
+ for (const field of indexFields) {
29
+ if (field === pk) continue;
30
+ this.indexes.set(field, new Map());
31
+ }
32
+ }
33
+
34
+ // Puts item to the end
35
+ // Removes previous item, if PK is matched
36
+ put(item: T): void {
37
+ const key = item[this.pk];
38
+ // Delete previous, if any
39
+ this.delete(key);
40
+
41
+ const node: LinkNode<T, K> = {
42
+ value: item,
43
+ cachedIndexValues: new Map(),
44
+ prev: this.tail,
45
+ next: null,
46
+ };
47
+ if (!this.head) {
48
+ this.head = node;
49
+ }
50
+ if (this.tail) {
51
+ this.tail.next = node;
52
+ }
53
+ this.tail = node;
54
+ this.map.set(key, node);
55
+ this.mapSize++;
56
+
57
+ // update secondary indexes
58
+ this.indexNewNode(node);
59
+ }
60
+
61
+ putBefore(targetKey: T[K], item: T): void {
62
+ const target = this.map.get(targetKey);
63
+ if (target == null) {
64
+ throw new Error(`Key not found: ${String(targetKey)}`);
65
+ }
66
+ const key = item[this.pk];
67
+ if (targetKey === key) {
68
+ return;
69
+ }
70
+ this.delete(key);
71
+
72
+ const node: LinkNode<T, K> = {
73
+ value: item,
74
+ cachedIndexValues: new Map(),
75
+ prev: target.prev,
76
+ next: target,
77
+ };
78
+ if (target.prev) {
79
+ target.prev.next = node;
80
+ } else {
81
+ this.head = node;
82
+ }
83
+ target.prev = node;
84
+
85
+ this.map.set(key, node);
86
+ this.mapSize++;
87
+ this.indexNewNode(node);
88
+ }
89
+
90
+ // TODO: need to make it in background, instead of making eng to call it manually
91
+ updateIndex(item: T) {
92
+ const node = this.map.get(item[this.pk]);
93
+ if (node == null) {
94
+ return;
95
+ }
96
+ for (const [field, map] of this.indexes) {
97
+ const newVal = node.value[field];
98
+ const oldVal = node.cachedIndexValues.get(field);
99
+ if (oldVal !== newVal) {
100
+ const oldSet = this.indexes
101
+ .get(field)
102
+ // We know for sure, it's okay to index on that value
103
+ ?.get(oldVal as T[keyof T]);
104
+
105
+ oldSet?.delete(node);
106
+ let set = this.indexes.get(field)?.get(newVal);
107
+ if (!set) {
108
+ set = new Set();
109
+ map.set(newVal, set);
110
+ }
111
+ set.add(node);
112
+ node.cachedIndexValues.set(field, newVal);
113
+ this.notifySubscribers(field, newVal);
114
+ }
115
+ }
116
+ }
117
+
118
+ private indexNewNode(node: LinkNode<T, K>) {
119
+ for (const [field, map] of this.indexes) {
120
+ const val = node.value[field];
121
+ node.cachedIndexValues.set(field, val);
122
+ let set = map.get(val);
123
+ if (!set) {
124
+ set = new Set();
125
+ map.set(val, set);
126
+ }
127
+ set.add(node);
128
+ this.notifySubscribers(field, val);
129
+ }
130
+ }
131
+
132
+ private notifySubscribers(field: keyof T, val: T[keyof T] | undefined) {
133
+ if (val === undefined) {
134
+ return;
135
+ }
136
+ const listeners = this.indexAddedSubscribers.get(field);
137
+ if (!listeners) {
138
+ return;
139
+ }
140
+ for (const listener of listeners) {
141
+ listener(val);
142
+ }
143
+ }
144
+
145
+ subscribeToIndexChange(
146
+ field: keyof T,
147
+ listener: IndexAddedSubscriber<T>,
148
+ ): () => void {
149
+ if (!this.indexes.has(field)) {
150
+ throw new Error(
151
+ `Cannot subscribe to non-indexed field "${String(field)}"`,
152
+ );
153
+ }
154
+ let listeners = this.indexAddedSubscribers.get(field);
155
+ if (!listeners) {
156
+ listeners = new Set();
157
+ this.indexAddedSubscribers.set(field, listeners);
158
+ }
159
+ listeners.add(listener);
160
+ return () => {
161
+ listeners.delete(listener);
162
+ };
163
+ }
164
+
165
+ putAfter(targetKey: T[K], item: T): void {
166
+ const target = this.map.get(targetKey);
167
+ if (target == null) {
168
+ throw new Error(`Key not found: ${String(targetKey)}`);
169
+ }
170
+ const key = item[this.pk];
171
+ if (targetKey === key) {
172
+ return;
173
+ }
174
+
175
+ const node: LinkNode<T, K> = {
176
+ value: item,
177
+ cachedIndexValues: new Map(),
178
+ prev: target,
179
+ next: target.next,
180
+ };
181
+ if (target.next) {
182
+ target.next.prev = node;
183
+ } else {
184
+ this.tail = node;
185
+ }
186
+ target.next = node;
187
+
188
+ this.map.set(key, node);
189
+ this.mapSize++;
190
+ this.indexNewNode(node);
191
+ }
192
+
193
+ delete(key: T[K]): boolean {
194
+ const node = this.map.get(key);
195
+ if (node == null) {
196
+ return false;
197
+ }
198
+
199
+ // unlink from list
200
+ if (node.prev) {
201
+ node.prev.next = node.next;
202
+ } else {
203
+ this.head = node.next;
204
+ }
205
+ if (node.next) {
206
+ node.next.prev = node.prev;
207
+ } else {
208
+ this.tail = node.prev;
209
+ }
210
+
211
+ // remove from indexes
212
+ this.map.delete(key);
213
+ for (const [field, map] of this.indexes) {
214
+ const indexValue = node.value[field];
215
+ const set = map.get(indexValue);
216
+ if (set != null) {
217
+ set.delete(node);
218
+ }
219
+ }
220
+
221
+ this.mapSize--;
222
+ return true;
223
+ }
224
+
225
+ at(n: number): Maybe<T> {
226
+ let current: Maybe<LinkNode<T, K>>;
227
+ if (n >= 0) {
228
+ current = this.head;
229
+ for (let i = 0; i < n && current != null; i++) {
230
+ current = current.next;
231
+ }
232
+ } else {
233
+ current = this.tail;
234
+ for (let i = n; i < -1 && current != null; i++) {
235
+ current = current.prev;
236
+ }
237
+ }
238
+ return current?.value;
239
+ }
240
+
241
+ get(key: T[K]): Maybe<T> {
242
+ return this.map.get(key)?.value;
243
+ }
244
+
245
+ getByIndex<K extends keyof T>(field: K, val: T[keyof T]): Array<T> {
246
+ const map = this.indexes.get(field);
247
+ if (!map) {
248
+ throw new Error(`No index defined for field "${String(field)}"`);
249
+ }
250
+ const set = map.get(val);
251
+ return set ? Array.from(set.values(), (node) => node.value) : [];
252
+ }
253
+
254
+ // Get next item in insertion order
255
+ getNext(item: T): Maybe<T> {
256
+ return this.map.get(item[this.pk])?.next?.value;
257
+ }
258
+
259
+ // Get previous item in insertion order
260
+ getPrev(item: T): Maybe<T> {
261
+ return this.map.get(item[this.pk])?.prev?.value;
262
+ }
263
+
264
+ size(): number {
265
+ return this.mapSize;
266
+ }
267
+
268
+ [Symbol.iterator](): Iterator<T> {
269
+ let curr = this.head;
270
+ return {
271
+ next(): IteratorResult<T> {
272
+ if (!curr) {
273
+ return { done: true, value: undefined! };
274
+ }
275
+ const value = curr.value;
276
+ curr = curr.next;
277
+ return { done: false, value };
278
+ },
279
+ };
280
+ }
281
+ }
@@ -0,0 +1,67 @@
1
+ import { expect, test } from "bun:test";
2
+ import { LinkedMap } from "./LinkedMap";
3
+
4
+ type Entry = { key: string; value: number };
5
+
6
+ test("put adds items to the end", () => {
7
+ const map = new LinkedMap<Entry, "key">("key");
8
+ map.put({ key: "foo", value: 1 });
9
+ map.put({ key: "bar", value: 2 });
10
+ map.put({ key: "baz", value: 94 });
11
+
12
+ expect([...map].map((e) => [e.key, e.value])).toEqual([
13
+ ["foo", 1],
14
+ ["bar", 2],
15
+ ["baz", 94],
16
+ ]);
17
+ });
18
+
19
+ test("putBefore and putAfter work correctly", () => {
20
+ const map = new LinkedMap<Entry, "key">("key");
21
+ map.put({ key: "foo", value: 1 });
22
+ map.put({ key: "bar", value: 2 });
23
+ map.put({ key: "baz", value: 94 });
24
+
25
+ map.putBefore("bar", { key: "hey", value: 13 }); // insert before "bar"
26
+
27
+ expect([...map].map((e) => [e.key, e.value])).toEqual([
28
+ ["foo", 1],
29
+ ["hey", 13],
30
+ ["bar", 2],
31
+ ["baz", 94],
32
+ ]);
33
+
34
+ map.putAfter("baz", { key: "zap", value: 42 }); // insert after "baz"
35
+
36
+ expect([...map].map((e) => [e.key, e.value])).toEqual([
37
+ ["foo", 1],
38
+ ["hey", 13],
39
+ ["bar", 2],
40
+ ["baz", 94],
41
+ ["zap", 42],
42
+ ]);
43
+ });
44
+
45
+ test("map has access by key", () => {
46
+ const map = new LinkedMap<Entry, "key">("key");
47
+ map.put({ key: "foo", value: 1 });
48
+ map.put({ key: "bar", value: 2 });
49
+ map.put({ key: "hey", value: 13 });
50
+ map.put({ key: "baz", value: 94 });
51
+
52
+ expect(map.get("baz")?.value).toEqual(94);
53
+ expect(map.get("bar")?.value).toEqual(2);
54
+ expect(map.get("hey")?.value).toEqual(13);
55
+ });
56
+
57
+ test("map has access by index", () => {
58
+ const map = new LinkedMap<Entry, "key">("key");
59
+ map.put({ key: "foo", value: 1 });
60
+ map.put({ key: "bar", value: 2 });
61
+ map.put({ key: "hey", value: 13 });
62
+ map.put({ key: "baz", value: 94 });
63
+
64
+ expect(map.at(-1)?.value).toEqual(94);
65
+ expect(map.at(2)?.value).toEqual(13);
66
+ expect(map.at(0)?.value).toEqual(1);
67
+ });