joist-core 2.4.0-next.1 → 2.4.0-next.2
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/build/IndexManager.cjs
CHANGED
|
@@ -86,6 +86,7 @@ var IndexManager = class {
|
|
|
86
86
|
indexes.set(fieldName, entry);
|
|
87
87
|
}
|
|
88
88
|
for (const entity of entities) {
|
|
89
|
+
if (entity.isDeletedEntity) continue;
|
|
89
90
|
const value = getFieldValue(entity, fieldName, entry.field);
|
|
90
91
|
entry.index.add(value, entity);
|
|
91
92
|
}
|
|
@@ -101,6 +102,7 @@ var IndexManager = class {
|
|
|
101
102
|
if (entry === void 0 || entry.indexedCount === entities.length) return;
|
|
102
103
|
for (let i = entry.indexedCount; i < entities.length; i++) {
|
|
103
104
|
const entity = entities[i];
|
|
105
|
+
if (entity.isDeletedEntity) continue;
|
|
104
106
|
const value = getFieldValue(entity, fieldName, entry.field);
|
|
105
107
|
entry.index.add(value, entity);
|
|
106
108
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndexManager.cjs","names":["#indexes","groupBy","getMetadata","isLoadedReference","#valueToEntities","isEntity"],"sources":["../src/IndexManager.ts"],"sourcesContent":["import { type Entity, isEntity } from \"src/Entity.ts\";\nimport { type EntityMetadata, type Field, getMetadata } from \"src/EntityMetadata.ts\";\nimport { type ManyToOneReference, type PolymorphicReference, isLoadedReference } from \"src/relations/index.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\ntype FieldValue = any;\ntype FieldName = string;\ntype EntityTag = string;\ntype FieldIndexEntry = {\n field: Field;\n index: FieldIndex;\n /** Tracks how far into the EM's list-per-tag array this field index has been lazily populated. */\n indexedCount: number;\n};\nconst emptySet = new Set<Entity>();\n\n// The test reproducing a n^2 with n=500 went from 100ms to 50ms if indexed\nexport const indexThreshold = 500;\n\n/**\n * IndexManager provides field-based indexing for entity queries to avoid O(n) linear scans of `em.entities`.\n *\n * Key features:\n * - Only indexes entity types with >1000 entities to avoid overhead for small datasets\n * - Supports dual indexing for m2o/poly fields (by ID for saved, by instance for unsaved)\n * - Automatically maintains indexes when fields are updated via setField()\n */\nexport class IndexManager {\n readonly #indexes: Map<EntityTag, Map<FieldName, FieldIndexEntry>> = new Map();\n\n /** @return if we should index entities of this type/count. */\n shouldIndexType(entityCount: number): boolean {\n return entityCount >= indexThreshold;\n }\n\n /** Visible for testing. */\n isIndexed(tagName: string): boolean {\n return this.#indexes.has(tagName);\n }\n\n /** Enables indexing for an entity type and builds indexes for the requested fields. */\n enableIndexingForType<T extends Entity>(meta: EntityMetadata<T>, entities: T[], where: object): void {\n const { tagName } = meta;\n if (!this.#indexes.has(tagName)) {\n this.#indexes.set(tagName, new Map());\n }\n\n const fieldNames = Object.keys(where);\n const fieldIndexes = this.#indexes.get(tagName)!;\n const missingFieldNames = fieldNames.filter((fieldName) => !fieldIndexes.has(fieldName));\n if (missingFieldNames.length === 0) return;\n\n // If subtypes are involved, group by each subtype\n if (meta.baseType || meta.subTypes.length > 0) {\n [...groupBy(entities, (e) => getMetadata(e)).entries()].forEach(([meta, entities]) => {\n this.addEntitiesToIndex(meta, entities, missingFieldNames);\n });\n } else {\n this.addEntitiesToIndex(meta, entities, missingFieldNames);\n }\n }\n\n /** Updates indexes when a field value changes. */\n maybeUpdateFieldIndex(entity: Entity, fieldName: string, oldValue: any, newValue: any): void {\n // Fast return for the common case, which is no indexing\n if (this.#indexes.size === 0) return;\n\n const meta = getMetadata(entity);\n const fieldIndexes = this.#indexes.get(meta.tagName);\n if (!fieldIndexes) return; // Type not indexed\n\n const fieldIndex = fieldIndexes.get(fieldName)?.index;\n if (!fieldIndex) return; // Field is not indexed\n\n fieldIndex.remove(oldValue, entity);\n fieldIndex.add(newValue, entity);\n }\n\n /**\n * Finds entities matching the given where clause using indexes.\n *\n * Returns the raw candidate set (possibly a live internal index set, so do not mutate it);\n * the caller applies its own subtype/deleted filtering while building the final array.\n */\n findMatching<T extends Entity>(meta: EntityMetadata<T>, entities: T[], where: any): ReadonlySet<T> {\n const { tagName } = meta;\n if (!this.#indexes.has(tagName)) {\n throw new Error(`${meta.type} is not indexed`);\n }\n const fieldIndexes = this.#indexes.get(tagName)!;\n // Start with all entities of the 1st condition, then intersect (AND) each subsequent field in `where`\n let candidates: Set<Entity> | undefined;\n for (const [fieldName, value] of Object.entries(where)) {\n this.indexUnindexedEntities(entities, fieldName);\n const fieldCandidates = fieldIndexes.get(fieldName)?.index.get(value);\n if (!candidates) {\n candidates = fieldCandidates ?? emptySet; // This is the 1st clause\n } else {\n candidates = fieldCandidates ? intersectSets(candidates, fieldCandidates) : emptySet;\n }\n // Early exit if no candidates remain\n if (candidates.size === 0) break;\n }\n if (candidates === undefined) {\n throw new Error(`Expected where clause with at least one condition`);\n }\n return candidates as unknown as ReadonlySet<T>;\n }\n\n // `entities` should all be of the exact same subtype\n private addEntitiesToIndex(meta: EntityMetadata, entities: Entity[], fieldNames: string[]): void {\n const { tagName } = meta;\n const indexes = this.#indexes.get(tagName)!;\n // Iterate over each field so we can do a shared fieldIndex lookup\n for (const fieldName of fieldNames) {\n const field = meta.allFields[fieldName];\n if (!field) continue;\n let entry = indexes.get(fieldName);\n if (!entry) {\n entry = { field, index: new FieldIndex(), indexedCount: 0 };\n indexes.set(fieldName, entry);\n }\n for (const entity of entities) {\n const value = getFieldValue(entity, fieldName, entry.field);\n entry.index.add(value, entity);\n }\n entry.indexedCount = entities.length;\n }\n }\n\n /** Lazily indexes newly-registered entities only for fields that are queried. */\n private indexUnindexedEntities<T extends Entity>(entities: T[], fieldName: string): void {\n const first = entities[0];\n if (first === undefined) return;\n\n const meta = getMetadata(first);\n const entry = this.#indexes.get(meta.tagName)?.get(fieldName);\n if (entry === undefined || entry.indexedCount === entities.length) return;\n\n for (let i = entry.indexedCount; i < entities.length; i++) {\n const entity = entities[i];\n const value = getFieldValue(entity, fieldName, entry.field);\n entry.index.add(value, entity);\n }\n entry.indexedCount = entities.length;\n }\n}\n\n// Use the same field access pattern as entityMatches\nfunction getFieldValue(entity: Entity, fieldName: string, field: Field): any {\n const fn = fieldName as keyof Entity;\n switch (field.kind) {\n case \"primaryKey\":\n return entity.idTaggedMaybe;\n case \"enum\":\n case \"primitive\":\n return (entity as any)[fn];\n case \"m2o\":\n case \"poly\":\n const relation = (entity as any)[fn] as\n | ManyToOneReference<Entity, any, any>\n | PolymorphicReference<Entity, any, any>;\n if (isLoadedReference(relation)) {\n return relation.get;\n } else if (relation.isSet) {\n return relation.id;\n } else {\n return undefined;\n }\n default:\n return undefined;\n }\n}\n\n/**\n * For a given field, like `firstName`, holds the reverse index of value `bob` -> entities `[a1, a2]`.\n *\n * This also handles indexes `Entity` values as both instances & IDs, to handle both:\n * - new/unpersisted entities not yet having IDs, and\n * - relations not having loaded their instances yet\n */\nclass FieldIndex {\n readonly #valueToEntities = new Map<FieldValue, Set<Entity>>();\n\n /** @return entities that have `value` as their current value for this field. */\n get(value: any): Set<Entity> | undefined {\n if (isEntity(value) && value.idTaggedMaybe) {\n // If `value` is a persisted entity like `a:1`, and entities could have indexed their `author_id=a:1` field\n // values as the unloaded \"just an a:1 string id\" value, before `a:1` was loaded into memory.\n const matchId = this.#valueToEntities.get(value.idTaggedMaybe);\n // But other entities could also have indexes `a:1` as an instance, so go ahead and check both\n const matchInstance = this.#valueToEntities.get(value);\n if (matchId && matchInstance) {\n return new Set([...matchId, ...matchInstance]);\n }\n return matchId ?? matchInstance;\n }\n // Treat null and undefined as equivalent for unset relations\n if (value === null) value = undefined;\n return this.#valueToEntities.get(value);\n }\n\n add(value: any, entity: Entity): void {\n // If this is an entity, store both the entity itself & its ID\n if (isEntity(value) && value.idTaggedMaybe) {\n this.doAdd(value.idTaggedMaybe, entity);\n }\n this.doAdd(value, entity);\n }\n\n remove(value: any, entity: Entity): void {\n if (isEntity(value) && value.idTaggedMaybe) {\n this.doRemove(value.idTaggedMaybe, entity);\n }\n this.doRemove(value, entity);\n }\n\n private doAdd(value: any, entity: Entity): void {\n const set = this.#valueToEntities.get(value) ?? new Set();\n if (set.size === 0) this.#valueToEntities.set(value, set);\n set.add(entity);\n }\n\n private doRemove(value: any, entity: Entity): void {\n const set = this.#valueToEntities.get(value);\n if (set) {\n set.delete(entity);\n if (set.size === 0) {\n this.#valueToEntities.delete(value);\n }\n }\n }\n}\n\nfunction intersectSets<T>(set1: Set<T>, set2: Set<T>): Set<T> {\n const [smaller, larger] = set1.size <= set2.size ? [set1, set2] : [set2, set1];\n const result = new Set<T>();\n for (const item of smaller) {\n if (larger.has(item)) {\n result.add(item);\n }\n }\n return result;\n}\n"],"mappings":";;;;;;;AAcA,MAAM,2BAAW,IAAI,IAAY;AAGjC,MAAa,iBAAiB;;;;;;;;;AAU9B,IAAa,eAAb,MAA0B;CACxB,2BAAqE,IAAI,IAAI;;CAG7E,gBAAgB,aAA8B;EAC5C,OAAO,eAAA;CACT;;CAGA,UAAU,SAA0B;EAClC,OAAO,KAAKA,SAAS,IAAI,OAAO;CAClC;;CAGA,sBAAwC,MAAyB,UAAe,OAAqB;EACnG,MAAM,EAAE,YAAY;EACpB,IAAI,CAAC,KAAKA,SAAS,IAAI,OAAO,GAC5B,KAAKA,SAAS,IAAI,yBAAS,IAAI,IAAI,CAAC;EAGtC,MAAM,aAAa,OAAO,KAAK,KAAK;EACpC,MAAM,eAAe,KAAKA,SAAS,IAAI,OAAO;EAC9C,MAAM,oBAAoB,WAAW,QAAQ,cAAc,CAAC,aAAa,IAAI,SAAS,CAAC;EACvF,IAAI,kBAAkB,WAAW,GAAG;EAGpC,IAAI,KAAK,YAAY,KAAK,SAAS,SAAS,GAC1C,CAAC,GAAGC,cAAAA,QAAQ,WAAW,MAAMC,uBAAAA,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,cAAc;GACpF,KAAK,mBAAmB,MAAM,UAAU,iBAAiB;EAC3D,CAAC;OAED,KAAK,mBAAmB,MAAM,UAAU,iBAAiB;CAE7D;;CAGA,sBAAsB,QAAgB,WAAmB,UAAe,UAAqB;EAE3F,IAAI,KAAKF,SAAS,SAAS,GAAG;EAE9B,MAAM,OAAOE,uBAAAA,YAAY,MAAM;EAC/B,MAAM,eAAe,KAAKF,SAAS,IAAI,KAAK,OAAO;EACnD,IAAI,CAAC,cAAc;EAEnB,MAAM,aAAa,aAAa,IAAI,SAAS,CAAC,EAAE;EAChD,IAAI,CAAC,YAAY;EAEjB,WAAW,OAAO,UAAU,MAAM;EAClC,WAAW,IAAI,UAAU,MAAM;CACjC;;;;;;;CAQA,aAA+B,MAAyB,UAAe,OAA4B;EACjG,MAAM,EAAE,YAAY;EACpB,IAAI,CAAC,KAAKA,SAAS,IAAI,OAAO,GAC5B,MAAM,IAAI,MAAM,GAAG,KAAK,KAAK,gBAAgB;EAE/C,MAAM,eAAe,KAAKA,SAAS,IAAI,OAAO;EAE9C,IAAI;EACJ,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,KAAK,GAAG;GACtD,KAAK,uBAAuB,UAAU,SAAS;GAC/C,MAAM,kBAAkB,aAAa,IAAI,SAAS,CAAC,EAAE,MAAM,IAAI,KAAK;GACpE,IAAI,CAAC,YACH,aAAa,mBAAmB;QAEhC,aAAa,kBAAkB,cAAc,YAAY,eAAe,IAAI;GAG9E,IAAI,WAAW,SAAS,GAAG;EAC7B;EACA,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,MAAM,mDAAmD;EAErE,OAAO;CACT;CAGA,mBAA2B,MAAsB,UAAoB,YAA4B;EAC/F,MAAM,EAAE,YAAY;EACpB,MAAM,UAAU,KAAKA,SAAS,IAAI,OAAO;EAEzC,KAAK,MAAM,aAAa,YAAY;GAClC,MAAM,QAAQ,KAAK,UAAU;GAC7B,IAAI,CAAC,OAAO;GACZ,IAAI,QAAQ,QAAQ,IAAI,SAAS;GACjC,IAAI,CAAC,OAAO;IACV,QAAQ;KAAE;KAAO,OAAO,IAAI,WAAW;KAAG,cAAc;IAAE;IAC1D,QAAQ,IAAI,WAAW,KAAK;GAC9B;GACA,KAAK,MAAM,UAAU,UAAU;IAC7B,MAAM,QAAQ,cAAc,QAAQ,WAAW,MAAM,KAAK;IAC1D,MAAM,MAAM,IAAI,OAAO,MAAM;GAC/B;GACA,MAAM,eAAe,SAAS;EAChC;CACF;;CAGA,uBAAiD,UAAe,WAAyB;EACvF,MAAM,QAAQ,SAAS;EACvB,IAAI,UAAU,KAAA,GAAW;EAEzB,MAAM,OAAOE,uBAAAA,YAAY,KAAK;EAC9B,MAAM,QAAQ,KAAKF,SAAS,IAAI,KAAK,OAAO,CAAC,EAAE,IAAI,SAAS;EAC5D,IAAI,UAAU,KAAA,KAAa,MAAM,iBAAiB,SAAS,QAAQ;EAEnE,KAAK,IAAI,IAAI,MAAM,cAAc,IAAI,SAAS,QAAQ,KAAK;GACzD,MAAM,SAAS,SAAS;GACxB,MAAM,QAAQ,cAAc,QAAQ,WAAW,MAAM,KAAK;GAC1D,MAAM,MAAM,IAAI,OAAO,MAAM;EAC/B;EACA,MAAM,eAAe,SAAS;CAChC;AACF;AAGA,SAAS,cAAc,QAAgB,WAAmB,OAAmB;CAC3E,MAAM,KAAK;CACX,QAAQ,MAAM,MAAd;EACE,KAAK,cACH,OAAO,OAAO;EAChB,KAAK;EACL,KAAK,aACH,OAAQ,OAAe;EACzB,KAAK;EACL,KAAK;GACH,MAAM,WAAY,OAAe;GAGjC,IAAIG,4BAAAA,kBAAkB,QAAQ,GAC5B,OAAO,SAAS;QACX,IAAI,SAAS,OAClB,OAAO,SAAS;QAEhB;EAEJ,SACE;CACJ;AACF;;;;;;;;AASA,IAAM,aAAN,MAAiB;CACf,mCAA4B,IAAI,IAA6B;;CAG7D,IAAI,OAAqC;EACvC,IAAIE,eAAAA,SAAS,KAAK,KAAK,MAAM,eAAe;GAG1C,MAAM,UAAU,KAAKD,iBAAiB,IAAI,MAAM,aAAa;GAE7D,MAAM,gBAAgB,KAAKA,iBAAiB,IAAI,KAAK;GACrD,IAAI,WAAW,eACb,uBAAO,IAAI,IAAI,CAAC,GAAG,SAAS,GAAG,aAAa,CAAC;GAE/C,OAAO,WAAW;EACpB;EAEA,IAAI,UAAU,MAAM,QAAQ,KAAA;EAC5B,OAAO,KAAKA,iBAAiB,IAAI,KAAK;CACxC;CAEA,IAAI,OAAY,QAAsB;EAEpC,IAAIC,eAAAA,SAAS,KAAK,KAAK,MAAM,eAC3B,KAAK,MAAM,MAAM,eAAe,MAAM;EAExC,KAAK,MAAM,OAAO,MAAM;CAC1B;CAEA,OAAO,OAAY,QAAsB;EACvC,IAAIA,eAAAA,SAAS,KAAK,KAAK,MAAM,eAC3B,KAAK,SAAS,MAAM,eAAe,MAAM;EAE3C,KAAK,SAAS,OAAO,MAAM;CAC7B;CAEA,MAAc,OAAY,QAAsB;EAC9C,MAAM,MAAM,KAAKD,iBAAiB,IAAI,KAAK,qBAAK,IAAI,IAAI;EACxD,IAAI,IAAI,SAAS,GAAG,KAAKA,iBAAiB,IAAI,OAAO,GAAG;EACxD,IAAI,IAAI,MAAM;CAChB;CAEA,SAAiB,OAAY,QAAsB;EACjD,MAAM,MAAM,KAAKA,iBAAiB,IAAI,KAAK;EAC3C,IAAI,KAAK;GACP,IAAI,OAAO,MAAM;GACjB,IAAI,IAAI,SAAS,GACf,KAAKA,iBAAiB,OAAO,KAAK;EAEtC;CACF;AACF;AAEA,SAAS,cAAiB,MAAc,MAAsB;CAC5D,MAAM,CAAC,SAAS,UAAU,KAAK,QAAQ,KAAK,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,IAAI;CAC7E,MAAM,yBAAS,IAAI,IAAO;CAC1B,KAAK,MAAM,QAAQ,SACjB,IAAI,OAAO,IAAI,IAAI,GACjB,OAAO,IAAI,IAAI;CAGnB,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"IndexManager.cjs","names":["#indexes","groupBy","getMetadata","isLoadedReference","#valueToEntities","isEntity"],"sources":["../src/IndexManager.ts"],"sourcesContent":["import { type Entity, isEntity } from \"src/Entity.ts\";\nimport { type EntityMetadata, type Field, getMetadata } from \"src/EntityMetadata.ts\";\nimport { type ManyToOneReference, type PolymorphicReference, isLoadedReference } from \"src/relations/index.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\ntype FieldValue = any;\ntype FieldName = string;\ntype EntityTag = string;\ntype FieldIndexEntry = {\n field: Field;\n index: FieldIndex;\n /** Tracks how far into the EM's list-per-tag array this field index has been lazily populated. */\n indexedCount: number;\n};\nconst emptySet = new Set<Entity>();\n\n// The test reproducing a n^2 with n=500 went from 100ms to 50ms if indexed\nexport const indexThreshold = 500;\n\n/**\n * IndexManager provides field-based indexing for entity queries to avoid O(n) linear scans of `em.entities`.\n *\n * Key features:\n * - Only indexes entity types with >1000 entities to avoid overhead for small datasets\n * - Supports dual indexing for m2o/poly fields (by ID for saved, by instance for unsaved)\n * - Automatically maintains indexes when fields are updated via setField()\n */\nexport class IndexManager {\n readonly #indexes: Map<EntityTag, Map<FieldName, FieldIndexEntry>> = new Map();\n\n /** @return if we should index entities of this type/count. */\n shouldIndexType(entityCount: number): boolean {\n return entityCount >= indexThreshold;\n }\n\n /** Visible for testing. */\n isIndexed(tagName: string): boolean {\n return this.#indexes.has(tagName);\n }\n\n /** Enables indexing for an entity type and builds indexes for the requested fields. */\n enableIndexingForType<T extends Entity>(meta: EntityMetadata<T>, entities: T[], where: object): void {\n const { tagName } = meta;\n if (!this.#indexes.has(tagName)) {\n this.#indexes.set(tagName, new Map());\n }\n\n const fieldNames = Object.keys(where);\n const fieldIndexes = this.#indexes.get(tagName)!;\n const missingFieldNames = fieldNames.filter((fieldName) => !fieldIndexes.has(fieldName));\n if (missingFieldNames.length === 0) return;\n\n // If subtypes are involved, group by each subtype\n if (meta.baseType || meta.subTypes.length > 0) {\n [...groupBy(entities, (e) => getMetadata(e)).entries()].forEach(([meta, entities]) => {\n this.addEntitiesToIndex(meta, entities, missingFieldNames);\n });\n } else {\n this.addEntitiesToIndex(meta, entities, missingFieldNames);\n }\n }\n\n /** Updates indexes when a field value changes. */\n maybeUpdateFieldIndex(entity: Entity, fieldName: string, oldValue: any, newValue: any): void {\n // Fast return for the common case, which is no indexing\n if (this.#indexes.size === 0) return;\n\n const meta = getMetadata(entity);\n const fieldIndexes = this.#indexes.get(meta.tagName);\n if (!fieldIndexes) return; // Type not indexed\n\n const fieldIndex = fieldIndexes.get(fieldName)?.index;\n if (!fieldIndex) return; // Field is not indexed\n\n fieldIndex.remove(oldValue, entity);\n fieldIndex.add(newValue, entity);\n }\n\n /**\n * Finds entities matching the given where clause using indexes.\n *\n * Returns the raw candidate set (possibly a live internal index set, so do not mutate it);\n * the caller applies its own subtype/deleted filtering while building the final array.\n */\n findMatching<T extends Entity>(meta: EntityMetadata<T>, entities: T[], where: any): ReadonlySet<T> {\n const { tagName } = meta;\n if (!this.#indexes.has(tagName)) {\n throw new Error(`${meta.type} is not indexed`);\n }\n const fieldIndexes = this.#indexes.get(tagName)!;\n // Start with all entities of the 1st condition, then intersect (AND) each subsequent field in `where`\n let candidates: Set<Entity> | undefined;\n for (const [fieldName, value] of Object.entries(where)) {\n this.indexUnindexedEntities(entities, fieldName);\n const fieldCandidates = fieldIndexes.get(fieldName)?.index.get(value);\n if (!candidates) {\n candidates = fieldCandidates ?? emptySet; // This is the 1st clause\n } else {\n candidates = fieldCandidates ? intersectSets(candidates, fieldCandidates) : emptySet;\n }\n // Early exit if no candidates remain\n if (candidates.size === 0) break;\n }\n if (candidates === undefined) {\n throw new Error(`Expected where clause with at least one condition`);\n }\n return candidates as unknown as ReadonlySet<T>;\n }\n\n // `entities` should all be of the exact same subtype\n private addEntitiesToIndex(meta: EntityMetadata, entities: Entity[], fieldNames: string[]): void {\n const { tagName } = meta;\n const indexes = this.#indexes.get(tagName)!;\n // Iterate over each field so we can do a shared fieldIndex lookup\n for (const fieldName of fieldNames) {\n const field = meta.allFields[fieldName];\n if (!field) continue;\n let entry = indexes.get(fieldName);\n if (!entry) {\n entry = { field, index: new FieldIndex(), indexedCount: 0 };\n indexes.set(fieldName, entry);\n }\n for (const entity of entities) {\n // Deleted entities never match, and reading a flushed one's relations throws\n if (entity.isDeletedEntity) continue;\n const value = getFieldValue(entity, fieldName, entry.field);\n entry.index.add(value, entity);\n }\n entry.indexedCount = entities.length;\n }\n }\n\n /** Lazily indexes newly-registered entities only for fields that are queried. */\n private indexUnindexedEntities<T extends Entity>(entities: T[], fieldName: string): void {\n const first = entities[0];\n if (first === undefined) return;\n\n const meta = getMetadata(first);\n const entry = this.#indexes.get(meta.tagName)?.get(fieldName);\n if (entry === undefined || entry.indexedCount === entities.length) return;\n\n for (let i = entry.indexedCount; i < entities.length; i++) {\n const entity = entities[i];\n if (entity.isDeletedEntity) continue;\n const value = getFieldValue(entity, fieldName, entry.field);\n entry.index.add(value, entity);\n }\n entry.indexedCount = entities.length;\n }\n}\n\n// Use the same field access pattern as entityMatches\nfunction getFieldValue(entity: Entity, fieldName: string, field: Field): any {\n const fn = fieldName as keyof Entity;\n switch (field.kind) {\n case \"primaryKey\":\n return entity.idTaggedMaybe;\n case \"enum\":\n case \"primitive\":\n return (entity as any)[fn];\n case \"m2o\":\n case \"poly\":\n const relation = (entity as any)[fn] as\n | ManyToOneReference<Entity, any, any>\n | PolymorphicReference<Entity, any, any>;\n if (isLoadedReference(relation)) {\n return relation.get;\n } else if (relation.isSet) {\n return relation.id;\n } else {\n return undefined;\n }\n default:\n return undefined;\n }\n}\n\n/**\n * For a given field, like `firstName`, holds the reverse index of value `bob` -> entities `[a1, a2]`.\n *\n * This also handles indexes `Entity` values as both instances & IDs, to handle both:\n * - new/unpersisted entities not yet having IDs, and\n * - relations not having loaded their instances yet\n */\nclass FieldIndex {\n readonly #valueToEntities = new Map<FieldValue, Set<Entity>>();\n\n /** @return entities that have `value` as their current value for this field. */\n get(value: any): Set<Entity> | undefined {\n if (isEntity(value) && value.idTaggedMaybe) {\n // If `value` is a persisted entity like `a:1`, and entities could have indexed their `author_id=a:1` field\n // values as the unloaded \"just an a:1 string id\" value, before `a:1` was loaded into memory.\n const matchId = this.#valueToEntities.get(value.idTaggedMaybe);\n // But other entities could also have indexes `a:1` as an instance, so go ahead and check both\n const matchInstance = this.#valueToEntities.get(value);\n if (matchId && matchInstance) {\n return new Set([...matchId, ...matchInstance]);\n }\n return matchId ?? matchInstance;\n }\n // Treat null and undefined as equivalent for unset relations\n if (value === null) value = undefined;\n return this.#valueToEntities.get(value);\n }\n\n add(value: any, entity: Entity): void {\n // If this is an entity, store both the entity itself & its ID\n if (isEntity(value) && value.idTaggedMaybe) {\n this.doAdd(value.idTaggedMaybe, entity);\n }\n this.doAdd(value, entity);\n }\n\n remove(value: any, entity: Entity): void {\n if (isEntity(value) && value.idTaggedMaybe) {\n this.doRemove(value.idTaggedMaybe, entity);\n }\n this.doRemove(value, entity);\n }\n\n private doAdd(value: any, entity: Entity): void {\n const set = this.#valueToEntities.get(value) ?? new Set();\n if (set.size === 0) this.#valueToEntities.set(value, set);\n set.add(entity);\n }\n\n private doRemove(value: any, entity: Entity): void {\n const set = this.#valueToEntities.get(value);\n if (set) {\n set.delete(entity);\n if (set.size === 0) {\n this.#valueToEntities.delete(value);\n }\n }\n }\n}\n\nfunction intersectSets<T>(set1: Set<T>, set2: Set<T>): Set<T> {\n const [smaller, larger] = set1.size <= set2.size ? [set1, set2] : [set2, set1];\n const result = new Set<T>();\n for (const item of smaller) {\n if (larger.has(item)) {\n result.add(item);\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndexManager.d.cts","names":[],"sources":["../src/IndexManager.ts"],"mappings":";;;cAiBa;;;;;;;;;cAUA;;;EAIX,gBAAgB;;EAKhB,UAAU;;EAKV,sBAAsB,UAAU,QAAQ,MAAM,eAAe,IAAI,UAAU,KAAK;;EAsBhF,sBAAsB,QAAQ,QAAQ,mBAAmB,eAAe;;;;;;;EAqBxE,aAAa,UAAU,QAAQ,MAAM,eAAe,IAAI,UAAU,KAAK,aAAa,YAAY;UA0BxF;;
|
|
1
|
+
{"version":3,"file":"IndexManager.d.cts","names":[],"sources":["../src/IndexManager.ts"],"mappings":";;;cAiBa;;;;;;;;;cAUA;;;EAIX,gBAAgB;;EAKhB,UAAU;;EAKV,sBAAsB,UAAU,QAAQ,MAAM,eAAe,IAAI,UAAU,KAAK;;EAsBhF,sBAAsB,QAAQ,QAAQ,mBAAmB,eAAe;;;;;;;EAqBxE,aAAa,UAAU,QAAQ,MAAM,eAAe,IAAI,UAAU,KAAK,aAAa,YAAY;UA0BxF;;UAuBA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndexManager.d.mts","names":[],"sources":["../src/IndexManager.ts"],"mappings":";;;cAiBa;;;;;;;;;cAUA;;;EAIX,gBAAgB;;EAKhB,UAAU;;EAKV,sBAAsB,UAAU,QAAQ,MAAM,eAAe,IAAI,UAAU,KAAK;;EAsBhF,sBAAsB,QAAQ,QAAQ,mBAAmB,eAAe;;;;;;;EAqBxE,aAAa,UAAU,QAAQ,MAAM,eAAe,IAAI,UAAU,KAAK,aAAa,YAAY;UA0BxF;;
|
|
1
|
+
{"version":3,"file":"IndexManager.d.mts","names":[],"sources":["../src/IndexManager.ts"],"mappings":";;;cAiBa;;;;;;;;;cAUA;;;EAIX,gBAAgB;;EAKhB,UAAU;;EAKV,sBAAsB,UAAU,QAAQ,MAAM,eAAe,IAAI,UAAU,KAAK;;EAsBhF,sBAAsB,QAAQ,QAAQ,mBAAmB,eAAe;;;;;;;EAqBxE,aAAa,UAAU,QAAQ,MAAM,eAAe,IAAI,UAAU,KAAK,aAAa,YAAY;UA0BxF;;UAuBA"}
|
package/build/IndexManager.js
CHANGED
|
@@ -85,6 +85,7 @@ var IndexManager = class {
|
|
|
85
85
|
indexes.set(fieldName, entry);
|
|
86
86
|
}
|
|
87
87
|
for (const entity of entities) {
|
|
88
|
+
if (entity.isDeletedEntity) continue;
|
|
88
89
|
const value = getFieldValue(entity, fieldName, entry.field);
|
|
89
90
|
entry.index.add(value, entity);
|
|
90
91
|
}
|
|
@@ -100,6 +101,7 @@ var IndexManager = class {
|
|
|
100
101
|
if (entry === void 0 || entry.indexedCount === entities.length) return;
|
|
101
102
|
for (let i = entry.indexedCount; i < entities.length; i++) {
|
|
102
103
|
const entity = entities[i];
|
|
104
|
+
if (entity.isDeletedEntity) continue;
|
|
103
105
|
const value = getFieldValue(entity, fieldName, entry.field);
|
|
104
106
|
entry.index.add(value, entity);
|
|
105
107
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndexManager.js","names":["#indexes","#valueToEntities"],"sources":["../src/IndexManager.ts"],"sourcesContent":["import { type Entity, isEntity } from \"src/Entity.ts\";\nimport { type EntityMetadata, type Field, getMetadata } from \"src/EntityMetadata.ts\";\nimport { type ManyToOneReference, type PolymorphicReference, isLoadedReference } from \"src/relations/index.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\ntype FieldValue = any;\ntype FieldName = string;\ntype EntityTag = string;\ntype FieldIndexEntry = {\n field: Field;\n index: FieldIndex;\n /** Tracks how far into the EM's list-per-tag array this field index has been lazily populated. */\n indexedCount: number;\n};\nconst emptySet = new Set<Entity>();\n\n// The test reproducing a n^2 with n=500 went from 100ms to 50ms if indexed\nexport const indexThreshold = 500;\n\n/**\n * IndexManager provides field-based indexing for entity queries to avoid O(n) linear scans of `em.entities`.\n *\n * Key features:\n * - Only indexes entity types with >1000 entities to avoid overhead for small datasets\n * - Supports dual indexing for m2o/poly fields (by ID for saved, by instance for unsaved)\n * - Automatically maintains indexes when fields are updated via setField()\n */\nexport class IndexManager {\n readonly #indexes: Map<EntityTag, Map<FieldName, FieldIndexEntry>> = new Map();\n\n /** @return if we should index entities of this type/count. */\n shouldIndexType(entityCount: number): boolean {\n return entityCount >= indexThreshold;\n }\n\n /** Visible for testing. */\n isIndexed(tagName: string): boolean {\n return this.#indexes.has(tagName);\n }\n\n /** Enables indexing for an entity type and builds indexes for the requested fields. */\n enableIndexingForType<T extends Entity>(meta: EntityMetadata<T>, entities: T[], where: object): void {\n const { tagName } = meta;\n if (!this.#indexes.has(tagName)) {\n this.#indexes.set(tagName, new Map());\n }\n\n const fieldNames = Object.keys(where);\n const fieldIndexes = this.#indexes.get(tagName)!;\n const missingFieldNames = fieldNames.filter((fieldName) => !fieldIndexes.has(fieldName));\n if (missingFieldNames.length === 0) return;\n\n // If subtypes are involved, group by each subtype\n if (meta.baseType || meta.subTypes.length > 0) {\n [...groupBy(entities, (e) => getMetadata(e)).entries()].forEach(([meta, entities]) => {\n this.addEntitiesToIndex(meta, entities, missingFieldNames);\n });\n } else {\n this.addEntitiesToIndex(meta, entities, missingFieldNames);\n }\n }\n\n /** Updates indexes when a field value changes. */\n maybeUpdateFieldIndex(entity: Entity, fieldName: string, oldValue: any, newValue: any): void {\n // Fast return for the common case, which is no indexing\n if (this.#indexes.size === 0) return;\n\n const meta = getMetadata(entity);\n const fieldIndexes = this.#indexes.get(meta.tagName);\n if (!fieldIndexes) return; // Type not indexed\n\n const fieldIndex = fieldIndexes.get(fieldName)?.index;\n if (!fieldIndex) return; // Field is not indexed\n\n fieldIndex.remove(oldValue, entity);\n fieldIndex.add(newValue, entity);\n }\n\n /**\n * Finds entities matching the given where clause using indexes.\n *\n * Returns the raw candidate set (possibly a live internal index set, so do not mutate it);\n * the caller applies its own subtype/deleted filtering while building the final array.\n */\n findMatching<T extends Entity>(meta: EntityMetadata<T>, entities: T[], where: any): ReadonlySet<T> {\n const { tagName } = meta;\n if (!this.#indexes.has(tagName)) {\n throw new Error(`${meta.type} is not indexed`);\n }\n const fieldIndexes = this.#indexes.get(tagName)!;\n // Start with all entities of the 1st condition, then intersect (AND) each subsequent field in `where`\n let candidates: Set<Entity> | undefined;\n for (const [fieldName, value] of Object.entries(where)) {\n this.indexUnindexedEntities(entities, fieldName);\n const fieldCandidates = fieldIndexes.get(fieldName)?.index.get(value);\n if (!candidates) {\n candidates = fieldCandidates ?? emptySet; // This is the 1st clause\n } else {\n candidates = fieldCandidates ? intersectSets(candidates, fieldCandidates) : emptySet;\n }\n // Early exit if no candidates remain\n if (candidates.size === 0) break;\n }\n if (candidates === undefined) {\n throw new Error(`Expected where clause with at least one condition`);\n }\n return candidates as unknown as ReadonlySet<T>;\n }\n\n // `entities` should all be of the exact same subtype\n private addEntitiesToIndex(meta: EntityMetadata, entities: Entity[], fieldNames: string[]): void {\n const { tagName } = meta;\n const indexes = this.#indexes.get(tagName)!;\n // Iterate over each field so we can do a shared fieldIndex lookup\n for (const fieldName of fieldNames) {\n const field = meta.allFields[fieldName];\n if (!field) continue;\n let entry = indexes.get(fieldName);\n if (!entry) {\n entry = { field, index: new FieldIndex(), indexedCount: 0 };\n indexes.set(fieldName, entry);\n }\n for (const entity of entities) {\n const value = getFieldValue(entity, fieldName, entry.field);\n entry.index.add(value, entity);\n }\n entry.indexedCount = entities.length;\n }\n }\n\n /** Lazily indexes newly-registered entities only for fields that are queried. */\n private indexUnindexedEntities<T extends Entity>(entities: T[], fieldName: string): void {\n const first = entities[0];\n if (first === undefined) return;\n\n const meta = getMetadata(first);\n const entry = this.#indexes.get(meta.tagName)?.get(fieldName);\n if (entry === undefined || entry.indexedCount === entities.length) return;\n\n for (let i = entry.indexedCount; i < entities.length; i++) {\n const entity = entities[i];\n const value = getFieldValue(entity, fieldName, entry.field);\n entry.index.add(value, entity);\n }\n entry.indexedCount = entities.length;\n }\n}\n\n// Use the same field access pattern as entityMatches\nfunction getFieldValue(entity: Entity, fieldName: string, field: Field): any {\n const fn = fieldName as keyof Entity;\n switch (field.kind) {\n case \"primaryKey\":\n return entity.idTaggedMaybe;\n case \"enum\":\n case \"primitive\":\n return (entity as any)[fn];\n case \"m2o\":\n case \"poly\":\n const relation = (entity as any)[fn] as\n | ManyToOneReference<Entity, any, any>\n | PolymorphicReference<Entity, any, any>;\n if (isLoadedReference(relation)) {\n return relation.get;\n } else if (relation.isSet) {\n return relation.id;\n } else {\n return undefined;\n }\n default:\n return undefined;\n }\n}\n\n/**\n * For a given field, like `firstName`, holds the reverse index of value `bob` -> entities `[a1, a2]`.\n *\n * This also handles indexes `Entity` values as both instances & IDs, to handle both:\n * - new/unpersisted entities not yet having IDs, and\n * - relations not having loaded their instances yet\n */\nclass FieldIndex {\n readonly #valueToEntities = new Map<FieldValue, Set<Entity>>();\n\n /** @return entities that have `value` as their current value for this field. */\n get(value: any): Set<Entity> | undefined {\n if (isEntity(value) && value.idTaggedMaybe) {\n // If `value` is a persisted entity like `a:1`, and entities could have indexed their `author_id=a:1` field\n // values as the unloaded \"just an a:1 string id\" value, before `a:1` was loaded into memory.\n const matchId = this.#valueToEntities.get(value.idTaggedMaybe);\n // But other entities could also have indexes `a:1` as an instance, so go ahead and check both\n const matchInstance = this.#valueToEntities.get(value);\n if (matchId && matchInstance) {\n return new Set([...matchId, ...matchInstance]);\n }\n return matchId ?? matchInstance;\n }\n // Treat null and undefined as equivalent for unset relations\n if (value === null) value = undefined;\n return this.#valueToEntities.get(value);\n }\n\n add(value: any, entity: Entity): void {\n // If this is an entity, store both the entity itself & its ID\n if (isEntity(value) && value.idTaggedMaybe) {\n this.doAdd(value.idTaggedMaybe, entity);\n }\n this.doAdd(value, entity);\n }\n\n remove(value: any, entity: Entity): void {\n if (isEntity(value) && value.idTaggedMaybe) {\n this.doRemove(value.idTaggedMaybe, entity);\n }\n this.doRemove(value, entity);\n }\n\n private doAdd(value: any, entity: Entity): void {\n const set = this.#valueToEntities.get(value) ?? new Set();\n if (set.size === 0) this.#valueToEntities.set(value, set);\n set.add(entity);\n }\n\n private doRemove(value: any, entity: Entity): void {\n const set = this.#valueToEntities.get(value);\n if (set) {\n set.delete(entity);\n if (set.size === 0) {\n this.#valueToEntities.delete(value);\n }\n }\n }\n}\n\nfunction intersectSets<T>(set1: Set<T>, set2: Set<T>): Set<T> {\n const [smaller, larger] = set1.size <= set2.size ? [set1, set2] : [set2, set1];\n const result = new Set<T>();\n for (const item of smaller) {\n if (larger.has(item)) {\n result.add(item);\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"IndexManager.js","names":["#indexes","#valueToEntities"],"sources":["../src/IndexManager.ts"],"sourcesContent":["import { type Entity, isEntity } from \"src/Entity.ts\";\nimport { type EntityMetadata, type Field, getMetadata } from \"src/EntityMetadata.ts\";\nimport { type ManyToOneReference, type PolymorphicReference, isLoadedReference } from \"src/relations/index.ts\";\nimport { groupBy } from \"src/utils.ts\";\n\ntype FieldValue = any;\ntype FieldName = string;\ntype EntityTag = string;\ntype FieldIndexEntry = {\n field: Field;\n index: FieldIndex;\n /** Tracks how far into the EM's list-per-tag array this field index has been lazily populated. */\n indexedCount: number;\n};\nconst emptySet = new Set<Entity>();\n\n// The test reproducing a n^2 with n=500 went from 100ms to 50ms if indexed\nexport const indexThreshold = 500;\n\n/**\n * IndexManager provides field-based indexing for entity queries to avoid O(n) linear scans of `em.entities`.\n *\n * Key features:\n * - Only indexes entity types with >1000 entities to avoid overhead for small datasets\n * - Supports dual indexing for m2o/poly fields (by ID for saved, by instance for unsaved)\n * - Automatically maintains indexes when fields are updated via setField()\n */\nexport class IndexManager {\n readonly #indexes: Map<EntityTag, Map<FieldName, FieldIndexEntry>> = new Map();\n\n /** @return if we should index entities of this type/count. */\n shouldIndexType(entityCount: number): boolean {\n return entityCount >= indexThreshold;\n }\n\n /** Visible for testing. */\n isIndexed(tagName: string): boolean {\n return this.#indexes.has(tagName);\n }\n\n /** Enables indexing for an entity type and builds indexes for the requested fields. */\n enableIndexingForType<T extends Entity>(meta: EntityMetadata<T>, entities: T[], where: object): void {\n const { tagName } = meta;\n if (!this.#indexes.has(tagName)) {\n this.#indexes.set(tagName, new Map());\n }\n\n const fieldNames = Object.keys(where);\n const fieldIndexes = this.#indexes.get(tagName)!;\n const missingFieldNames = fieldNames.filter((fieldName) => !fieldIndexes.has(fieldName));\n if (missingFieldNames.length === 0) return;\n\n // If subtypes are involved, group by each subtype\n if (meta.baseType || meta.subTypes.length > 0) {\n [...groupBy(entities, (e) => getMetadata(e)).entries()].forEach(([meta, entities]) => {\n this.addEntitiesToIndex(meta, entities, missingFieldNames);\n });\n } else {\n this.addEntitiesToIndex(meta, entities, missingFieldNames);\n }\n }\n\n /** Updates indexes when a field value changes. */\n maybeUpdateFieldIndex(entity: Entity, fieldName: string, oldValue: any, newValue: any): void {\n // Fast return for the common case, which is no indexing\n if (this.#indexes.size === 0) return;\n\n const meta = getMetadata(entity);\n const fieldIndexes = this.#indexes.get(meta.tagName);\n if (!fieldIndexes) return; // Type not indexed\n\n const fieldIndex = fieldIndexes.get(fieldName)?.index;\n if (!fieldIndex) return; // Field is not indexed\n\n fieldIndex.remove(oldValue, entity);\n fieldIndex.add(newValue, entity);\n }\n\n /**\n * Finds entities matching the given where clause using indexes.\n *\n * Returns the raw candidate set (possibly a live internal index set, so do not mutate it);\n * the caller applies its own subtype/deleted filtering while building the final array.\n */\n findMatching<T extends Entity>(meta: EntityMetadata<T>, entities: T[], where: any): ReadonlySet<T> {\n const { tagName } = meta;\n if (!this.#indexes.has(tagName)) {\n throw new Error(`${meta.type} is not indexed`);\n }\n const fieldIndexes = this.#indexes.get(tagName)!;\n // Start with all entities of the 1st condition, then intersect (AND) each subsequent field in `where`\n let candidates: Set<Entity> | undefined;\n for (const [fieldName, value] of Object.entries(where)) {\n this.indexUnindexedEntities(entities, fieldName);\n const fieldCandidates = fieldIndexes.get(fieldName)?.index.get(value);\n if (!candidates) {\n candidates = fieldCandidates ?? emptySet; // This is the 1st clause\n } else {\n candidates = fieldCandidates ? intersectSets(candidates, fieldCandidates) : emptySet;\n }\n // Early exit if no candidates remain\n if (candidates.size === 0) break;\n }\n if (candidates === undefined) {\n throw new Error(`Expected where clause with at least one condition`);\n }\n return candidates as unknown as ReadonlySet<T>;\n }\n\n // `entities` should all be of the exact same subtype\n private addEntitiesToIndex(meta: EntityMetadata, entities: Entity[], fieldNames: string[]): void {\n const { tagName } = meta;\n const indexes = this.#indexes.get(tagName)!;\n // Iterate over each field so we can do a shared fieldIndex lookup\n for (const fieldName of fieldNames) {\n const field = meta.allFields[fieldName];\n if (!field) continue;\n let entry = indexes.get(fieldName);\n if (!entry) {\n entry = { field, index: new FieldIndex(), indexedCount: 0 };\n indexes.set(fieldName, entry);\n }\n for (const entity of entities) {\n // Deleted entities never match, and reading a flushed one's relations throws\n if (entity.isDeletedEntity) continue;\n const value = getFieldValue(entity, fieldName, entry.field);\n entry.index.add(value, entity);\n }\n entry.indexedCount = entities.length;\n }\n }\n\n /** Lazily indexes newly-registered entities only for fields that are queried. */\n private indexUnindexedEntities<T extends Entity>(entities: T[], fieldName: string): void {\n const first = entities[0];\n if (first === undefined) return;\n\n const meta = getMetadata(first);\n const entry = this.#indexes.get(meta.tagName)?.get(fieldName);\n if (entry === undefined || entry.indexedCount === entities.length) return;\n\n for (let i = entry.indexedCount; i < entities.length; i++) {\n const entity = entities[i];\n if (entity.isDeletedEntity) continue;\n const value = getFieldValue(entity, fieldName, entry.field);\n entry.index.add(value, entity);\n }\n entry.indexedCount = entities.length;\n }\n}\n\n// Use the same field access pattern as entityMatches\nfunction getFieldValue(entity: Entity, fieldName: string, field: Field): any {\n const fn = fieldName as keyof Entity;\n switch (field.kind) {\n case \"primaryKey\":\n return entity.idTaggedMaybe;\n case \"enum\":\n case \"primitive\":\n return (entity as any)[fn];\n case \"m2o\":\n case \"poly\":\n const relation = (entity as any)[fn] as\n | ManyToOneReference<Entity, any, any>\n | PolymorphicReference<Entity, any, any>;\n if (isLoadedReference(relation)) {\n return relation.get;\n } else if (relation.isSet) {\n return relation.id;\n } else {\n return undefined;\n }\n default:\n return undefined;\n }\n}\n\n/**\n * For a given field, like `firstName`, holds the reverse index of value `bob` -> entities `[a1, a2]`.\n *\n * This also handles indexes `Entity` values as both instances & IDs, to handle both:\n * - new/unpersisted entities not yet having IDs, and\n * - relations not having loaded their instances yet\n */\nclass FieldIndex {\n readonly #valueToEntities = new Map<FieldValue, Set<Entity>>();\n\n /** @return entities that have `value` as their current value for this field. */\n get(value: any): Set<Entity> | undefined {\n if (isEntity(value) && value.idTaggedMaybe) {\n // If `value` is a persisted entity like `a:1`, and entities could have indexed their `author_id=a:1` field\n // values as the unloaded \"just an a:1 string id\" value, before `a:1` was loaded into memory.\n const matchId = this.#valueToEntities.get(value.idTaggedMaybe);\n // But other entities could also have indexes `a:1` as an instance, so go ahead and check both\n const matchInstance = this.#valueToEntities.get(value);\n if (matchId && matchInstance) {\n return new Set([...matchId, ...matchInstance]);\n }\n return matchId ?? matchInstance;\n }\n // Treat null and undefined as equivalent for unset relations\n if (value === null) value = undefined;\n return this.#valueToEntities.get(value);\n }\n\n add(value: any, entity: Entity): void {\n // If this is an entity, store both the entity itself & its ID\n if (isEntity(value) && value.idTaggedMaybe) {\n this.doAdd(value.idTaggedMaybe, entity);\n }\n this.doAdd(value, entity);\n }\n\n remove(value: any, entity: Entity): void {\n if (isEntity(value) && value.idTaggedMaybe) {\n this.doRemove(value.idTaggedMaybe, entity);\n }\n this.doRemove(value, entity);\n }\n\n private doAdd(value: any, entity: Entity): void {\n const set = this.#valueToEntities.get(value) ?? new Set();\n if (set.size === 0) this.#valueToEntities.set(value, set);\n set.add(entity);\n }\n\n private doRemove(value: any, entity: Entity): void {\n const set = this.#valueToEntities.get(value);\n if (set) {\n set.delete(entity);\n if (set.size === 0) {\n this.#valueToEntities.delete(value);\n }\n }\n }\n}\n\nfunction intersectSets<T>(set1: Set<T>, set2: Set<T>): Set<T> {\n const [smaller, larger] = set1.size <= set2.size ? [set1, set2] : [set2, set1];\n const result = new Set<T>();\n for (const item of smaller) {\n if (larger.has(item)) {\n result.add(item);\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joist-core",
|
|
3
|
-
"version": "2.4.0-next.
|
|
3
|
+
"version": "2.4.0-next.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"build"
|
|
43
43
|
],
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"joist-utils": "2.4.0-next.
|
|
45
|
+
"joist-utils": "2.4.0-next.2"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"ansis": "^4.3.1",
|