diff --git a/docs/en/v1/api/common/index.md b/docs/en/v1/api/common/index.md index 108b2201..c18448ec 100644 --- a/docs/en/v1/api/common/index.md +++ b/docs/en/v1/api/common/index.md @@ -127,6 +127,9 @@ Async pause to wait for a certain time. ### [memo](/en/v1/api/common/memo) Evaluates a function only once and reuses the result (lazy memoization). +### [memoObject](/en/v1/api/common/memoObject) +Memoizes an object exposed through a `Proxy` and keeps keys aligned with writes. + ### [memoPromise](/en/v1/api/common/memoPromise) Lazy memoization for functions returning a value or a promise. diff --git a/docs/en/v1/api/common/memo.md b/docs/en/v1/api/common/memo.md index 5a746427..150237a8 100644 --- a/docs/en/v1/api/common/memo.md +++ b/docs/en/v1/api/common/memo.md @@ -5,8 +5,8 @@ prev: text: "sleep" link: "/en/v1/api/common/sleep" next: - text: "memoPromise" - link: "/en/v1/api/common/memoPromise" + text: "memoObject" + link: "/en/v1/api/common/memoObject" --- # memo diff --git a/docs/en/v1/api/common/memoObject.md b/docs/en/v1/api/common/memoObject.md new file mode 100644 index 00000000..0944407e --- /dev/null +++ b/docs/en/v1/api/common/memoObject.md @@ -0,0 +1,49 @@ +--- +outline: [2, 3] +description: "The memoObject() function builds a proxy around a memoized object. The getter is evaluated once, then reads/writes target the same reference." +prev: + text: "memo" + link: "/en/v1/api/common/memo" +next: + text: "memoPromise" + link: "/en/v1/api/common/memoPromise" +--- + +# memoObject + +The **`memoObject()`** function builds a proxy around a memoized object. The getter is evaluated lazily on first access, then all reads/writes target the same object. + +## Interactive example + + + +## Syntax + +```typescript +function memoObject< + GenericOutput extends object +>( + getter: () => GenericOutput +): GenericOutput; +``` + +## Parameters + +- `getter` : Function called on first access to produce the proxied object. + +## Return value + +A proxied `GenericOutput` object: +- reads (`obj.prop`) return values from the memoized object; +- writes (`obj.prop = value`) mutate the memoized object; +- `Object.keys()` and the `in` operator reflect keys after writes. + +## See also + +- [`memo`](/en/v1/api/common/memo) - Lazy memoization for synchronous values +- [`memoPromise`](/en/v1/api/common/memoPromise) - Lazy memoization for async-capable values +- [`override`](/en/v1/api/common/override) - Override methods and default properties on an object diff --git a/docs/en/v1/api/common/memoPromise.md b/docs/en/v1/api/common/memoPromise.md index 157f8c8d..ab6f15d1 100644 --- a/docs/en/v1/api/common/memoPromise.md +++ b/docs/en/v1/api/common/memoPromise.md @@ -2,8 +2,8 @@ outline: [2, 3] description: "The memoPromise() function lazily evaluates a function that returns a value or a promise, then memoizes the resolved result." prev: - text: "memo" - link: "/en/v1/api/common/memo" + text: "memoObject" + link: "/en/v1/api/common/memoObject" next: text: "stringToMillisecond" link: "/en/v1/api/common/stringToMillisecond" diff --git a/docs/examples/v1/api/clean/flag/tryout.doc.ts b/docs/examples/v1/api/clean/flag/tryout.doc.ts index bead46a3..37e26a07 100644 --- a/docs/examples/v1/api/clean/flag/tryout.doc.ts +++ b/docs/examples/v1/api/clean/flag/tryout.doc.ts @@ -16,14 +16,14 @@ namespace User { export const MajorFlag = C.createFlag< Entity, // mandatory "majorUser", // mandatory - Age // optional + { age: Age } // optional >("majorUser"); export type MajorFlag = C.GetFlag; export function isMajor(entity: Entity) { if (C.greaterThan(entity.age, 18)) { return E.success( - MajorFlag.append(entity, entity.age), + MajorFlag.append(entity, { age: entity.age }), ); } return E.left("not-major"); diff --git a/docs/examples/v1/api/clean/unwrapEntity/tryout.doc.ts b/docs/examples/v1/api/clean/unwrapEntity/tryout.doc.ts index 2b214703..ebb83936 100644 --- a/docs/examples/v1/api/clean/unwrapEntity/tryout.doc.ts +++ b/docs/examples/v1/api/clean/unwrapEntity/tryout.doc.ts @@ -21,7 +21,7 @@ export namespace User { export const IsAdmin = C.createFlag< Entity, "isAdmin", - boolean + { value: boolean } >("isAdmin"); export type IsAdmin = C.GetFlag; } @@ -33,7 +33,7 @@ const user = User.Entity.new({ createdAt: User.CreatedAt.createOrThrow(D.now()), }); -const flaggedUser = User.IsAdmin.append(user, true); +const flaggedUser = User.IsAdmin.append(user, { value: true }); const unwrappedUser = C.unwrapEntity(flaggedUser); type check = ExpectType< diff --git a/docs/examples/v1/api/common/memoObject/tryout.doc.ts b/docs/examples/v1/api/common/memoObject/tryout.doc.ts new file mode 100644 index 00000000..ff8b3a10 --- /dev/null +++ b/docs/examples/v1/api/common/memoObject/tryout.doc.ts @@ -0,0 +1,15 @@ +import { memoObject } from "@duplojs/utils"; + +let calls = 0; +const state = memoObject(() => { + calls += 1; + return { count: 1 }; +}); + +const first = state.count; +const second = state.count; +// calls = 1 + +state.count = 2; +const updated = state.count; +// updated = 2 diff --git a/docs/fr/v1/api/common/index.md b/docs/fr/v1/api/common/index.md index bb66909c..1c8f9113 100644 --- a/docs/fr/v1/api/common/index.md +++ b/docs/fr/v1/api/common/index.md @@ -127,6 +127,9 @@ Pause asynchrone pour attendre un certain temps. ### [memo](/fr/v1/api/common/memo) Évalue une fonction une seule fois et réutilise le résultat (memoization lazy). +### [memoObject](/fr/v1/api/common/memoObject) +Mémoïse un objet exposé via `Proxy` et garde les clés alignées avec les écritures. + ### [memoPromise](/fr/v1/api/common/memoPromise) Mémoïsation paresseuse pour des fonctions retournant une valeur ou une promesse. diff --git a/docs/fr/v1/api/common/memo.md b/docs/fr/v1/api/common/memo.md index 9948d7b4..cc5b4c04 100644 --- a/docs/fr/v1/api/common/memo.md +++ b/docs/fr/v1/api/common/memo.md @@ -5,8 +5,8 @@ prev: text: "sleep" link: "/fr/v1/api/common/sleep" next: - text: "memoPromise" - link: "/fr/v1/api/common/memoPromise" + text: "memoObject" + link: "/fr/v1/api/common/memoObject" --- # memo diff --git a/docs/fr/v1/api/common/memoObject.md b/docs/fr/v1/api/common/memoObject.md new file mode 100644 index 00000000..2af0131a --- /dev/null +++ b/docs/fr/v1/api/common/memoObject.md @@ -0,0 +1,49 @@ +--- +outline: [2, 3] +description: "La fonction memoObject() crée un proxy autour d'un objet mémorisé. Le getter n'est évalué qu'une fois, puis les lectures/écritures passent par la même référence." +prev: + text: "memo" + link: "/fr/v1/api/common/memo" +next: + text: "memoPromise" + link: "/fr/v1/api/common/memoPromise" +--- + +# memoObject + +La fonction **`memoObject()`** crée un proxy autour d'un objet mémorisé. Le getter est évalué paresseusement au premier accès, puis toutes les lectures/écritures utilisent le même objet. + +## Exemple interactif + + + +## Syntaxe + +```typescript +function memoObject< + GenericOutput extends object +>( + getter: () => GenericOutput +): GenericOutput; +``` + +## Paramètres + +- `getter` : Fonction appelée au premier accès pour produire l'objet proxifié. + +## Valeur de retour + +Un objet de type `GenericOutput` proxifié : +- les lectures (`obj.prop`) renvoient les valeurs de l'objet mémorisé ; +- les écritures (`obj.prop = value`) modifient l'objet mémorisé ; +- `Object.keys()` et l'opérateur `in` reflètent les clés après écriture. + +## Voir aussi + +- [`memo`](/fr/v1/api/common/memo) - Mémoïsation paresseuse synchrone +- [`memoPromise`](/fr/v1/api/common/memoPromise) - Mémoïsation paresseuse pour valeurs async +- [`override`](/fr/v1/api/common/override) - Surcharger méthodes et propriétés d'un objet diff --git a/docs/fr/v1/api/common/memoPromise.md b/docs/fr/v1/api/common/memoPromise.md index 287fd61b..253b4b2a 100644 --- a/docs/fr/v1/api/common/memoPromise.md +++ b/docs/fr/v1/api/common/memoPromise.md @@ -2,8 +2,8 @@ outline: [2, 3] description: "La fonction memoPromise() évalue paresseusement une fonction qui retourne une valeur ou une promesse, puis mémorise le résultat résolu." prev: - text: "memo" - link: "/fr/v1/api/common/memo" + text: "memoObject" + link: "/fr/v1/api/common/memoObject" next: text: "stringToMillisecond" link: "/fr/v1/api/common/stringToMillisecond" diff --git a/docs/public/libs/v1/clean/entity/index.cjs b/docs/public/libs/v1/clean/entity/index.cjs index 93b42543..34dceaf5 100644 --- a/docs/public/libs/v1/clean/entity/index.cjs +++ b/docs/public/libs/v1/clean/entity/index.cjs @@ -5,21 +5,22 @@ var newType = require('../newType.cjs'); var property = require('./property.cjs'); var kind$1 = require('../../common/kind.cjs'); var pipe = require('../../common/pipe.cjs'); -var map = require('../../array/map.cjs'); -var entry = require('../../object/entry.cjs'); -var base = require('../constraint/base.cjs'); -var transform = require('../../dataParser/parsers/transform.cjs'); -var entries = require('../../object/entries.cjs'); -var forward = require('../../common/forward.cjs'); var errorKindNamespace = require('../../common/errorKindNamespace.cjs'); -var index = require('../../dataParser/parsers/object/index.cjs'); -var fromEntries = require('../../object/fromEntries.cjs'); -var wrapValue = require('../../common/wrapValue.cjs'); +var memo = require('../../common/memo.cjs'); var override = require('../../common/override.cjs'); var is = require('../../either/left/is.cjs'); var unwrap$1 = require('../../common/unwrap.cjs'); var create = require('../../either/left/create.cjs'); var create$1 = require('../../either/right/create.cjs'); +var transform = require('../../dataParser/parsers/transform.cjs'); +var index = require('../../dataParser/parsers/object/index.cjs'); +var fromEntries = require('../../object/fromEntries.cjs'); +var map = require('../../array/map.cjs'); +var entry = require('../../object/entry.cjs'); +var base = require('../constraint/base.cjs'); +var wrapValue = require('../../common/wrapValue.cjs'); +var entries = require('../../object/entries.cjs'); +var forward = require('../../common/forward.cjs'); const entityKind = kind.createCleanKind("entity"); const entityHandlerKind = kind.createCleanKind("entity-handler"); @@ -39,8 +40,8 @@ function createEntity(name, getPropertiesDefinition) { function theNew(properties) { return entityKind.addTo(properties, name); } - const propertiesDefinition = getPropertiesDefinition(property.entityPropertyDefinitionTools); - const mapDataParser = pipe.pipe(forward.forward(propertiesDefinition), entries.entries, map.map(([key, property$1]) => entry.entry(key, property.entityPropertyDefinitionToDataParser(property$1, (newTypeHandler) => { + const propertiesDefinition = memo.memo(() => getPropertiesDefinition(property.entityPropertyDefinitionTools)); + const mapDataParser = memo.memo(() => pipe.pipe(forward.forward(propertiesDefinition.value), entries.entries, map.map(([key, property$1]) => entry.entry(key, property.entityPropertyDefinitionToDataParser(property$1, (newTypeHandler) => { const allKind = { ...base.constrainedTypeKind.setTo({}, newTypeHandler.internal.constraintKindValue), ...newType.newTypeKind.setTo({}, newTypeHandler.name), @@ -49,16 +50,16 @@ function createEntity(name, getPropertiesDefinition) { ...allKind, [wrapValue.keyWrappedValue]: value, })); - }))), fromEntries.fromEntries, index.object, (dataParser) => transform.transform(dataParser, (value) => entityKind.setTo(value, name))); + }))), fromEntries.fromEntries, index.object, (dataParser) => transform.transform(dataParser, (value) => entityKind.setTo(value, name)))); function map$1(rawProperties) { - const result = mapDataParser.parse(rawProperties); + const result = mapDataParser.value.parse(rawProperties); if (is.isLeft(result)) { return create.left("createEntityError", unwrap$1.unwrap(result)); } return create$1.right("createEntity", unwrap$1.unwrap(result)); } function mapOrThrow(rawProperties) { - const result = mapDataParser.parse(rawProperties); + const result = mapDataParser.value.parse(rawProperties); if (is.isLeft(result)) { throw new CreateEntityError(rawProperties, unwrap$1.unwrap(result)); } @@ -67,9 +68,14 @@ function createEntity(name, getPropertiesDefinition) { function is$1(input) { return entityKind.has(input) && entityKind.getValue(input) === name; } - function update(entity, newProperties) { + function update(...args) { + if (args.length === 1) { + const [newProperties] = args; + return (entity) => update(entity, newProperties); + } + const [entity, newProperties] = args; const updatedEntity = {}; - for (const key in propertiesDefinition) { + for (const key in propertiesDefinition.value) { updatedEntity[key] = newProperties[key] !== undefined ? newProperties[key] : entity[key]; @@ -78,10 +84,16 @@ function createEntity(name, getPropertiesDefinition) { } return pipe.pipe({ name, - propertiesDefinition, - mapDataParser, + get propertiesDefinition() { + return propertiesDefinition.value; + }, + get mapDataParser() { + return mapDataParser.value; + }, internal: { - mapDataParser, + get mapDataParser() { + return mapDataParser.value; + }, }, new: theNew, map: map$1, diff --git a/docs/public/libs/v1/clean/entity/index.d.ts b/docs/public/libs/v1/clean/entity/index.d.ts index 1fe4e099..ba0959d8 100644 --- a/docs/public/libs/v1/clean/entity/index.d.ts +++ b/docs/public/libs/v1/clean/entity/index.d.ts @@ -112,6 +112,7 @@ export interface EntityHandler, const GenericProperties extends Partial>>(properties: GenericProperties): (entity: GenericEntity) => EntityUpdate; update, const GenericProperties extends Partial>>(entity: GenericEntity, properties: GenericProperties): EntityUpdate; } declare const CreateEntityError_base: new (params: { diff --git a/docs/public/libs/v1/clean/entity/index.mjs b/docs/public/libs/v1/clean/entity/index.mjs index fd147a08..3381557b 100644 --- a/docs/public/libs/v1/clean/entity/index.mjs +++ b/docs/public/libs/v1/clean/entity/index.mjs @@ -1,24 +1,25 @@ import { createCleanKind } from '../kind.mjs'; import { newTypeKind } from '../newType.mjs'; -import { entityPropertyDefinitionTools, entityPropertyDefinitionToDataParser } from './property.mjs'; +import { entityPropertyDefinitionToDataParser, entityPropertyDefinitionTools } from './property.mjs'; export { entityPropertyArrayKind, entityPropertyIdentifierKind, entityPropertyNullableKind, entityPropertyStructureKind, entityPropertyUnionKind } from './property.mjs'; import { kindHeritage } from '../../common/kind.mjs'; import { pipe } from '../../common/pipe.mjs'; -import { map } from '../../array/map.mjs'; -import { entry } from '../../object/entry.mjs'; -import { constrainedTypeKind } from '../constraint/base.mjs'; -import { transform } from '../../dataParser/parsers/transform.mjs'; -import { entries } from '../../object/entries.mjs'; -import { forward } from '../../common/forward.mjs'; import { createErrorKind } from '../../common/errorKindNamespace.mjs'; -import { object } from '../../dataParser/parsers/object/index.mjs'; -import { fromEntries } from '../../object/fromEntries.mjs'; -import { keyWrappedValue } from '../../common/wrapValue.mjs'; +import { memo } from '../../common/memo.mjs'; import { createOverride } from '../../common/override.mjs'; import { isLeft } from '../../either/left/is.mjs'; import { unwrap } from '../../common/unwrap.mjs'; import { left } from '../../either/left/create.mjs'; import { right } from '../../either/right/create.mjs'; +import { transform } from '../../dataParser/parsers/transform.mjs'; +import { object } from '../../dataParser/parsers/object/index.mjs'; +import { fromEntries } from '../../object/fromEntries.mjs'; +import { map } from '../../array/map.mjs'; +import { entry } from '../../object/entry.mjs'; +import { constrainedTypeKind } from '../constraint/base.mjs'; +import { keyWrappedValue } from '../../common/wrapValue.mjs'; +import { entries } from '../../object/entries.mjs'; +import { forward } from '../../common/forward.mjs'; const entityKind = createCleanKind("entity"); const entityHandlerKind = createCleanKind("entity-handler"); @@ -38,8 +39,8 @@ function createEntity(name, getPropertiesDefinition) { function theNew(properties) { return entityKind.addTo(properties, name); } - const propertiesDefinition = getPropertiesDefinition(entityPropertyDefinitionTools); - const mapDataParser = pipe(forward(propertiesDefinition), entries, map(([key, property]) => entry(key, entityPropertyDefinitionToDataParser(property, (newTypeHandler) => { + const propertiesDefinition = memo(() => getPropertiesDefinition(entityPropertyDefinitionTools)); + const mapDataParser = memo(() => pipe(forward(propertiesDefinition.value), entries, map(([key, property]) => entry(key, entityPropertyDefinitionToDataParser(property, (newTypeHandler) => { const allKind = { ...constrainedTypeKind.setTo({}, newTypeHandler.internal.constraintKindValue), ...newTypeKind.setTo({}, newTypeHandler.name), @@ -48,16 +49,16 @@ function createEntity(name, getPropertiesDefinition) { ...allKind, [keyWrappedValue]: value, })); - }))), fromEntries, object, (dataParser) => transform(dataParser, (value) => entityKind.setTo(value, name))); + }))), fromEntries, object, (dataParser) => transform(dataParser, (value) => entityKind.setTo(value, name)))); function map$1(rawProperties) { - const result = mapDataParser.parse(rawProperties); + const result = mapDataParser.value.parse(rawProperties); if (isLeft(result)) { return left("createEntityError", unwrap(result)); } return right("createEntity", unwrap(result)); } function mapOrThrow(rawProperties) { - const result = mapDataParser.parse(rawProperties); + const result = mapDataParser.value.parse(rawProperties); if (isLeft(result)) { throw new CreateEntityError(rawProperties, unwrap(result)); } @@ -66,9 +67,14 @@ function createEntity(name, getPropertiesDefinition) { function is(input) { return entityKind.has(input) && entityKind.getValue(input) === name; } - function update(entity, newProperties) { + function update(...args) { + if (args.length === 1) { + const [newProperties] = args; + return (entity) => update(entity, newProperties); + } + const [entity, newProperties] = args; const updatedEntity = {}; - for (const key in propertiesDefinition) { + for (const key in propertiesDefinition.value) { updatedEntity[key] = newProperties[key] !== undefined ? newProperties[key] : entity[key]; @@ -77,10 +83,16 @@ function createEntity(name, getPropertiesDefinition) { } return pipe({ name, - propertiesDefinition, - mapDataParser, + get propertiesDefinition() { + return propertiesDefinition.value; + }, + get mapDataParser() { + return mapDataParser.value; + }, internal: { - mapDataParser, + get mapDataParser() { + return mapDataParser.value; + }, }, new: theNew, map: map$1, diff --git a/docs/public/libs/v1/clean/flag.cjs b/docs/public/libs/v1/clean/flag.cjs index dd3f5d6d..b22d56af 100644 --- a/docs/public/libs/v1/clean/flag.cjs +++ b/docs/public/libs/v1/clean/flag.cjs @@ -1,5 +1,6 @@ 'use strict'; +var index = require('./entity/index.cjs'); var kind = require('./kind.cjs'); var pipe = require('../common/pipe.cjs'); var override = require('../common/override.cjs'); @@ -10,17 +11,21 @@ const flagKind = kind.createCleanKind("flag"); * {@include clean/createFlag/index.md} */ function createFlag(name) { + function append(maybeEntity, value) { + if (!index.entityKind.has(maybeEntity)) { + return (entity) => append(entity, maybeEntity); + } + const flagValue = flagKind.has(maybeEntity) + ? { + ...flagKind.getValue(maybeEntity), + [name]: value, + } + : { [name]: value }; + return flagKind.addTo(maybeEntity, flagValue); + } return pipe.pipe({ name, - append(entity, value) { - const flagValue = flagKind.has(entity) - ? { - ...flagKind.getValue(entity), - [name]: value, - } - : { [name]: value }; - return flagKind.addTo(entity, flagValue); - }, + append, getValue(entity) { return flagKind.getValue(entity)[name]; }, diff --git a/docs/public/libs/v1/clean/flag.d.ts b/docs/public/libs/v1/clean/flag.d.ts index 09cfaebe..92a80486 100644 --- a/docs/public/libs/v1/clean/flag.d.ts +++ b/docs/public/libs/v1/clean/flag.d.ts @@ -2,7 +2,7 @@ import { type Kind, type IsEqual, type Or, type GetKindValue } from "../common"; import { type Entity } from "./entity"; declare const flagHandlerKind: import("../common").KindHandler>; export declare const flagKind: import("../common").KindHandler>>; -export interface FlagHandler extends Kind { +export interface FlagHandler = never> extends Kind { /** * The flag name stored as the key on the entity. * @@ -20,7 +20,8 @@ export interface FlagHandler(entity: GenericInputEntity, ...args: IsEqual extends true ? [] : [GenericInputValue]): (GenericInputEntity & Flag); + append(...args: IsEqual extends true ? [] : [GenericInputValue]): (entity: GenericInputEntity) => (GenericInputEntity & Flag); + append(entity: GenericInputEntity, ...args: IsEqual extends true ? [] : [GenericInputValue]): (GenericInputEntity & Flag); /** * Retrieves the value carried by the flag. * @@ -106,7 +107,7 @@ export interface Flag(name: Or<[ +export declare function createFlag = never>(name: Or<[ IsEqual, IsEqual ]> extends true ? never : NoInfer): FlagHandler; diff --git a/docs/public/libs/v1/clean/flag.mjs b/docs/public/libs/v1/clean/flag.mjs index 875d0015..f18bb290 100644 --- a/docs/public/libs/v1/clean/flag.mjs +++ b/docs/public/libs/v1/clean/flag.mjs @@ -1,3 +1,4 @@ +import { entityKind } from './entity/index.mjs'; import { createCleanKind } from './kind.mjs'; import { pipe } from '../common/pipe.mjs'; import { createOverride } from '../common/override.mjs'; @@ -8,17 +9,21 @@ const flagKind = createCleanKind("flag"); * {@include clean/createFlag/index.md} */ function createFlag(name) { + function append(maybeEntity, value) { + if (!entityKind.has(maybeEntity)) { + return (entity) => append(entity, maybeEntity); + } + const flagValue = flagKind.has(maybeEntity) + ? { + ...flagKind.getValue(maybeEntity), + [name]: value, + } + : { [name]: value }; + return flagKind.addTo(maybeEntity, flagValue); + } return pipe({ name, - append(entity, value) { - const flagValue = flagKind.has(entity) - ? { - ...flagKind.getValue(entity), - [name]: value, - } - : { [name]: value }; - return flagKind.addTo(entity, flagValue); - }, + append, getValue(entity) { return flagKind.getValue(entity)[name]; }, diff --git a/docs/public/libs/v1/common/index.d.ts b/docs/public/libs/v1/common/index.d.ts index 98df42d0..15b9a557 100644 --- a/docs/public/libs/v1/common/index.d.ts +++ b/docs/public/libs/v1/common/index.d.ts @@ -58,6 +58,7 @@ export * from "./or"; export * from "./whenElse"; export * from "./justReturn"; export * from "./memo"; +export * from "./memoObject"; export * from "./memoPromise"; export * from "./instanceOf"; export * from "./globalStore"; diff --git a/docs/public/libs/v1/common/memoObject.cjs b/docs/public/libs/v1/common/memoObject.cjs new file mode 100644 index 00000000..775f29bb --- /dev/null +++ b/docs/public/libs/v1/common/memoObject.cjs @@ -0,0 +1,33 @@ +'use strict'; + +var memo = require('./memo.cjs'); + +/** + * {@include common/memoObject/index.md} + */ +function memoObject(getter) { + const memoizedValue = memo.memo(getter); + let memoizedKeys = memo.memo(() => Object.keys(memoizedValue.value)); + return new Proxy({}, { + get: (_target, prop) => memoizedValue.value[prop], + set: (_target, prop, value) => { + memoizedValue.value[prop] = value; + memoizedKeys = memo.memo(() => Object.keys(memoizedValue.value)); + return true; + }, + ownKeys() { + return memoizedKeys.value; + }, + has(_target, prop) { + return memoizedKeys.value.includes(prop); + }, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + }; + }, + }); +} + +exports.memoObject = memoObject; diff --git a/docs/public/libs/v1/common/memoObject.d.ts b/docs/public/libs/v1/common/memoObject.d.ts new file mode 100644 index 00000000..4764d1f1 --- /dev/null +++ b/docs/public/libs/v1/common/memoObject.d.ts @@ -0,0 +1,29 @@ +/** + * The memoObject() function lazily evaluates a getter, memoizes the returned object, and exposes it through a proxy. + * + * Call style: direct call (`memoObject(getter)`). + * + * Reads and writes go through the same memoized object. + * + * ```ts + * let calls = 0; + * const state = memoObject(() => { + * calls += 1; + * return { + * count: 1, + * }; + * }); + * + * const first = state.count; + * const second = state.count; + * // calls = 1 + * + * state.count = 2; + * const updated = state.count; + * // updated = 2 + * ``` + * + * @see https://utils.duplojs.dev/en/v1/api/common/memoObject + * + */ +export declare function memoObject(getter: () => GenericOutput): GenericOutput; diff --git a/docs/public/libs/v1/common/memoObject.mjs b/docs/public/libs/v1/common/memoObject.mjs new file mode 100644 index 00000000..edcccddd --- /dev/null +++ b/docs/public/libs/v1/common/memoObject.mjs @@ -0,0 +1,31 @@ +import { memo } from './memo.mjs'; + +/** + * {@include common/memoObject/index.md} + */ +function memoObject(getter) { + const memoizedValue = memo(getter); + let memoizedKeys = memo(() => Object.keys(memoizedValue.value)); + return new Proxy({}, { + get: (_target, prop) => memoizedValue.value[prop], + set: (_target, prop, value) => { + memoizedValue.value[prop] = value; + memoizedKeys = memo(() => Object.keys(memoizedValue.value)); + return true; + }, + ownKeys() { + return memoizedKeys.value; + }, + has(_target, prop) { + return memoizedKeys.value.includes(prop); + }, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + }; + }, + }); +} + +export { memoObject }; diff --git a/docs/public/libs/v1/dataParser/extended/string.d.ts b/docs/public/libs/v1/dataParser/extended/string.d.ts index 0029544b..bf0eb44f 100644 --- a/docs/public/libs/v1/dataParser/extended/string.d.ts +++ b/docs/public/libs/v1/dataParser/extended/string.d.ts @@ -216,7 +216,7 @@ export declare function url(definition?: Partial("majorUser"); export type MajorFlag = C.GetFlag; export function isMajor(entity: Entity) { if (C.greaterThan(entity.age, 18)) { return E.success( - MajorFlag.append(entity, entity.age), + MajorFlag.append(entity, { age: entity.age }), ); } return E.left("not-major"); @@ -49,5 +49,5 @@ const result = pipe( ); // E.Left<"not-major", undefined> | E.Right<"not-thirsty-anymore", undefined> -const flagged = User.MajorFlag.append(user, user.age); +const flagged = User.MajorFlag.append(user, { age: user.age }); const value = User.MajorFlag.getValue(flagged); diff --git a/jsDoc/common/memoObject/example.ts b/jsDoc/common/memoObject/example.ts new file mode 100644 index 00000000..c9a047dd --- /dev/null +++ b/jsDoc/common/memoObject/example.ts @@ -0,0 +1,17 @@ +import { memoObject } from "@scripts"; + +let calls = 0; +const state = memoObject(() => { + calls += 1; + return { + count: 1, + }; +}); + +const first = state.count; +const second = state.count; +// calls = 1 + +state.count = 2; +const updated = state.count; +// updated = 2 diff --git a/jsDoc/common/memoObject/index.md b/jsDoc/common/memoObject/index.md new file mode 100644 index 00000000..8981b6ca --- /dev/null +++ b/jsDoc/common/memoObject/index.md @@ -0,0 +1,11 @@ +The memoObject() function lazily evaluates a getter, memoizes the returned object, and exposes it through a proxy. + +Call style: direct call (`memoObject(getter)`). + +Reads and writes go through the same memoized object. + +```ts +{@include common/memoObject/example.ts[3,17]} +``` + +@see https://utils.duplojs.dev/en/v1/api/common/memoObject diff --git a/scripts/clean/entity/index.ts b/scripts/clean/entity/index.ts index 6644bde1..1e267265 100644 --- a/scripts/clean/entity/index.ts +++ b/scripts/clean/entity/index.ts @@ -1,4 +1,4 @@ -import { type SimplifyTopLevel, type Kind, unwrap, kindHeritage, createErrorKind, pipe, forward, type RemoveKind, type RemoveReadonly, createOverride, type AnyFunction, type GetKindValue, keyWrappedValue } from "@scripts/common"; +import { type SimplifyTopLevel, type Kind, unwrap, kindHeritage, createErrorKind, pipe, forward, type RemoveKind, type RemoveReadonly, createOverride, type AnyFunction, type GetKindValue, keyWrappedValue, memo } from "@scripts/common"; import { createCleanKind } from "../kind"; import { newTypeKind } from "../newType"; import { constrainedTypeKind } from "../constraint"; @@ -138,6 +138,13 @@ export interface EntityHandler< /** * {@include clean/createEntity/update.md} */ + update< + const GenericEntity extends Entity, + const GenericProperties extends Partial>, + >( + properties: GenericProperties, + ): (entity: GenericEntity) => EntityUpdate; + update< const GenericEntity extends Entity, const GenericProperties extends Partial>, @@ -182,49 +189,53 @@ export function createEntity< return entityKind.addTo(properties, name); } - const propertiesDefinition = getPropertiesDefinition(entityPropertyDefinitionTools); - - const mapDataParser = pipe( - forward(propertiesDefinition), - DObject.entries, - DArray.map( - ([key, property]) => DObject.entry( - key, - entityPropertyDefinitionToDataParser( - property, - (newTypeHandler) => { - const allKind = { - ...constrainedTypeKind.setTo( - {}, - newTypeHandler.internal.constraintKindValue, - ), - ...newTypeKind.setTo( - {}, - newTypeHandler.name, - ), - }; - - return DDataParser.transform( - newTypeHandler.internal.dataParser, - (value) => ({ - ...allKind, - [keyWrappedValue]: value, - }), - ); - }, + const propertiesDefinition = memo( + () => getPropertiesDefinition(entityPropertyDefinitionTools), + ); + + const mapDataParser = memo( + () => pipe( + forward(propertiesDefinition.value), + DObject.entries, + DArray.map( + ([key, property]) => DObject.entry( + key, + entityPropertyDefinitionToDataParser( + property, + (newTypeHandler) => { + const allKind = { + ...constrainedTypeKind.setTo( + {}, + newTypeHandler.internal.constraintKindValue, + ), + ...newTypeKind.setTo( + {}, + newTypeHandler.name, + ), + }; + + return DDataParser.transform( + newTypeHandler.internal.dataParser, + (value) => ({ + ...allKind, + [keyWrappedValue]: value, + }), + ); + }, + ), ), ), - ), - DObject.fromEntries, - DDataParser.object, - (dataParser) => DDataParser.transform( - dataParser, - (value) => entityKind.setTo(value, name), + DObject.fromEntries, + DDataParser.object, + (dataParser) => DDataParser.transform( + dataParser, + (value) => entityKind.setTo(value, name), + ), ), ); function map(rawProperties: PropertiesToMapOfEntity) { - const result = mapDataParser.parse(rawProperties); + const result = mapDataParser.value.parse(rawProperties); if (DEither.isLeft(result)) { return DEither.left( @@ -240,7 +251,7 @@ export function createEntity< } function mapOrThrow(rawProperties: PropertiesToMapOfEntity) { - const result = mapDataParser.parse(rawProperties); + const result = mapDataParser.value.parse(rawProperties); if (DEither.isLeft(result)) { throw new CreateEntityError(rawProperties, unwrap(result)); @@ -254,12 +265,17 @@ export function createEntity< } function update( - entity: EntityProperties, - newProperties: Partial, + ...args: [Partial] + | [EntityProperties, Partial] ) { + if (args.length === 1) { + const [newProperties] = args; + return (entity: EntityProperties) => update(entity, newProperties); + } + const [entity, newProperties] = args; const updatedEntity: RemoveReadonly = {}; - for (const key in propertiesDefinition) { + for (const key in propertiesDefinition.value) { updatedEntity[key] = newProperties[key] !== undefined ? newProperties[key] : entity[key]; @@ -271,10 +287,16 @@ export function createEntity< return pipe( { name, - propertiesDefinition, - mapDataParser, + get propertiesDefinition() { + return propertiesDefinition.value; + }, + get mapDataParser() { + return mapDataParser.value; + }, internal: { - mapDataParser, + get mapDataParser() { + return mapDataParser.value; + }, }, new: theNew, map, diff --git a/scripts/clean/flag.ts b/scripts/clean/flag.ts index b097129c..323b01c8 100644 --- a/scripts/clean/flag.ts +++ b/scripts/clean/flag.ts @@ -1,5 +1,5 @@ -import { type Kind, type IsEqual, type Or, type GetKindValue, createOverride, pipe, type AnyFunction, type RemoveKind } from "@scripts/common"; -import { type Entity } from "./entity"; +import { type Kind, type IsEqual, type Or, type GetKindValue, createOverride, pipe, type AnyFunction, type RemoveKind, type AnyValue } from "@scripts/common"; +import { entityKind, type Entity } from "./entity"; import { createCleanKind } from "./kind"; const flagHandlerKind = createCleanKind("flag-handler"); @@ -12,7 +12,7 @@ export const flagKind = createCleanKind< export interface FlagHandler< GenericEntity extends Entity = Entity, GenericName extends string = string, - GenericValue extends unknown = never, + GenericValue extends Record = never, > extends Kind { /** @@ -25,7 +25,22 @@ export interface FlagHandler< */ append< GenericInputEntity extends GenericEntity, - GenericInputValue extends GenericValue, + const GenericInputValue extends GenericValue, + >( + ...args: IsEqual< + GenericValue, + never + > extends true + ? [] + : [GenericInputValue] + ): (entity: GenericInputEntity) => ( + & GenericInputEntity + & Flag + ); + + append< + GenericInputEntity extends GenericEntity, + const GenericInputValue extends GenericValue, >( entity: GenericInputEntity, ...args: IsEqual< @@ -77,7 +92,7 @@ export interface Flag< export function createFlag< GenericEntity extends Entity = never, GenericName extends string = never, - GenericValue extends unknown = never, + GenericValue extends Record = never, >( name: Or<[ IsEqual, @@ -90,22 +105,28 @@ export function createFlag< GenericName, GenericValue > { + function append(maybeEntity: Entity | AnyValue, value: any) { + if (!entityKind.has(maybeEntity)) { + return (entity: Entity) => append(entity, maybeEntity); + } + + const flagValue = flagKind.has(maybeEntity) + ? { + ...(flagKind.getValue(maybeEntity) as object), + [name]: value, + } + : { [name]: value }; + + return flagKind.addTo( + maybeEntity, + flagValue, + ); + } + return pipe( { name, - append(entity: Entity, value: any) { - const flagValue = flagKind.has(entity) - ? { - ...(flagKind.getValue(entity) as object), - [name]: value, - } - : { [name]: value }; - - return flagKind.addTo( - entity, - flagValue, - ); - }, + append, getValue(entity: Entity) { return flagKind.getValue(entity as never)[name]; }, diff --git a/scripts/common/index.ts b/scripts/common/index.ts index 9b04da74..c4ec315c 100644 --- a/scripts/common/index.ts +++ b/scripts/common/index.ts @@ -37,6 +37,7 @@ export * from "./or"; export * from "./whenElse"; export * from "./justReturn"; export * from "./memo"; +export * from "./memoObject"; export * from "./memoPromise"; export * from "./instanceOf"; export * from "./globalStore"; diff --git a/scripts/common/memoObject.ts b/scripts/common/memoObject.ts new file mode 100644 index 00000000..36765543 --- /dev/null +++ b/scripts/common/memoObject.ts @@ -0,0 +1,38 @@ +import { memo } from "./memo"; + +/** + * {@include common/memoObject/index.md} + */ +export function memoObject< + GenericOutput extends object, +>( + getter: () => GenericOutput, +): GenericOutput { + const memoizedValue = memo(getter); + let memoizedKeys = memo(() => Object.keys(memoizedValue.value)); + return new Proxy( + {} as GenericOutput, + { + get: (_target, prop) => memoizedValue.value[prop as never], + set: (_target, prop, value) => { + (memoizedValue.value[prop as never] as any) = value; + + memoizedKeys = memo(() => Object.keys(memoizedValue.value)); + + return true; + }, + ownKeys() { + return memoizedKeys.value; + }, + has(_target, prop) { + return memoizedKeys.value.includes(prop as never); + }, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + }; + }, + }, + ); +} diff --git a/tests/clean/entity/index.test.ts b/tests/clean/entity/index.test.ts index f53afa82..603b32c5 100644 --- a/tests/clean/entity/index.test.ts +++ b/tests/clean/entity/index.test.ts @@ -1,4 +1,4 @@ -import { DClean, DDataParser, DEither, DPE, type RemoveKind, type ExpectType } from "@scripts"; +import { DClean, DDataParser, DEither, DPE, type RemoveKind, type ExpectType, pipe } from "@scripts"; describe("createEntity", () => { const MaxConstraint = DClean.createConstraint( @@ -575,12 +575,15 @@ describe("createEntity", () => { ), ); - const updated = ProfileEntity.update(created, { - config: { - ...created.config, - note: ProfileLabel.createOrThrow("owner"), - }, - }); + const updated = pipe( + created, + ProfileEntity.update({ + config: { + ...created.config, + note: ProfileLabel.createOrThrow("owner"), + }, + }), + ); expect(updated).toStrictEqual( DClean.entityKind.setTo({ @@ -610,4 +613,17 @@ describe("createEntity", () => { "strict" >; }); + + it("expect getter return good value", () => { + expect(Object.keys(FormEntity.propertiesDefinition)).toStrictEqual([ + "name", + "type", + "inputs", + "description", + "tags", + "test", + ]); + expect(DDataParser.dataParserKind.has(FormEntity.mapDataParser)).toStrictEqual(true); + expect(DDataParser.dataParserKind.has(FormEntity.internal.mapDataParser)).toStrictEqual(true); + }); }); diff --git a/tests/clean/entity/unwrap.test.ts b/tests/clean/entity/unwrap.test.ts index a0bb2635..98b4d578 100644 --- a/tests/clean/entity/unwrap.test.ts +++ b/tests/clean/entity/unwrap.test.ts @@ -167,8 +167,8 @@ describe("unwrapEntity", () => { }, }); - const Archived = DClean.createFlag("archived"); - const featuredEntity = Archived.append(entity, true); + const Archived = DClean.createFlag("archived"); + const featuredEntity = Archived.append(entity, { value: true }); const result = pipe( featuredEntity, @@ -186,7 +186,7 @@ describe("unwrapEntity", () => { }, _entityName: "Article", _flags: { - archived: true, + archived: { value: true }, }, }); @@ -203,7 +203,7 @@ describe("unwrapEntity", () => { }; readonly _entityName: "Article"; readonly _flags: { - readonly archived: true; + readonly archived: { readonly value: true }; }; }, "strict" diff --git a/tests/clean/flag.test.ts b/tests/clean/flag.test.ts index 7eae39fd..a6945453 100644 --- a/tests/clean/flag.test.ts +++ b/tests/clean/flag.test.ts @@ -1,4 +1,4 @@ -import { DPE, type ExpectType, DClean } from "@scripts"; +import { DPE, type ExpectType, DClean, pipe } from "@scripts"; describe("createFlag", () => { const Id = DClean.createNewType("id", DPE.number()); @@ -14,11 +14,11 @@ describe("createFlag", () => { }); it("creates a handler with the provided name", () => { - const isAdmin = DClean.createFlag("isAdmin"); + const isAdmin = DClean.createFlag("isAdmin"); type check = ExpectType< DClean.GetFlag, - DClean.Flag<"isAdmin", boolean>, + DClean.Flag<"isAdmin", { value: boolean }>, "strict" >; @@ -26,13 +26,13 @@ describe("createFlag", () => { }); it("appends flags and merges existing flag data", () => { - const isAdmin = DClean.createFlag("isAdmin"); - const beta = DClean.createFlag("beta"); + const isAdmin = DClean.createFlag("isAdmin"); + const beta = DClean.createFlag("beta"); const marker = DClean.createFlag("marker"); - const withAdmin = isAdmin.append(baseUser, true); - expect(DClean.flagKind.getValue(withAdmin)).toEqual({ isAdmin: true }); - expect(isAdmin.getValue(withAdmin)).toStrictEqual(true); + const withAdmin = isAdmin.append(baseUser, { value: true }); + expect(DClean.flagKind.getValue(withAdmin)).toEqual({ isAdmin: { value: true } }); + expect(isAdmin.getValue(withAdmin)).toStrictEqual({ value: true }); type Check1 = ExpectType< typeof withAdmin, @@ -41,17 +41,17 @@ describe("createFlag", () => { & { readonly id: DClean.NewType<"id", 1, never>; } - & DClean.Flag<"isAdmin", true> + & DClean.Flag<"isAdmin", { readonly value: true }> ), "strict" >; - const withBeta = beta.append(withAdmin, "on"); + const withBeta = beta.append(withAdmin, { value: "on" }); expect(DClean.flagKind.getValue(withBeta)).toEqual({ - isAdmin: true, - beta: "on", + isAdmin: { value: true }, + beta: { value: "on" }, }); - expect(beta.getValue(withBeta)).toStrictEqual("on"); + expect(beta.getValue(withBeta)).toStrictEqual({ value: "on" }); type Check2 = ExpectType< typeof withBeta, @@ -60,16 +60,16 @@ describe("createFlag", () => { & { readonly id: DClean.NewType<"id", 1, never>; } - & DClean.Flag<"isAdmin", true> - & DClean.Flag<"beta", "on"> + & DClean.Flag<"isAdmin", { readonly value: true }> + & DClean.Flag<"beta", { readonly value: "on" }> ), "strict" >; const withMarker = marker.append(withBeta); expect(DClean.flagKind.getValue(withMarker)).toEqual({ - isAdmin: true, - beta: "on", + isAdmin: { value: true }, + beta: { value: "on" }, marker: undefined, }); expect(marker.getValue(withMarker)).toStrictEqual(undefined); @@ -81,8 +81,8 @@ describe("createFlag", () => { & { readonly id: DClean.NewType<"id", 1, never>; } - & DClean.Flag<"isAdmin", true> - & DClean.Flag<"beta", "on"> + & DClean.Flag<"isAdmin", { readonly value: true }> + & DClean.Flag<"beta", { readonly value: "on" }> & DClean.Flag<"marker", never> ), "strict" @@ -93,20 +93,20 @@ describe("createFlag", () => { }); it("checks if a flag is present on the entity", () => { - const isAdmin = DClean.createFlag("isAdmin"); + const isAdmin = DClean.createFlag("isAdmin"); const marker = DClean.createFlag("marker"); expect(isAdmin.has(baseUser)).toBe(false); expect(marker.has(baseUser)).toBe(false); - const withAdmin = isAdmin.append(baseUser, true); + const withAdmin = isAdmin.append(baseUser, { value: true }); if (isAdmin.has(withAdmin)) { type check = ExpectType< typeof withAdmin, DClean.Entity<"User"> & { readonly id: DClean.NewType<"id", 1, never>; - } & DClean.Flag<"isAdmin", true>, + } & DClean.Flag<"isAdmin", { readonly value: true }>, "strict" >; } @@ -116,4 +116,37 @@ describe("createFlag", () => { const withMarker = marker.append(baseUser); expect(marker.has(withMarker)).toBe(true); }); + + it("append in pipe", () => { + const isAdmin = DClean.createFlag("isAdmin"); + const marker = DClean.createFlag("marker"); + + const entityIsAdmin = pipe( + baseUser, + isAdmin.append({ value: false }), + ); + + type check = ExpectType< + typeof entityIsAdmin, + DClean.Entity<"User"> & { + readonly id: DClean.NewType<"id", 1, never>; + } & DClean.Flag<"isAdmin", { + readonly value: false; + }>, + "strict" + >; + + const entityWithMarker = pipe( + baseUser, + marker.append, + ); + + type check1 = ExpectType< + typeof entityWithMarker, + DClean.Entity<"User"> & { + readonly id: DClean.NewType<"id", number, never>; + } & DClean.Flag<"marker", never>, + "strict" + >; + }); }); diff --git a/tests/common/memoObject.test.ts b/tests/common/memoObject.test.ts new file mode 100644 index 00000000..5e3ccee1 --- /dev/null +++ b/tests/common/memoObject.test.ts @@ -0,0 +1,58 @@ +import { memoObject, pipe, type ExpectType } from "@scripts"; + +describe("memoObject", () => { + it("memoizes the getter and proxies get/set operations", () => { + const getter = vi.fn(() => ({ + count: 1 as const, + })); + const result = memoObject(getter); + + expect(getter).not.toBeCalled(); + + const firstCount = result.count; + + expect(firstCount).toBe(1); + expect(getter).toBeCalledTimes(1); + + result.count = 2 as never; + expect(result.count).toBe(2); + expect(getter).toBeCalledTimes(1); + + type check = ExpectType< + typeof firstCount, + 1, + "strict" + >; + }); + + it("exposes keys and descriptors through proxy traps", () => { + const result = memoObject(() => ({ + foo: 1, + bar: 2, + })); + + expect("foo" in result).toBe(true); + expect("missing" in result).toBe(false); + expect(Object.keys(result)).toEqual(["foo", "bar"]); + expect(Object.getOwnPropertyDescriptor(result, "foo")).toMatchObject({ + enumerable: true, + configurable: true, + }); + }); + + it("keeps a memoized key list after keys have been read once", () => { + const result = memoObject(() => ({ + foo: 1, + } as { + foo: number; + bar?: number; + })); + + expect(Object.keys(result)).toEqual(["foo"]); + + result.bar = 2; + expect(result.bar).toBe(2); + expect(Object.keys(result)).toEqual(["foo", "bar"]); + expect("bar" in result).toBe(true); + }); +});