This repository has been archived by the owner on Jan 2, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcreate-node.ts
448 lines (411 loc) · 15.5 KB
/
create-node.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
import { VContext } from "./vcontext";
import {
Source,
SourceReferenceRepresentationFactory
} from "./source";
import {
isSourceReference,
SourceReference,
MarshalledSourceReference
} from "./source-reference";
import {
FragmentVNode,
isFragmentVNode,
isMarshalledVNode,
isVNode,
MarshalledVNode,
VNode,
VNodeRepresentationSource
} from "./vnode";
import {
isAsyncIterable,
isIterable,
isPromise,
asyncExtendedIterable,
isIterableIterator,
getNext
} from "iterable";
import { children as childrenGenerator, ChildrenContext } from "./children";
import { Fragment } from "./fragment";
// Access to re-assign a functional vnode child between children reads
export const Child = Symbol("Function VNode Child");
export type CreateNodeFragmentSourceFirstStage =
| Function
| Promise<unknown>
| typeof Fragment;
export type CreateNodeFragmentSourceSecondStage =
| AsyncIterable<unknown>
| Iterable<unknown>
| IterableIterator<unknown>
| undefined
| null;
export interface CreateNodeFn<
O extends object = object,
S = Source<O>,
C extends VNodeRepresentationSource = VNodeRepresentationSource,
Output extends VNode = VNode
> {
<TO extends O, S extends CreateNodeFragmentSourceFirstStage>(source: S): FragmentVNode & {
source: S;
options: never;
children: never;
};
<S extends CreateNodeFragmentSourceFirstStage>(source: S): FragmentVNode & {
source: S;
options: never;
children: never;
};
<TO extends O, S extends CreateNodeFragmentSourceFirstStage>(source: S, options: TO): FragmentVNode & {
source: S;
options: TO;
children: never;
};
<TO extends O, S extends CreateNodeFragmentSourceFirstStage>(source: S, options?: TO, ...children: C[]): FragmentVNode & {
source: S;
options: TO;
};
<Input extends FragmentVNode>(source: Input, ...throwAway: unknown[]): Input;
<Input extends VNode>(source: Input, ...throwAway: unknown[]): Input;
<TO extends O, S extends CreateNodeFragmentSourceSecondStage>(source: S): FragmentVNode & {
source: S;
options: never;
children: never;
};
<S extends CreateNodeFragmentSourceSecondStage>(source: S): FragmentVNode & {
source: S;
options: never;
children: never;
};
<TO extends O, S extends CreateNodeFragmentSourceSecondStage>(source: S, options: TO): FragmentVNode & {
source: S;
options: TO;
children: never;
};
<TO extends O, S extends CreateNodeFragmentSourceSecondStage>(source: S, options?: TO, ...children: C[]): FragmentVNode & {
source: S;
options: TO;
};
<TO extends O, S extends SourceReference>(source: S): VNode & {
source: S;
options: never;
scalar: true;
children: never;
};
<TO extends O, S extends SourceReference>(source: S, options?: TO): VNode & {
source: S;
options: TO;
scalar: true;
children: never;
};
<TO extends O, S extends SourceReference>(source: S, options?: TO, ...children: C[]): VNode & {
source: S;
options: TO;
scalar: false;
};
<TO extends O>(source: S, options?: TO, ...children: C[]): Output;
}
export type CreateNodeFnUndefinedOptionsCatch<
Test extends (source: CreateNodeFragmentSourceFirstStage) => FragmentVNode & { source: CreateNodeFragmentSourceFirstStage, options: never }> = Test;
export type CreateNodeFnGivenOptionsCatch<
Test extends (source: CreateNodeFragmentSourceFirstStage, options: { key: "value" }) => FragmentVNode & { source: CreateNodeFragmentSourceFirstStage, options: { key: "value" } }> = Test;
type ThrowAwayCreateNodeFnUndefinedOptionsCatch = CreateNodeFnUndefinedOptionsCatch<typeof createNode>;
type ThrowAwayCreateNodeFnGivenOptionsCatch = CreateNodeFnGivenOptionsCatch<typeof createNode>;
export type CreateNodeFnCatch<
O extends object = object,
S = Source<O>,
C extends VNodeRepresentationSource = VNodeRepresentationSource,
Output extends VNode = VNode,
Test extends CreateNodeFn<O, S, C, Output> = CreateNodeFn<O, S, C, Output>
> = Test;
// This will throw if createNode doesn't match the type for CreateNodeFn, this gives us type safety :)
type TestThrow = CreateNodeFnCatch<
object,
Source<object>,
VNodeRepresentationSource,
VNode,
typeof createNode
>;
const childrenContext: ChildrenContext = {
createNode
};
/**
* Generates instances of {@link FragmentVNode} based on the provided source
*
* See {@link Source} for an explanation on each type and how they are represented as a {@link VNode}
*
* The provided {@link VContext} may override this functionality, possibly resulting in a {@link NativeVNode}
*
* The special case to point out here is if the source is an `IterableIterator` (see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#Is_a_generator_object_an_iterator_or_an_iterable})
* then each iteration will result in a new {@link VNode} being created
*/
export function createNode<O extends object = object, S extends CreateNodeFragmentSourceFirstStage = CreateNodeFragmentSourceFirstStage>(source: S, options?: O, ...children: VNodeRepresentationSource[]): FragmentVNode & {
source: S;
options: O;
};
export function createNode<O extends object = object, S extends CreateNodeFragmentSourceFirstStage = CreateNodeFragmentSourceFirstStage>(source: S, options?: O, ...children: VNodeRepresentationSource[]): FragmentVNode & {
source: S;
options: O;
};
export function createNode<Input extends FragmentVNode>(source: Input, ...throwAway: unknown[]): Input;
export function createNode<Input extends VNode = VNode>(source: Input, ...throwAway: unknown[]): Input;
export function createNode<O extends object = object, S extends CreateNodeFragmentSourceSecondStage = CreateNodeFragmentSourceSecondStage>(source: S, options?: O, ...children: VNodeRepresentationSource[]): FragmentVNode & {
source: S;
options: O;
};
export function createNode<O extends object = object, S extends CreateNodeFragmentSourceSecondStage = CreateNodeFragmentSourceSecondStage>(source: S, options?: O, ...children: VNodeRepresentationSource[]): FragmentVNode & {
source: S;
options: O;
};
export function createNode<O extends object = object, S extends SourceReference = SourceReference>(source: S, options?: O): VNode & {
source: S;
options: O;
scalar: true;
children: never;
};
export function createNode<O extends object = object, S extends SourceReference = SourceReference>(source: S, options?: O, ...children: VNodeRepresentationSource[]): VNode & {
source: S;
options: O;
scalar: boolean;
};
export function createNode<O extends object = object>(source: Source<O>, options?: O, ...children: VNodeRepresentationSource[]): VNode;
export function createNode<O extends object = object>(source: Source<O>, options?: O, ...children: VNodeRepresentationSource[]): VNode {
/**
* If the source is a function we're going to invoke it as soon as possible with the provided options
*
* The function _may_ return any other kind of source, so we need to start our process again
*/
if (source instanceof Function) {
return functionVNode(source);
}
/**
* Only if the source is a promise we want to await it
*
* This may be wasteful, but the idea is that we wouldn't cause a next tick for no reason
* Maybe this isn't the case if the value isn't a promise to start with ¯\_(ツ)_/¯
*/
if (source && isPromise(source)) {
return {
source,
reference: Fragment,
children: replay(() => promiseGenerator(source))
};
}
/**
* If we have a fragment then we want to pass it back through our function so the next
* statement is invoked to handle fragments with children
*/
if (source === Fragment) {
return createNode({ reference: Fragment, source }, options, ...children);
}
/**
* If we already have a {@link VNode} then we don't and can't do any more
*/
if (source && isVNode(source)) {
let nextSource: VNode = source;
/**
* Extend our vnode options if we have been provided them
* Each property that is not passed will match the initial property
*/
if (options && source.options !== options) {
nextSource = {
...nextSource,
options: {
...nextSource.options,
...options
}
};
}
/**
* Replace children if they have been given and the source doesn't already have children
*/
if (children.length && !nextSource.children) {
nextSource = {
...nextSource,
children: replay(() => childrenGenerator(childrenContext, ...children))
};
}
return nextSource;
}
/**
* If we already have a {@link MarshalledVNode} then we need to turn its children into an async iterable
* and ensure they're unmarshalled
*/
if (source && isMarshalledVNode(source)) {
return unmarshal(source);
}
const reference = getReference(options);
/**
* A source reference may be in reference to a context we don't know about, this can be resolved from
* external contexts by rolling through the {@link VNode} state, or watching context events
*
* This could be used by analytics tracking for tags that show up
*
* Either way, if we have a source reference, we have a primitive value that we can look up later on
*/
if (isSourceReference(source)) {
return sourceReferenceVNode(reference, source, options, ...children);
}
/**
* Here is our nice `IterableIterator` that allows us to produce multiple versions for the same source
*
* This specifically cannot be re-run twice, but this is expected to be returned from a function, where
* functions can be run twice
*
* See {@link generator} for details
*/
if (source && isIterableIterator(source)) {
return {
source,
reference: Fragment,
children: generator(Symbol("Iterable Iterator"), source)
};
}
/**
* This will cover `Array`, `Set`, `Map`, and anything else implementing `Iterable` or `AsyncIterable`
*
* We will create a `Fragment` that holds our node state to grab later
*/
if (source && (isIterable(source) || isAsyncIterable(source))) {
const childrenInstance = childrenGenerator(childrenContext, ...children);
return {
source,
reference: Fragment,
children: replay(() => childrenGenerator(childrenContext, asyncExtendedIterable(source).map(value => createNode(value, options, childrenInstance))))
};
}
/**
* Allows for `undefined`, an empty `VNode`
*/
if (!source) {
return { reference: Fragment, source };
}
/**
* We _shouldn't_ get here AFAIK, each kind of source should have been dealt with by the time we get here
*/
throw new Error("Unexpected VNode source provided");
/**
* Iterates through an `IterableIterator` to generate new {@link VNode} instances
*
* This allows an implementor to decide when their node returns state, including pushing new values _as they arrive_
*
* {@link getNext} provides an error boundary if the `IterableIterator` provides a `throw` function
*
* @param newReference
* @param reference
*/
async function *generator(newReference: SourceReference, reference: IterableIterator<SourceReference> | AsyncIterableIterator<SourceReference>): AsyncIterable<VNode[]> {
let next: IteratorResult<SourceReference>;
do {
next = await getNext(reference, newReference);
if (next.done) {
continue;
}
const nextNode = createNode(next.value);
if (isFragmentVNode(nextNode)) {
yield* nextNode.children ?? [];
}
yield [nextNode];
} while (!next.done);
}
async function *promiseGenerator(promise: Promise<SourceReference | VNode>): AsyncIterable<VNode[]> {
const result = await promise;
yield [
createNode(result, options, ...children)
];
}
function functionVNode(source: SourceReferenceRepresentationFactory<O>): VNode {
const defaultOptions = {};
const resolvedOptions = isDefaultOptionsO(defaultOptions) ? defaultOptions : options;
const node: VNode & {
[Child]?: VNode,
source: typeof source,
options: typeof resolvedOptions
} = {
reference: Fragment,
source,
options: resolvedOptions,
children: replay(() => functionAsChildren()),
};
return node;
async function *functionAsChildren(): AsyncIterable<VNode[]> {
const options = node.options;
const source = node.source;
// Lazy create the children when the function is first invoked
// This allows children to be a bit more dynamic
//
// We will only provide a child to node.source if we have at least one child provided
const child = node[Child] = node[Child] ?? children.length ? createNode(Fragment, {}, ...children) : undefined;
// Referencing node here allows for external to update the nodes implementation on the fly...
const nextSource = source(options, child);
// If the nextSource is the same as node.source, then we should finish here, it will always return itself
// If node.source returns a promise then we can safely assume this was intentional as a "loop" around
// A function can also return an iterator (async or sync) that returns itself too
//
// This is to only detect hard loops
// We will also reference the different dependency here, as they might have been re-assigned,
// meaning the possible return from this function has changed, meaning the return value could be different
const possibleMatchingSource: unknown = nextSource;
if (
possibleMatchingSource !== source ||
source !== node.source ||
options !== node.options ||
child !== node[Child]
) {
yield [
createNode(nextSource)
];
}
}
function isDefaultOptionsO(value: unknown): value is O {
return value === defaultOptions && !options;
}
}
function unmarshal(source: MarshalledVNode): VNode {
if (isSourceReference(source)) {
return sourceReferenceVNode(getReference(), source);
}
return {
...source,
// Replace our reference if required
reference: isSourceReference(source.reference) ? getMarshalledReference(source.reference) : getReference(source.options),
children: source.children ? replay(() => asyncExtendedIterable(source.children).map(children => [...children].map(unmarshal))) : undefined
};
}
function sourceReferenceVNode(reference: SourceReference, source: SourceReference, options?: object, ...children: VNodeRepresentationSource[]): VNode {
return {
reference: reference || getReference(options),
scalar: !children.length,
source,
options,
children: children.length ? replay(() => childrenGenerator(childrenContext, ...children)) : undefined
};
}
function replay<T>(fn: () => AsyncIterable<T>): AsyncIterable<T> {
return {
[Symbol.asyncIterator]: () => fn()[Symbol.asyncIterator]()
};
}
}
function getMarshalledReference(reference: MarshalledSourceReference): SourceReference {
return getReference({
reference
});
}
function getReference(options?: object) {
return getReferenceFromOptions(options) ?? Symbol("@opennetwork/vnode");
}
function isReferenceOptions(options: object): options is object & { reference: SourceReference } {
function isReferenceOptionsLike(options: object): options is object & { reference?: unknown } {
return options && options.hasOwnProperty("reference");
}
return (
isReferenceOptionsLike(options) &&
isSourceReference(options.reference)
);
}
function getReferenceFromOptions(options: object | undefined): SourceReference {
if (!isReferenceOptions(options)) {
return undefined;
}
return options.reference;
}