Skip to content

Commit

Permalink
Batching support (#122)
Browse files Browse the repository at this point in the history
* Very naive batching implementation

Co-authored-by: Omar <Shenato@users.noreply.github.com>

* Batches effects of other batches

Co-authored-by: Omar <Shenato@users.noreply.github.com>

* Extract batch to a separated module and make it apply to Facet by default

* WIP: logging extra usage

* Remove log and schedule on effects

* Logs scheduler success ration

* Rename batch into scheduler

* Documentation

* Only starts a batch on createFacet if we are notifying a change

* Also test useFacetEffect

* Keep batch started until the end

Making sure to exhaust all tasks before exiting

* Fix linter

* Expose batch as a public API

* Small refactor

* Make sure to cancel tasks that are no longer needed

Needs unit tests

* Testing order of execution of scheduled tasks within batches

* Fix cancelation of tasks

Still needs unit tests (as previous implementation proved to not be sufficient)

* Rename canceled to scheduled

* Uses an array to keep track of tasks

* Using a "regular for" should be faster

More info: https://esbench.com/bench/6317fc2a6c89f600a5701bc9

* Unit test canceling tasks

* Cleanup development log, plus some extra docs

* Avoids scheduling for first event within a batch

Also properly handles exceptions within a batch.

---------

Co-authored-by: Omar <Shenato@users.noreply.github.com>
  • Loading branch information
pirelenito and Shenato committed Jun 15, 2023
1 parent 7105eff commit c5c7887
Show file tree
Hide file tree
Showing 7 changed files with 464 additions and 42 deletions.
11 changes: 7 additions & 4 deletions packages/@react-facet/core/src/facet/createFacet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defaultEqualityCheck } from '../equalityChecks'
import { Cleanup, EqualityCheck, Listener, WritableFacet, StartSubscription, Option, NO_VALUE } from '../types'
import { batch } from '../scheduler'

export interface FacetOptions<V> {
initialValue: Option<V>
Expand Down Expand Up @@ -42,11 +43,13 @@ export function createFacet<V>({
}
}

currentValue = newValue
batch(() => {
currentValue = newValue

for (const listener of listeners) {
listener(currentValue)
}
for (const listener of listeners) {
listener(currentValue)
}
})
}

/**
Expand Down
24 changes: 17 additions & 7 deletions packages/@react-facet/core/src/hooks/useFacetEffect.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useLayoutEffect } from 'react'
import { Facet, Unsubscribe, Cleanup, NO_VALUE, ExtractFacetValues } from '../types'
import { cancelScheduledTask, scheduleTask } from '../scheduler'

export const createUseFacetEffect = (useHook: typeof useEffect | typeof useLayoutEffect) => {
return function <Y extends Facet<unknown>[], T extends [...Y]>(
Expand Down Expand Up @@ -34,22 +35,31 @@ export const createUseFacetEffect = (useHook: typeof useEffect | typeof useLayou
const unsubscribes: Unsubscribe[] = []
const values: unknown[] = facets.map(() => NO_VALUE)

const task = () => {
hasAllDependencies = hasAllDependencies || values.every((value) => value != NO_VALUE)

if (hasAllDependencies) {
if (cleanup != null) {
cleanup()
}

cleanup = effectMemoized(...(values as ExtractFacetValues<T>))
}
}

facets.forEach((facet, index) => {
unsubscribes[index] = facet.observe((value) => {
values[index] = value
hasAllDependencies = hasAllDependencies || values.every((value) => value != NO_VALUE)

if (hasAllDependencies) {
if (cleanup != null) {
cleanup()
}

cleanup = effectMemoized(...(values as ExtractFacetValues<T>))
scheduleTask(task)
} else {
task()
}
})
})

return () => {
cancelScheduledTask(task)
unsubscribes.forEach((unsubscribe) => unsubscribe())
if (cleanup != null) {
cleanup()
Expand Down
1 change: 1 addition & 0 deletions packages/@react-facet/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './helpers'
export * from './hooks'
export * from './mapFacets'
export * from './types'
export { batch } from './scheduler'
55 changes: 24 additions & 31 deletions packages/@react-facet/core/src/mapFacets/mapIntoObserveArray.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cancelScheduledTask, scheduleTask } from '../scheduler'
import { defaultEqualityCheck } from '../equalityChecks'
import { EqualityCheck, Listener, Option, NO_VALUE, Observe, Facet, NoValue } from '../types'

Expand All @@ -14,31 +15,22 @@ export function mapIntoObserveArray<M>(
const dependencyValues: Option<unknown>[] = facets.map(() => NO_VALUE)
let hasAllDependencies = false

const subscriptions = facets.map((facet, index) => {
// Most common scenario is not having any equality check
if (equalityCheck == null) {
return facet.observe((value) => {
dependencyValues[index] = value

hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
const task =
checker == null
? () => {
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
if (!hasAllDependencies) return

if (hasAllDependencies) {
const result = fn(...dependencyValues)
if (result === NO_VALUE) return

listener(result)
}
})
}

// Then we optimize for the second most common scenario of using the defaultEqualityCheck (by inline its implementation)
if (equalityCheck === defaultEqualityCheck) {
return facet.observe((value) => {
dependencyValues[index] = value

hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
: equalityCheck === defaultEqualityCheck
? () => {
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
if (!hasAllDependencies) return

if (hasAllDependencies) {
const result = fn(...dependencyValues)
if (result === NO_VALUE) return

Expand All @@ -59,29 +51,30 @@ export function mapIntoObserveArray<M>(

listener(result)
}
})
}
: () => {
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
if (!hasAllDependencies) return

// Just a type-check guard, it will never happen
if (checker == null) return () => {}
const result = fn(...dependencyValues)
if (result === NO_VALUE) return
if (checker(result)) return

// Finally we use the custom equality check
listener(result)
}

const subscriptions = facets.map((facet, index) => {
return facet.observe((value) => {
dependencyValues[index] = value

hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)

if (hasAllDependencies) {
const result = fn(...dependencyValues)
if (result === NO_VALUE) return
if (checker(result)) return

listener(result)
scheduleTask(task)
} else {
task()
}
})
})

return () => {
cancelScheduledTask(task)
subscriptions.forEach((unsubscribe) => unsubscribe())
}
}
Expand Down

0 comments on commit c5c7887

Please sign in to comment.