Skip to content

Commit c5c7887

Browse files
pirelenitoShenato
andauthored
Batching support (#122)
* Very naive batching implementation Co-authored-by: Omar <[email protected]> * Batches effects of other batches Co-authored-by: Omar <[email protected]> * 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 <[email protected]>
1 parent 7105eff commit c5c7887

File tree

7 files changed

+464
-42
lines changed

7 files changed

+464
-42
lines changed

packages/@react-facet/core/src/facet/createFacet.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defaultEqualityCheck } from '../equalityChecks'
22
import { Cleanup, EqualityCheck, Listener, WritableFacet, StartSubscription, Option, NO_VALUE } from '../types'
3+
import { batch } from '../scheduler'
34

45
export interface FacetOptions<V> {
56
initialValue: Option<V>
@@ -42,11 +43,13 @@ export function createFacet<V>({
4243
}
4344
}
4445

45-
currentValue = newValue
46+
batch(() => {
47+
currentValue = newValue
4648

47-
for (const listener of listeners) {
48-
listener(currentValue)
49-
}
49+
for (const listener of listeners) {
50+
listener(currentValue)
51+
}
52+
})
5053
}
5154

5255
/**

packages/@react-facet/core/src/hooks/useFacetEffect.tsx

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback, useEffect, useLayoutEffect } from 'react'
22
import { Facet, Unsubscribe, Cleanup, NO_VALUE, ExtractFacetValues } from '../types'
3+
import { cancelScheduledTask, scheduleTask } from '../scheduler'
34

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

38+
const task = () => {
39+
hasAllDependencies = hasAllDependencies || values.every((value) => value != NO_VALUE)
40+
41+
if (hasAllDependencies) {
42+
if (cleanup != null) {
43+
cleanup()
44+
}
45+
46+
cleanup = effectMemoized(...(values as ExtractFacetValues<T>))
47+
}
48+
}
49+
3750
facets.forEach((facet, index) => {
3851
unsubscribes[index] = facet.observe((value) => {
3952
values[index] = value
40-
hasAllDependencies = hasAllDependencies || values.every((value) => value != NO_VALUE)
41-
4253
if (hasAllDependencies) {
43-
if (cleanup != null) {
44-
cleanup()
45-
}
46-
47-
cleanup = effectMemoized(...(values as ExtractFacetValues<T>))
54+
scheduleTask(task)
55+
} else {
56+
task()
4857
}
4958
})
5059
})
5160

5261
return () => {
62+
cancelScheduledTask(task)
5363
unsubscribes.forEach((unsubscribe) => unsubscribe())
5464
if (cleanup != null) {
5565
cleanup()

packages/@react-facet/core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './helpers'
77
export * from './hooks'
88
export * from './mapFacets'
99
export * from './types'
10+
export { batch } from './scheduler'

packages/@react-facet/core/src/mapFacets/mapIntoObserveArray.ts

+24-31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cancelScheduledTask, scheduleTask } from '../scheduler'
12
import { defaultEqualityCheck } from '../equalityChecks'
23
import { EqualityCheck, Listener, Option, NO_VALUE, Observe, Facet, NoValue } from '../types'
34

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

17-
const subscriptions = facets.map((facet, index) => {
18-
// Most common scenario is not having any equality check
19-
if (equalityCheck == null) {
20-
return facet.observe((value) => {
21-
dependencyValues[index] = value
22-
23-
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
18+
const task =
19+
checker == null
20+
? () => {
21+
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
22+
if (!hasAllDependencies) return
2423

25-
if (hasAllDependencies) {
2624
const result = fn(...dependencyValues)
2725
if (result === NO_VALUE) return
2826

2927
listener(result)
3028
}
31-
})
32-
}
33-
34-
// Then we optimize for the second most common scenario of using the defaultEqualityCheck (by inline its implementation)
35-
if (equalityCheck === defaultEqualityCheck) {
36-
return facet.observe((value) => {
37-
dependencyValues[index] = value
38-
39-
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
29+
: equalityCheck === defaultEqualityCheck
30+
? () => {
31+
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
32+
if (!hasAllDependencies) return
4033

41-
if (hasAllDependencies) {
4234
const result = fn(...dependencyValues)
4335
if (result === NO_VALUE) return
4436

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

6052
listener(result)
6153
}
62-
})
63-
}
54+
: () => {
55+
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
56+
if (!hasAllDependencies) return
6457

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

68-
// Finally we use the custom equality check
62+
listener(result)
63+
}
64+
65+
const subscriptions = facets.map((facet, index) => {
6966
return facet.observe((value) => {
7067
dependencyValues[index] = value
71-
72-
hasAllDependencies = hasAllDependencies || dependencyValues.every((value) => value != NO_VALUE)
73-
7468
if (hasAllDependencies) {
75-
const result = fn(...dependencyValues)
76-
if (result === NO_VALUE) return
77-
if (checker(result)) return
78-
79-
listener(result)
69+
scheduleTask(task)
70+
} else {
71+
task()
8072
}
8173
})
8274
})
8375

8476
return () => {
77+
cancelScheduledTask(task)
8578
subscriptions.forEach((unsubscribe) => unsubscribe())
8679
}
8780
}

0 commit comments

Comments
 (0)