Skip to content

Commit

Permalink
Add markEager and update FlexList docs (#17819)
Browse files Browse the repository at this point in the history
## Description

This adds an escape hatch for using `FlexList`s when the items in the
list are functions.
It also improves some of the documentation for `FlexList` and renames
`normalizeFlexList` to `normalizeFlexListLazy` for clarity.
  • Loading branch information
noencke authored Oct 17, 2023
1 parent aa783fb commit f7690e3
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 14 deletions.
1 change: 1 addition & 0 deletions experimental/dds/tree2/src/feature-libraries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export {
validateStructFieldName,
Unenforced,
AllowedTypeSet,
markEager,
MapFieldSchema,
} from "./typed-schema";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,83 @@
* Licensed under the MIT License.
*/

/** A symbol used to identify a `MarkedEager`. */
const flexListEager = Symbol("FlexList Eager");

/**
* An object that has been marked as eager (as opposed to lazy) when used as an item in a `FlexList`.
* It will be considered to be an eager item in a `FlexList` even if it is a function.
*/
interface MarkedEager {
[flexListEager]: true;
}

/** Returns true iff the given item is a function and is not a `MarkedEager`. */
export function isLazy<Item>(item: LazyItem<Item>): item is () => Item {
return typeof item === "function" && (item as Partial<MarkedEager>)[flexListEager] !== true;
}

/**
* Flexible way to list values.
* Can be values, functions that return the value (to allow cyclic references to work), or arrays.
* Mark the given object as an eager item in a `FlexList`.
* @remarks
* Does not work properly if T can be a function.
* This only has an effect on function objects that would otherwise be considered to be lazy items in a `FlexList`.
* @param t - The object to mark as eager.
* @returns `t`, marked as eager if applicable.
*/
export function markEager<T>(t: T): T {
return isLazy(t)
? Object.defineProperty(t, flexListEager, {
value: true,
configurable: true,
enumerable: false,
writable: false,
})
: t;
}

/**
* A flexible way to list values.
* Each item in the list can either be an "eager" **value** or a "lazy" **function that returns a value** (the latter allows cyclic references to work).
* @privateRemarks
* By default, items that are of type `"function"` will be considered lazy and all other items will be considered eager.
* To force a `"function"` item to be treated as an eager item, call `markEager` before putting it in the list.
* This is necessary e.g. when the eager list items are function types and the lazy items are functions that _return_ function types.
* `FlexList`s are processed by `normalizeFlexList` and `normalizeFlexListEager`.
* @alpha
*/
export type FlexList<Item = unknown> = readonly LazyItem<Item>[];

export function normalizeFlexList<List extends FlexList>(t: List): FlexListToLazyArray<List> {
/**
* Given a `FlexList` of eager and lazy items, return an equivalent list where all items are lazy.
*/
export function normalizeFlexListLazy<List extends FlexList>(t: List): FlexListToLazyArray<List> {
return t.map((value: LazyItem) => {
if (typeof value === "function") {
if (isLazy(value)) {
return value;
}
return () => value;
}) as FlexListToLazyArray<List>;
}

/**
* Given a `FlexList` of eager and lazy items, return an equivalent list where all items are eager.
*/
export function normalizeFlexListEager<List extends FlexList>(
t: List,
): FlexListToNonLazyArray<List> {
const data: readonly unknown[] = t.map((value: LazyItem) => {
if (typeof value === "function") {
return value() as unknown;
if (isLazy(value)) {
return value();
}
return value;
});
return data as FlexListToNonLazyArray<List>;
}

/**
* T, but can be wrapped in a function to allow referring to types before they are declared.
* This makes recursive and co-recursive types possible.
* An "eager" or "lazy" Item in a `FlexList`.
* Lazy items are wrapped in a function to allow referring to themselves before they are declared.
* This makes recursive and co-recursive items possible.
* @alpha
*/
export type LazyItem<Item = unknown> = Item | (() => Item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export {
schemaLintDefault,
} from "./schemaCollection";

export { FlexList } from "./flexList";
export { FlexList, markEager } from "./flexList";

// Below here are things that are used by the above, but not part of the desired API surface.
import * as InternalTypedSchemaTypes from "./internal";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { strict as assert } from "assert";
import {
FlexListToNonLazyArray,
FlexListToLazyArray,
normalizeFlexList,
normalizeFlexListLazy,
LazyItem,
ArrayHasFixedLength,
normalizeFlexListEager,
markEager,
FlexList,
isLazy,
// Allow importing from this specific file which is being tested:
/* eslint-disable-next-line import/no-internal-modules */
} from "../../../feature-libraries/typed-schema/flexList";
Expand Down Expand Up @@ -76,17 +79,32 @@ import { requireAssignableTo, requireFalse, requireTrue } from "../../../util";
}

describe("FlexList", () => {
it("normalizeFlexList", () => {
it("correctly normalizes lists to be lazy", () => {
const list = [2, (): 1 => 1] as const;
const normalized = normalizeFlexList(list);
const normalized = normalizeFlexListLazy(list);
assert(normalized.length === 2);
const data = normalized.map((f) => f());
assert.deepEqual(data, [2, 1]);
});

it("normalizeFlexListEager", () => {
it("correctly normalizes lists to be eager", () => {
const list = [2, (): 1 => 1] as const;
const normalized: readonly [2, 1] = normalizeFlexListEager(list);
assert.deepEqual(normalized, [2, 1]);
});

it("can mark functions as eager", () => {
const fn = () => 42;
assert.equal(isLazy(fn), true);
markEager(fn);
assert.equal(isLazy(fn), false);
});

it("correctly normalizes functions marked as eager", () => {
const eagerGenerator = markEager(() => 42);
const lazyGenerator = () => () => 42;
const list: FlexList<() => number> = [eagerGenerator, lazyGenerator];
normalizeFlexListEager(list).forEach((g) => assert.equal(g(), 42));
normalizeFlexListLazy(list).forEach((g) => assert.equal(g()(), 42));
});
});

0 comments on commit f7690e3

Please sign in to comment.