Skip to content

Commit

Permalink
feat: minor api changes, perf refactors
Browse files Browse the repository at this point in the history
  • Loading branch information
isaac-mason committed Feb 20, 2024
1 parent 12259b3 commit 68cb2b8
Show file tree
Hide file tree
Showing 13 changed files with 358 additions and 246 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-swans-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arancini/react": minor
---

feat: throw meaningful errors when using hooks and components outside of required contexts
27 changes: 27 additions & 0 deletions .changeset/light-mice-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@arancini/core": minor
"arancini": minor
---

feat: remove world.id and world.entity

Computing ids based on object identity is easy to do in userland if required. See below:

```ts
let entityIdCounter = 0
const entityToId = new Map<E, number>()
const idToEntity = new Map<number, E>()

const getEntityId = (entity: E) => {
let id = entityToId.get(entity)

if (id === undefined) {
id = entityIdCounter++
entityToId.set(entity, id)
}

return id
}

const getEntityById = (id: number) => idToEntity.get(id)
```
6 changes: 6 additions & 0 deletions .changeset/lovely-oranges-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@arancini/core": minor
"arancini": minor
---

feat: remove query.destroy(), use world.destroyQuery instead
6 changes: 6 additions & 0 deletions .changeset/neat-parrots-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@arancini/core": patch
"arancini": patch
---

feat: normalize 'not' conditions
6 changes: 6 additions & 0 deletions .changeset/serious-rice-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@arancini/core": patch
"arancini": patch
---

fix: return correct type from world.create
6 changes: 6 additions & 0 deletions .changeset/spicy-cooks-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@arancini/core": patch
"arancini": patch
---

feat: minor refactors for iteration performance
5 changes: 5 additions & 0 deletions .changeset/tender-cobras-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arancini/react": patch
---

feat: improve performance by removing a state update and rerender when initially adding components
7 changes: 5 additions & 2 deletions packages/arancini-core/src/entity-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export class EntityCollection<Entity> {
_entityPositions = new Map<Entity, number>()

get first(): Entity | undefined {
return this.entities[0] || undefined
return this.entities[0]
}

get size() {
return this.entities.length
}

[Symbol.iterator]() {
Expand Down Expand Up @@ -70,7 +74,6 @@ export const removeFromCollection = <E extends AnyEntity>(
collection.entities[index] = other
collection._entityPositions.set(other, index)
}

collection.entities.pop()

collection.version++
Expand Down
121 changes: 57 additions & 64 deletions packages/arancini-core/src/query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { EntityCollection } from './entity-collection'
import type { World } from './world'

export type With<T, P extends keyof T> = T & Required<Pick<T, P>>

Expand All @@ -26,35 +25,73 @@ export type QueryCondition<Entity> = {

export type QueryConditions<Entity> = QueryCondition<Entity>[]

export type QueryFn<Entity, ResultEntity> = (q: QueryBuilder<Entity>) => QueryBuilder<ResultEntity>
export type QueryFn<Entity, ResultEntity> = (
q: QueryBuilder<Entity>
) => QueryBuilder<ResultEntity>

export class Query<Entity> extends EntityCollection<Entity> {
references = new Set<unknown>()

constructor(
public world: World,
public key: string,
public conditions: QueryConditions<Entity>
public dedupe: string,
public conditions: QueryConditions<Entity>,
) {
super()
}
}

export const prepareQuery = (
queryFn: QueryFn<any, any>
): { conditions: QueryConditions<any>; dedupe: string } => {
/* evaluate queryFn */
const queryBuilder = new QueryBuilder()
queryFn(queryBuilder)
const queryBuilderConditions = queryBuilder.conditions

/* validate conditions */
if (queryBuilderConditions.length <= 0) {
throw new Error('Query must have at least one condition')
}

destroy() {
this.world.destroyQuery(this)
if (queryBuilderConditions.some((condition) => condition.components.length <= 0)) {
throw new Error('Query conditions must have at least one component')
}
}

export const getQueryResults = <Entity>(
queryConditions: QueryConditions<Entity>,
entities: Iterable<Entity>
): Entity[] => {
const matches: Entity[] = []
/* normalize conditions */
const normalisedConditions: QueryConditions<any> = []

for (const entity of entities) {
if (evaluateQueryConditions(queryConditions, entity)) {
matches.push(entity)
const combinedAllCondition: QueryCondition<any> = { type: 'all', components: [] }
const combinedNotCondition: QueryCondition<any> = { type: 'not', components: [] }

for (const condition of queryBuilderConditions) {
if (condition.type === 'all') {
combinedAllCondition.components.push(...condition.components)
} else if (condition.type === 'not') {
combinedNotCondition.components.push(...condition.components)
} else {
normalisedConditions.push(condition)
}
}

return matches
if (combinedAllCondition.components.length > 0) {
normalisedConditions.push(combinedAllCondition)
}
if (combinedNotCondition.components.length > 0) {
normalisedConditions.push(combinedNotCondition)
}

/* create query dedupe string */
const dedupe = normalisedConditions
.map(({ type, components }) => {
return `${type}(${components.sort().join(', ')})`
})
.sort()
.join(' && ')

return {
conditions: normalisedConditions,
dedupe,
}
}

export const evaluateQueryConditions = <Entity>(
Expand All @@ -79,58 +116,12 @@ export const evaluateQueryConditions = <Entity>(
return true
}

export const getQueryConditions = (
queryFn: QueryFn<any, any>
): QueryConditions<any> => {
/* get conditions */
const queryBuilder = new QueryBuilder()
queryFn(queryBuilder)
const queryConditions = queryBuilder.conditions

/* validate conditions */
if (queryConditions.length <= 0) {
throw new Error('Query must have at least one condition')
}

if (queryConditions.some((condition) => condition.components.length <= 0)) {
throw new Error('Query conditions must have at least one component')
}

/* combine the 'all' conditions */
const allCondition: QueryCondition<any> = { type: 'all', components: [] }
const others: QueryConditions<any> = []

for (const condition of queryConditions) {
if (condition.type === 'all') {
allCondition.components.push(...condition.components)
} else {
others.push(condition)
}
}

return [allCondition, ...others]
}

export const getQueryDedupeString = (
queryConditions: QueryConditions<unknown>
): string => {
return queryConditions
.map(({ type, components }) => {
if (type === 'all') {
return components.sort().join(',')
}

return [`${type}:${components.sort().join(',')}`]
})
.sort()
.join('&')
}

export class QueryBuilder<Entity> {
T!: Entity

conditions: QueryConditions<Entity> = []

/* conditions */
all = <C extends keyof Entity>(...components: C[]) => {
this.conditions.push({ type: 'all', components })
return this as unknown as QueryBuilder<With<Entity, C>>
Expand All @@ -146,6 +137,7 @@ export class QueryBuilder<Entity> {
return this as unknown as QueryBuilder<Without<Entity, C>>
}

/* condition aliases */
with = this.all
have = this.all
has = this.all
Expand All @@ -158,6 +150,7 @@ export class QueryBuilder<Entity> {
none = this.not
without = this.not

/* no-op grammar */
get and() {
return this
}
Expand Down
Loading

0 comments on commit 68cb2b8

Please sign in to comment.