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.
Files changed (54) hide show
  1. package/.env.development +1 -0
  2. package/.github/workflows/main.yml +24 -0
  3. package/README.md +1 -52
  4. package/TODOS.md +2 -0
  5. package/index.html +13 -0
  6. package/index.ts +1 -0
  7. package/package.json +12 -44
  8. package/src/DOM/Context.ts +36 -0
  9. package/src/DOM/DomEngine.ts +106 -0
  10. package/src/DOM/IRenderCycle.ts +9 -0
  11. package/src/DOM/Key.ts +1 -0
  12. package/src/DOM/Node.ts +472 -0
  13. package/src/DOM/Registry.ts +53 -0
  14. package/src/creo.ts +134 -0
  15. package/src/data-structures/assert/assert.ts +12 -0
  16. package/src/data-structures/indexed-map/IndexedMap.ts +281 -0
  17. package/src/data-structures/linked-map/LinkedMap.spec.ts +67 -0
  18. package/src/data-structures/linked-map/LinkedMap.ts +198 -0
  19. package/src/data-structures/list/List.spec.ts +181 -0
  20. package/src/data-structures/list/List.ts +195 -0
  21. package/src/data-structures/maybe/Maybe.ts +25 -0
  22. package/src/data-structures/null/null.ts +3 -0
  23. package/src/data-structures/record/IsRecordLike.spec.ts +29 -0
  24. package/src/data-structures/record/IsRecordLike.ts +3 -0
  25. package/src/data-structures/record/Record.spec.ts +240 -0
  26. package/src/data-structures/record/Record.ts +145 -0
  27. package/src/data-structures/shalllowEqual/shallowEqual.ts +26 -0
  28. package/src/data-structures/simpleKey/simpleKey.ts +8 -0
  29. package/src/data-structures/wildcard/wildcard.ts +1 -0
  30. package/src/examples/SimpleTodoList/SimpleTodoList.ts +53 -0
  31. package/src/examples/simple.ts +0 -0
  32. package/src/globals.d.ts +1 -0
  33. package/src/main.ts +24 -0
  34. package/src/style.css +41 -0
  35. package/src/ui/html/Block.ts +10 -0
  36. package/src/ui/html/Button.ts +12 -0
  37. package/src/ui/html/HStack.ts +10 -0
  38. package/src/ui/html/Inline.ts +12 -0
  39. package/src/ui/html/List.ts +10 -0
  40. package/src/ui/html/Text.ts +9 -0
  41. package/src/ui/html/VStack.ts +11 -0
  42. package/src/vite-env.d.ts +1 -0
  43. package/tsconfig.json +23 -0
  44. package/vite.config.js +10 -0
  45. package/LICENSE +0 -21
  46. package/dist/index.cjs +0 -54
  47. package/dist/index.cjs.map +0 -1
  48. package/dist/index.d.ts +0 -19
  49. package/dist/index.js +0 -51
  50. package/dist/index.js.map +0 -1
  51. package/dist/index.mjs +0 -51
  52. package/dist/index.mjs.map +0 -1
  53. package/dist/index.umd.js +0 -61
  54. 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,3 @@
1
+ export const _ = undefined;
2
+
3
+ export function emptyFn() {}
@@ -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,3 @@
1
+ export function isRecordLike<T, K extends object>(value: T | K): value is K {
2
+ return (typeof value === "object" || Array.isArray(value)) && value != null;
3
+ }
@@ -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
+ });