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.
- package/.env.development +1 -0
- package/.github/workflows/main.yml +24 -0
- package/README.md +1 -1
- package/TODOS.md +2 -0
- package/index.ts +1 -0
- package/package.json +7 -2
- 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/{tools/isRecordLike.spec.ts → data-structures/record/IsRecordLike.spec.ts} +1 -1
- package/src/{tools/isRecordLike.ts → data-structures/record/IsRecordLike.ts} +1 -1
- package/src/{record/record.spec.ts → data-structures/record/Record.spec.ts} +96 -2
- 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/globals.d.ts +1 -0
- package/src/main.ts +22 -11
- package/src/style.css +24 -79
- 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/tsconfig.json +2 -2
- package/vite.config.js +10 -0
- package/bun.lockb +0 -0
- package/src/record/record.ts +0 -101
- package/src/tools/optional.ts +0 -25
- package/src/ui/component.ts +0 -1
- package/src/ui/prop.ts +0 -13
- package/src/ui/state.ts +0 -0
- /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
|
+
});
|