creo 0.0.2 → 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.
- package/.env.development +1 -0
- package/.github/workflows/main.yml +24 -0
- package/README.md +1 -52
- package/TODOS.md +2 -0
- package/index.html +13 -0
- package/index.ts +1 -0
- package/package.json +12 -44
- package/src/DOM/Context.ts +36 -0
- package/src/DOM/DomEngine.ts +106 -0
- package/src/DOM/IRenderCycle.ts +9 -0
- package/src/DOM/Key.ts +1 -0
- package/src/DOM/Node.ts +472 -0
- package/src/DOM/Registry.ts +53 -0
- package/src/creo.ts +134 -0
- package/src/data-structures/assert/assert.ts +12 -0
- package/src/data-structures/indexed-map/IndexedMap.ts +281 -0
- package/src/data-structures/linked-map/LinkedMap.spec.ts +67 -0
- package/src/data-structures/linked-map/LinkedMap.ts +198 -0
- package/src/data-structures/list/List.spec.ts +181 -0
- package/src/data-structures/list/List.ts +195 -0
- package/src/data-structures/maybe/Maybe.ts +25 -0
- package/src/data-structures/null/null.ts +3 -0
- package/src/data-structures/record/IsRecordLike.spec.ts +29 -0
- package/src/data-structures/record/IsRecordLike.ts +3 -0
- package/src/data-structures/record/Record.spec.ts +240 -0
- package/src/data-structures/record/Record.ts +145 -0
- package/src/data-structures/shalllowEqual/shallowEqual.ts +26 -0
- package/src/data-structures/simpleKey/simpleKey.ts +8 -0
- package/src/data-structures/wildcard/wildcard.ts +1 -0
- package/src/examples/SimpleTodoList/SimpleTodoList.ts +53 -0
- package/src/examples/simple.ts +0 -0
- package/src/globals.d.ts +1 -0
- package/src/main.ts +24 -0
- package/src/style.css +41 -0
- package/src/ui/html/Block.ts +10 -0
- package/src/ui/html/Button.ts +12 -0
- package/src/ui/html/HStack.ts +10 -0
- package/src/ui/html/Inline.ts +12 -0
- package/src/ui/html/List.ts +10 -0
- package/src/ui/html/Text.ts +9 -0
- package/src/ui/html/VStack.ts +11 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +23 -0
- package/vite.config.js +10 -0
- package/LICENSE +0 -21
- package/dist/index.cjs +0 -54
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -19
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -51
- package/dist/index.mjs.map +0 -1
- package/dist/index.umd.js +0 -61
- package/dist/index.umd.js.map +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { List, ListNode } from "./List";
|
|
3
|
+
import { assertJust } from "../assert/assert";
|
|
4
|
+
|
|
5
|
+
test("addToEnd adds items to the end", () => {
|
|
6
|
+
const listInstance = List();
|
|
7
|
+
listInstance.addToEnd('1');
|
|
8
|
+
listInstance.addToEnd('2');
|
|
9
|
+
listInstance.addToEnd('3');
|
|
10
|
+
listInstance.addToEnd('4');
|
|
11
|
+
listInstance.addToEnd('5');
|
|
12
|
+
listInstance.addToEnd('6');
|
|
13
|
+
|
|
14
|
+
expect(listInstance.at(0)?.value).toBe('1');
|
|
15
|
+
expect(listInstance.at(1)?.value).toBe('2');
|
|
16
|
+
expect(listInstance.at(3)?.value).toBe('4');
|
|
17
|
+
expect(listInstance.at(5)?.value).toBe('6');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("at works with negative values", () => {
|
|
21
|
+
const l = List();
|
|
22
|
+
l.addToEnd('1');
|
|
23
|
+
l.addToEnd('2');
|
|
24
|
+
l.addToEnd('3');
|
|
25
|
+
l.addToEnd('4');
|
|
26
|
+
l.addToEnd('5');
|
|
27
|
+
l.addToEnd('6');
|
|
28
|
+
|
|
29
|
+
expect(l.at(-1)?.value).toBe('6');
|
|
30
|
+
expect(l.at(-5)?.value).toBe('2');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("if at exceeds the length, null to be returned", () => {
|
|
34
|
+
const listInstance = List();
|
|
35
|
+
listInstance.addToEnd('1');
|
|
36
|
+
listInstance.addToEnd('2');
|
|
37
|
+
listInstance.addToEnd('3');
|
|
38
|
+
listInstance.addToEnd('4');
|
|
39
|
+
listInstance.addToEnd('5');
|
|
40
|
+
listInstance.addToEnd('6');
|
|
41
|
+
|
|
42
|
+
expect(listInstance.at(-10)).toBe(null);
|
|
43
|
+
expect(listInstance.at(10)).toBe(null);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("Add to start", () => {
|
|
47
|
+
const listInstance = List();
|
|
48
|
+
listInstance.addToStart('1');
|
|
49
|
+
listInstance.addToStart('2');
|
|
50
|
+
listInstance.addToStart('3');
|
|
51
|
+
listInstance.addToStart('4');
|
|
52
|
+
listInstance.addToStart('5');
|
|
53
|
+
listInstance.addToStart('6');
|
|
54
|
+
|
|
55
|
+
expect(listInstance.at(0)?.value).toBe('6');
|
|
56
|
+
expect(listInstance.at(1)?.value).toBe('5');
|
|
57
|
+
expect(listInstance.at(2)?.value).toBe('4');
|
|
58
|
+
expect(listInstance.at(-3)?.value).toBe('3');
|
|
59
|
+
expect(listInstance.at(-2)?.value).toBe('2');
|
|
60
|
+
expect(listInstance.at(-1)?.value).toBe('1');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("from is a static method which builds a list from an array", () => {
|
|
64
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
65
|
+
|
|
66
|
+
expect(listInstance.at(-1)?.value).toBe('6');
|
|
67
|
+
expect(listInstance.at(-2)?.value).toBe('5');
|
|
68
|
+
expect(listInstance.at(-3)?.value).toBe('4');
|
|
69
|
+
expect(listInstance.at(2)?.value).toBe('3');
|
|
70
|
+
expect(listInstance.at(1)?.value).toBe('2');
|
|
71
|
+
expect(listInstance.at(0)?.value).toBe('1');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("Iterable", () => {
|
|
75
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
76
|
+
|
|
77
|
+
let resultString = ''
|
|
78
|
+
for (const item of listInstance) {
|
|
79
|
+
resultString += item;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
expect(resultString).toBe('123456');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("Mutating ListNode directly keeps List in correct state", () => {
|
|
86
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
87
|
+
|
|
88
|
+
const listNode: ListNode<string> = listInstance.at(2)!;
|
|
89
|
+
|
|
90
|
+
expect(listNode.value).toBe('3');
|
|
91
|
+
|
|
92
|
+
listNode.prev = '10';
|
|
93
|
+
|
|
94
|
+
expect(listInstance.at(2)?.value).toBe('10');
|
|
95
|
+
expect(listInstance.at(3)?.value).toBe('3');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("Mutating ListNode.prev directly keeps List in correct state", () => {
|
|
99
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
100
|
+
|
|
101
|
+
const listNode: ListNode<string> = listInstance.at(2)!;
|
|
102
|
+
|
|
103
|
+
expect(listNode.value).toBe('3');
|
|
104
|
+
|
|
105
|
+
listNode.prev = '10';
|
|
106
|
+
|
|
107
|
+
expect(Array.from(listInstance).join('')).toBe('12103456');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("Mutating ListNode.next directly keeps List in correct state", () => {
|
|
111
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
112
|
+
|
|
113
|
+
const listNode: ListNode<string> = listInstance.at(2)!;
|
|
114
|
+
|
|
115
|
+
expect(listNode.value).toBe('3');
|
|
116
|
+
|
|
117
|
+
listNode.next = '10';
|
|
118
|
+
|
|
119
|
+
expect(Array.from(listInstance).join('')).toBe('12310456');
|
|
120
|
+
expect(listInstance.at(-3)?.value).toBe('4');
|
|
121
|
+
expect(listInstance.at(-4)?.value).toBe('10');
|
|
122
|
+
expect(listInstance.at(-5)?.value).toBe('3');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("Mutating the first item using ListNode directly keeps List in correct state", () => {
|
|
126
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
127
|
+
|
|
128
|
+
listInstance.at(0)!.prev = 'test'
|
|
129
|
+
|
|
130
|
+
expect(Array.from(listInstance).join('')).toBe('test123456');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("Mutating the last item using ListNode directly keeps List in correct state", () => {
|
|
134
|
+
const listInstance: List<string> = List.from(['1','2','3','4','5','6']);
|
|
135
|
+
|
|
136
|
+
listInstance.at(-1)!.next = 'test'
|
|
137
|
+
|
|
138
|
+
expect(Array.from(listInstance).join('')).toBe('123456test');
|
|
139
|
+
expect(listInstance.at(-1)?.value).toBe('test');
|
|
140
|
+
expect(listInstance.at(-2)?.value).toBe('6');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('Mutating item in a middle keeps the list correct', () => {
|
|
144
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
145
|
+
|
|
146
|
+
listInstance.at(-4)!.next = 'test'
|
|
147
|
+
|
|
148
|
+
expect(Array.from(listInstance).join('')).toBe('123test456');
|
|
149
|
+
expect(listInstance.at(-2)?.value).toBe('5');
|
|
150
|
+
expect(listInstance.at(-4)?.value).toBe('test');
|
|
151
|
+
expect(listInstance.at(-5)?.value).toBe('3');
|
|
152
|
+
expect(listInstance.at(5)?.value).toBe('5');
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('First item deletion works correctly', () => {
|
|
156
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
157
|
+
|
|
158
|
+
listInstance.at(0)!.delete();
|
|
159
|
+
|
|
160
|
+
expect(Array.from(listInstance).join('')).toBe('23456');
|
|
161
|
+
expect(listInstance.at(0)?.value).toBe('2');
|
|
162
|
+
expect(listInstance.at(1)?.value).toBe('3');
|
|
163
|
+
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('addTo returns node', () => {
|
|
167
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
168
|
+
|
|
169
|
+
expect(listInstance.addToEnd('test')).toBe(listInstance.at(-1));
|
|
170
|
+
expect(listInstance.addToStart('test2')).toBe(listInstance.at(0));
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('Adding item in the middle of the list', () => {
|
|
174
|
+
const listInstance = List.from(['1','2','3','4','5','6']);
|
|
175
|
+
const node = listInstance.at(2);
|
|
176
|
+
assertJust(node);
|
|
177
|
+
expect(node.value).toBe('3');
|
|
178
|
+
node.next = '123';
|
|
179
|
+
|
|
180
|
+
expect([...listInstance]).toEqual(['1','2','3', '123','4','5','6'])
|
|
181
|
+
})
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linked list impl
|
|
3
|
+
*
|
|
4
|
+
* ideas:
|
|
5
|
+
* [-] Shall we use Record for list as well?
|
|
6
|
+
* [x] Support iterator
|
|
7
|
+
* [-] Make prev and next to receive ListNode
|
|
8
|
+
* [ ] Size support
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Maybe } from "../maybe/Maybe";
|
|
12
|
+
|
|
13
|
+
// #region List's Node
|
|
14
|
+
export class ListNode<T> {
|
|
15
|
+
#list: Maybe<WeakRef<ListClass<T>>>;
|
|
16
|
+
#next: Maybe<ListNode<T>>;
|
|
17
|
+
#prev: Maybe<ListNode<T>>;
|
|
18
|
+
public node: T;
|
|
19
|
+
|
|
20
|
+
constructor(node: T, prev: Maybe<ListNode<T>> = null, next: Maybe<ListNode<T>> = null, list: Maybe<WeakRef<ListClass<T>>>) {
|
|
21
|
+
this.#prev = prev;
|
|
22
|
+
this.#next = next;
|
|
23
|
+
this.node = node;
|
|
24
|
+
this.#list = list;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// #region Delete Node from LinkedList
|
|
28
|
+
delete() {
|
|
29
|
+
const maybeParent = this.#list?.deref();
|
|
30
|
+
if (this.#prev != null) {
|
|
31
|
+
this.#prev.#next = this.#next;
|
|
32
|
+
} else {
|
|
33
|
+
maybeParent?.updateHead_UNSAFE(this.#next);
|
|
34
|
+
}
|
|
35
|
+
if (this.#next != null) {
|
|
36
|
+
this.#next.#prev = this.#prev;
|
|
37
|
+
} else {
|
|
38
|
+
maybeParent?.updateTail_UNSAFE(this.#prev);
|
|
39
|
+
}
|
|
40
|
+
this.#next = null;
|
|
41
|
+
this.#prev = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// #region Getters/setters
|
|
45
|
+
set next(value: T) {
|
|
46
|
+
const oldNext = this.#next;
|
|
47
|
+
this.#next = new ListNode(value, this, this.#next, this.#list);
|
|
48
|
+
|
|
49
|
+
if (oldNext == null) {
|
|
50
|
+
const maybeParent = this.#list?.deref();
|
|
51
|
+
if (!maybeParent) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
maybeParent.updateTail_UNSAFE(this.#next);
|
|
55
|
+
} else {
|
|
56
|
+
oldNext.#prev = this.#next;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
set prev(value: T) {
|
|
61
|
+
const oldPrev = this.#prev;
|
|
62
|
+
this.#prev = new ListNode(value, this.#prev, this, this.#list);
|
|
63
|
+
|
|
64
|
+
if (oldPrev == null) {
|
|
65
|
+
const maybeParent = this.#list?.deref();
|
|
66
|
+
if (!maybeParent) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
maybeParent.updateHead_UNSAFE(this.#prev);
|
|
70
|
+
} else {
|
|
71
|
+
oldPrev.#next = this.#prev;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get value(): T {
|
|
76
|
+
return this.node;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get next(): Maybe<ListNode<T>> {
|
|
80
|
+
return this.#next;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get prev(): Maybe<ListNode<T>> {
|
|
84
|
+
return this.#prev;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// #region Getter of List
|
|
88
|
+
get list() {
|
|
89
|
+
return this.#list?.deref();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// #region IList public interface
|
|
94
|
+
export interface List<T> extends Iterable<T> {
|
|
95
|
+
addToStart(value: T): ListNode<T>;
|
|
96
|
+
delete(n: number): boolean;
|
|
97
|
+
at(n: number): Maybe<ListNode<T>>;
|
|
98
|
+
addToEnd(value: T): ListNode<T>;
|
|
99
|
+
[Symbol.iterator](): IterableIterator<T>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
// #region List class
|
|
104
|
+
class ListClass<T> implements List<T>{
|
|
105
|
+
#head: Maybe<ListNode<T>>;
|
|
106
|
+
#tail: Maybe<ListNode<T>>;
|
|
107
|
+
|
|
108
|
+
// #region internal methods
|
|
109
|
+
updateHead_UNSAFE(maybeNewHead: Maybe<ListNode<T>>) {
|
|
110
|
+
this.#head = maybeNewHead;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
updateTail_UNSAFE(maybeNewTail: Maybe<ListNode<T>>) {
|
|
114
|
+
this.#tail = maybeNewTail;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// #region Public methods
|
|
118
|
+
addToStart(value: T) {
|
|
119
|
+
if (this.#head != null) {
|
|
120
|
+
this.#head.prev = value;
|
|
121
|
+
} else {
|
|
122
|
+
const node = new ListNode(value, null, null, new WeakRef(this));
|
|
123
|
+
this.#head = node;
|
|
124
|
+
this.#tail = node;
|
|
125
|
+
}
|
|
126
|
+
return this.#head
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
delete(n: number): boolean {
|
|
130
|
+
const node = this.at(n);
|
|
131
|
+
if (node == null) {
|
|
132
|
+
// Cannot delete non-existed item
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Corner case: delete the first item
|
|
137
|
+
if (node === this.#head) {
|
|
138
|
+
this.#head = this.at(1);
|
|
139
|
+
}
|
|
140
|
+
if (node === this.#tail) {
|
|
141
|
+
this.#tail = this.at(-2);
|
|
142
|
+
}
|
|
143
|
+
node.delete();
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
at(n: number): Maybe<ListNode<T>> {
|
|
148
|
+
let current: Maybe<ListNode<T>>;
|
|
149
|
+
if (n >= 0) {
|
|
150
|
+
current = this.#head;
|
|
151
|
+
for (let i = 0; i < n && current != null; i++) {
|
|
152
|
+
current = current.next;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
current = this.#tail;
|
|
156
|
+
for (let i = n; i < -1 && current != null; i++) {
|
|
157
|
+
current = current.prev;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return current;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
addToEnd(value: T) {
|
|
164
|
+
if (this.#tail != null) {
|
|
165
|
+
this.#tail.next = value;
|
|
166
|
+
} else {
|
|
167
|
+
const node = new ListNode(value, null, null, new WeakRef(this));
|
|
168
|
+
this.#head = node;
|
|
169
|
+
this.#tail = node;
|
|
170
|
+
}
|
|
171
|
+
return this.#tail;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
*[Symbol.iterator]() {
|
|
175
|
+
let current = this.#head;
|
|
176
|
+
while (current) {
|
|
177
|
+
yield current.value;
|
|
178
|
+
current = current.next;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static from<T>(arrayLike: Iterable<T>): List<T> {
|
|
183
|
+
const list = new ListClass<T>;
|
|
184
|
+
for (const item of arrayLike) {
|
|
185
|
+
list.addToEnd(item);
|
|
186
|
+
}
|
|
187
|
+
return list;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// #region Exports
|
|
191
|
+
export function List<T>(): List<T> {
|
|
192
|
+
return new ListClass<T>();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
List.from = ListClass.from;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type None = null | undefined;
|
|
2
|
+
export type Just<T> = T;
|
|
3
|
+
export type Maybe<T> = Just<T> | None;
|
|
4
|
+
|
|
5
|
+
export function isJust<T>(v: Maybe<T>): v is Just<T> {
|
|
6
|
+
return v != null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isNone<T>(v: Maybe<T>): v is None {
|
|
10
|
+
return v == null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function unwrap<T>(v: Maybe<T>): Just<T> {
|
|
14
|
+
if (isJust(v)) {
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
throw new TypeError("Optional is none");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function orDefault<T, K>(v: Maybe<T>, alternative: K) {
|
|
21
|
+
if (isJust(v)) {
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
return alternative;
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { isRecordLike } from "./IsRecordLike";
|
|
3
|
+
|
|
4
|
+
test("Handles objects", () => {
|
|
5
|
+
expect(isRecordLike({})).toBe(true);
|
|
6
|
+
expect(isRecordLike({ foo: "bar" })).toBe(true);
|
|
7
|
+
expect(isRecordLike(new String())).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("Handles arrays", () => {
|
|
11
|
+
expect(isRecordLike([])).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("Fails nulls", () => {
|
|
15
|
+
expect(isRecordLike(null)).toBe(false);
|
|
16
|
+
expect(isRecordLike(undefined)).toBe(false);
|
|
17
|
+
expect(isRecordLike(NaN)).toBe(false);
|
|
18
|
+
expect(isRecordLike(0)).toBe(false);
|
|
19
|
+
expect(isRecordLike(false)).toBe(false);
|
|
20
|
+
expect(isRecordLike("")).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("Fails primitives", () => {
|
|
24
|
+
expect(isRecordLike(1)).toBe(false);
|
|
25
|
+
expect(isRecordLike("foo")).toBe(false);
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
expect(isRecordLike()).toBe(false);
|
|
28
|
+
expect(isRecordLike(Symbol.for("test"))).toBe(false);
|
|
29
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { expect, test, mock } from "bun:test";
|
|
2
|
+
import { record, RecordOf, onDidUpdate, isRecord } from "./Record";
|
|
3
|
+
|
|
4
|
+
test("Can define objects", () => {
|
|
5
|
+
const obj = record({
|
|
6
|
+
hello: "world",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
expect(obj).toEqual({ hello: "world" });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("Refers to the same objects", () => {
|
|
14
|
+
const test = record({
|
|
15
|
+
foo: {
|
|
16
|
+
bar: "baz",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(test.foo).toBe(test.foo);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("Updates fields correctly", () => {
|
|
24
|
+
const test: RecordOf<any> = record({
|
|
25
|
+
foo: {
|
|
26
|
+
bar: "baz",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(test.foo.bar).toBe("baz");
|
|
31
|
+
|
|
32
|
+
test.foo.bar = "123";
|
|
33
|
+
|
|
34
|
+
expect(test.foo.bar).toBe("123");
|
|
35
|
+
|
|
36
|
+
test.foo = { hello: "world" };
|
|
37
|
+
expect(test.foo.bar).toBe(undefined);
|
|
38
|
+
expect(test.foo.hello).toBe("world");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("Notifies on object updates", async () => {
|
|
42
|
+
const obj = record({
|
|
43
|
+
hello: "world",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await expect(
|
|
47
|
+
new Promise((resolve) => {
|
|
48
|
+
onDidUpdate(obj, (updated) => resolve(updated));
|
|
49
|
+
|
|
50
|
+
obj.hello = "new world";
|
|
51
|
+
}),
|
|
52
|
+
).resolves.toEqual({
|
|
53
|
+
hello: "new world",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("Notifies on object updates even if the listener was set after the change", async () => {
|
|
58
|
+
const obj = record({
|
|
59
|
+
hello: "world",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
obj.hello = "new world";
|
|
63
|
+
|
|
64
|
+
await expect(
|
|
65
|
+
new Promise((resolve) => {
|
|
66
|
+
onDidUpdate(obj, (updated) => resolve(updated));
|
|
67
|
+
}),
|
|
68
|
+
).resolves.toEqual({
|
|
69
|
+
hello: "new world",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("Implies updates immediately", async () => {
|
|
74
|
+
const obj = record({
|
|
75
|
+
hello: {
|
|
76
|
+
world: "foo",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
expect(obj).toEqual({
|
|
82
|
+
hello: {
|
|
83
|
+
world: "foo",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
obj.hello.world = "new";
|
|
88
|
+
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
expect(obj).toEqual({
|
|
91
|
+
hello: {
|
|
92
|
+
world: "new",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("Handles nested object updates", async () => {
|
|
98
|
+
const obj = record({
|
|
99
|
+
hello: {
|
|
100
|
+
world: "foo",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
obj.hello.world = "new";
|
|
105
|
+
|
|
106
|
+
await expect(
|
|
107
|
+
new Promise((resolve) => {
|
|
108
|
+
onDidUpdate(obj, (updated) => resolve(updated));
|
|
109
|
+
}),
|
|
110
|
+
).resolves.toEqual({
|
|
111
|
+
hello: {
|
|
112
|
+
world: "new",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("Can unsubscribe from updates", async () => {
|
|
118
|
+
const obj = record({
|
|
119
|
+
hello: {
|
|
120
|
+
world: "foo",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const unsubscribe = onDidUpdate(obj, (_updated) => {
|
|
125
|
+
throw Error("cannot get here");
|
|
126
|
+
});
|
|
127
|
+
// If you delete this line, the test gets broken:
|
|
128
|
+
unsubscribe();
|
|
129
|
+
|
|
130
|
+
obj.hello.world = "new";
|
|
131
|
+
|
|
132
|
+
await expect(
|
|
133
|
+
new Promise((resolve) => {
|
|
134
|
+
onDidUpdate(obj, (updated) => resolve(updated));
|
|
135
|
+
}),
|
|
136
|
+
).resolves.toEqual({
|
|
137
|
+
hello: {
|
|
138
|
+
world: "new",
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("Supports arrays", async () => {
|
|
144
|
+
const obj = record({
|
|
145
|
+
hello: {
|
|
146
|
+
world: ["this", "is", "array"],
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
obj.hello.world.sort();
|
|
151
|
+
|
|
152
|
+
await expect(
|
|
153
|
+
new Promise((resolve) => {
|
|
154
|
+
onDidUpdate(obj, (updated) => resolve(JSON.stringify(updated)));
|
|
155
|
+
}),
|
|
156
|
+
).resolves.toEqual('{"hello":{"world":["array","is","this"]}}');
|
|
157
|
+
|
|
158
|
+
obj.hello.world.push("123");
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
new Promise((resolve) => {
|
|
162
|
+
onDidUpdate(obj, (updated) => resolve(JSON.stringify(updated)));
|
|
163
|
+
}),
|
|
164
|
+
).resolves.toEqual('{"hello":{"world":["array","is","this","123"]}}');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("Supports iterable", async () => {
|
|
168
|
+
const obj = record(["hello", "world"]);
|
|
169
|
+
|
|
170
|
+
function iterate(...args: string[]) {
|
|
171
|
+
const [a, b] = args;
|
|
172
|
+
expect(a).toBe("hello");
|
|
173
|
+
expect(b).toBe("world");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
iterate(...obj);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("`has` works with records", async () => {
|
|
180
|
+
const originalObject = {
|
|
181
|
+
foo: "bar",
|
|
182
|
+
baz: "test",
|
|
183
|
+
nested: {
|
|
184
|
+
support: "exist",
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const wrapped = record(originalObject);
|
|
188
|
+
|
|
189
|
+
expect(isRecord(originalObject)).toBe(false);
|
|
190
|
+
expect(isRecord(wrapped)).toBe(true);
|
|
191
|
+
expect("foo" in wrapped).toBe(true);
|
|
192
|
+
expect("test" in wrapped).toBe(false);
|
|
193
|
+
expect("support" in wrapped.nested).toBe(true);
|
|
194
|
+
expect("foo" in wrapped.nested).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("Double tracked on nested object works correctly", async () => {
|
|
198
|
+
const originalObject = {
|
|
199
|
+
foo: "bar",
|
|
200
|
+
baz: "test",
|
|
201
|
+
nested: {
|
|
202
|
+
support: "exist",
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// This object is not tracked under the same parent, but might be considered in future to allow better objects and record composition.
|
|
207
|
+
// It would require objects to have multiple parents (so essentially many<=>many concept.)
|
|
208
|
+
const additionalObject = {
|
|
209
|
+
nested: originalObject.nested,
|
|
210
|
+
foo: "123",
|
|
211
|
+
hello: "234",
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const originalWrappped = record(originalObject);
|
|
215
|
+
|
|
216
|
+
const additionalWrapped = record(additionalObject);
|
|
217
|
+
|
|
218
|
+
const recordedNested = record(originalWrappped.nested);
|
|
219
|
+
|
|
220
|
+
const mockFn = mock();
|
|
221
|
+
|
|
222
|
+
onDidUpdate(originalWrappped, () => {
|
|
223
|
+
mockFn();
|
|
224
|
+
expect(originalObject.nested).toEqual(originalWrappped.nested);
|
|
225
|
+
});
|
|
226
|
+
onDidUpdate(additionalWrapped, () => {
|
|
227
|
+
// We should never hit that path
|
|
228
|
+
expect(1).toBe(0);
|
|
229
|
+
mockFn();
|
|
230
|
+
});
|
|
231
|
+
onDidUpdate(recordedNested, () => {
|
|
232
|
+
mockFn();
|
|
233
|
+
// @ts-ignore
|
|
234
|
+
expect(recordedNested).toEqual({ support: "updated" });
|
|
235
|
+
});
|
|
236
|
+
originalWrappped.nested.support = "updated";
|
|
237
|
+
// Updates are propagated in microtick queue, so we wait single tick
|
|
238
|
+
await Promise.resolve();
|
|
239
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
240
|
+
});
|