Skip to content

Commit 71cdb2f

Browse files
committed
feat(core): add invalidates option to properties
A property can name aggregate invalidations it raises; the node's commit runs each one's handler once, after the properties, however many of them raised it.
1 parent e3a2ff0 commit 71cdb2f

3 files changed

Lines changed: 135 additions & 4 deletions

File tree

packages/core/ui/core/native-updates/native-updates.spec.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Style } from '../../styling/style';
55
import { CssProperty, Property } from '../properties';
66
import { NativeUpdateBatch } from './batch';
77
import { NativeUpdates } from './scheduler';
8+
import { Invalidation, InvalidationPhase } from './invalidation';
89

910
/** `SuspendType.Loaded`; the enum is internal but the bit is part of the field's contract. */
1011
const Loaded = 1 << 20;
@@ -50,6 +51,31 @@ const threeProperty = new CssProperty<Style, string>({ name: 'three', cssName: '
5051
threeProperty.register(Style);
5152
trackNative(threeProperty, TestView);
5253

54+
const Content = new Invalidation('content', { phase: InvalidationPhase.Content });
55+
56+
/** Raises `Content` and has a `[setNative]` of its own. */
57+
const fourProperty = new Property<TestView, string>({ name: 'four', defaultValue: 'four-default', invalidates: [Content] });
58+
fourProperty.register(TestView);
59+
trackNative(fourProperty, TestView);
60+
61+
/** Raises `Content` and nothing else: no `[setNative]` is installed for it. */
62+
const fiveProperty = new Property<TestView, string>({ name: 'five', defaultValue: 'five-default', invalidates: [Content] });
63+
fiveProperty.register(TestView);
64+
65+
/** Raises `Content` on a class that handles neither the property nor the invalidation. */
66+
class BareView extends View {
67+
createNativeView(): Object {
68+
return {};
69+
}
70+
}
71+
72+
const sixProperty = new Property<BareView, string>({ name: 'six', defaultValue: 'six-default', invalidates: [Content] });
73+
sixProperty.register(BareView);
74+
75+
(TestView.prototype as any)[Content.apply] = function (batch: NativeUpdateBatch) {
76+
log.push(`content(${String(batch.node.constructor.name)})`);
77+
};
78+
5379
function loaded<T extends TestView>(view: T): T {
5480
(<any>view)._setupUI({});
5581
view.callLoaded();
@@ -374,3 +400,81 @@ describe('flushNativeUpdates', () => {
374400
expect(log).toEqual(['one=parent', 'one=child']);
375401
});
376402
});
403+
404+
describe('invalidates', () => {
405+
it('runs the aggregate handler after the property write', () => {
406+
const view: any = loaded(new TestView());
407+
408+
view.four = 'a';
409+
410+
expect(log).toEqual(['four=a', 'content(TestView)']);
411+
});
412+
413+
it('runs the aggregate handler for a property with no native setter of its own', () => {
414+
const view: any = loaded(new TestView());
415+
416+
view.five = 'a';
417+
418+
expect(log).toEqual(['content(TestView)']);
419+
});
420+
421+
it('runs the aggregate handler once however many properties raised it', () => {
422+
const view: any = loaded(new TestView());
423+
424+
NativeUpdates.batch(() => {
425+
view.four = 'a';
426+
view.five = 'b';
427+
view.one = 'c';
428+
});
429+
430+
expect(log).toEqual(['four=a', 'one=c', 'content(TestView)']);
431+
});
432+
433+
it('is a batch target an override can pre-empt or drop', () => {
434+
const pending: boolean[] = [];
435+
436+
class ContentFirstView extends TestView {
437+
public commitNativeUpdates(batch: NativeUpdateBatch): void {
438+
pending.push(batch.has(Content));
439+
batch.apply(Content);
440+
batch.skip(fourProperty);
441+
super.commitNativeUpdates(batch);
442+
}
443+
}
444+
const view: any = loaded(new ContentFirstView());
445+
pending.length = 0;
446+
447+
view.four = 'a';
448+
449+
expect(pending).toEqual([true]);
450+
expect(log).toEqual(['content(ContentFirstView)']);
451+
});
452+
453+
it('gives the aggregate handler the value the property held at the last commit', () => {
454+
const seen: unknown[] = [];
455+
456+
class ContentReadingView extends TestView {}
457+
(ContentReadingView.prototype as any)[Content.apply] = function (batch: NativeUpdateBatch) {
458+
seen.push(batch.previous(fourProperty));
459+
};
460+
461+
const view: any = loaded(new ContentReadingView());
462+
seen.length = 0;
463+
464+
view.four = 'a';
465+
view.four = 'b';
466+
467+
expect(seen).toEqual(['four-default', 'a']);
468+
});
469+
470+
it('does nothing on a node with no handler for the invalidation', () => {
471+
const view: any = new BareView();
472+
view._setupUI({});
473+
view.callLoaded();
474+
log.length = 0;
475+
476+
view.six = 'a';
477+
478+
expect(log).toEqual([]);
479+
});
480+
});

packages/core/ui/core/properties/index.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ export function _setDefaultCommitNativeUpdates(commit: unknown): void {
361361
* a commit apply it, right away when nothing is holding native updates back.
362362
*/
363363
function queueOrApplyNative(view: ViewBase, store: any, property: NativeProperty, value: any, write: NativeWrite, oldValue: any): void {
364-
if (view._suspendNativeUpdatesCount !== 0 || scheduler._depth !== 0 || view.commitNativeUpdates !== defaultCommitNativeUpdates) {
364+
if (view._suspendNativeUpdatesCount !== 0 || scheduler._depth !== 0 || view.commitNativeUpdates !== defaultCommitNativeUpdates || property.invalidates !== undefined) {
365365
queueNativeUpdate(view, property, oldValue);
366366

367367
return;
@@ -392,11 +392,12 @@ function queueNativeUpdate(view: ViewBase, property: NativeProperty, oldValue: a
392392
scheduler._hold(view);
393393
}
394394

395+
const invalidates = property.invalidates;
395396
const dirty = view._suspendedUpdates;
396397
if (dirty && view[property.setNative]) {
397-
// `NativeUpdateBatch.previous` is only reachable from a commit hook, so a node whose class
398-
// does not define one records nothing.
399-
if (view.commitNativeUpdates !== defaultCommitNativeUpdates) {
398+
// `NativeUpdateBatch.previous` is only reachable from a commit hook or an aggregate
399+
// handler, so a node with neither records nothing.
400+
if (invalidates !== undefined || view.commitNativeUpdates !== defaultCommitNativeUpdates) {
400401
let previous = view._pendingPrevious;
401402
if (!previous) {
402403
previous = view._pendingPrevious = new Map();
@@ -409,6 +410,17 @@ function queueNativeUpdate(view: ViewBase, property: NativeProperty, oldValue: a
409410
dirty[property.name] = property;
410411
}
411412

413+
if (invalidates) {
414+
let pending = view._pendingInvalidations;
415+
if (!pending) {
416+
pending = view._pendingInvalidations = new Set();
417+
}
418+
419+
for (let i = 0, length = invalidates.length; i < length; i++) {
420+
pending.add(invalidates[i]);
421+
}
422+
}
423+
412424
if (view._suspendNativeUpdatesCount === 0) {
413425
initNativeView(view);
414426
}
@@ -425,6 +437,7 @@ export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<
425437

426438
public readonly defaultValueKey: symbol;
427439
public readonly defaultValue: U;
440+
public readonly invalidates: readonly Invalidation[] | undefined;
428441
public readonly nativeValueChange: (owner: T, value: U) => void;
429442

430443
public isStyleProperty: boolean;
@@ -453,6 +466,7 @@ export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<
453466

454467
const defaultValue: U = options.defaultValue;
455468
this.defaultValue = defaultValue;
469+
this.invalidates = options.invalidates;
456470

457471
const eventName = propertyName + 'Change';
458472

@@ -787,6 +801,7 @@ export class CssProperty<T extends Style, U> {
787801
public readonly sourceKey: symbol;
788802
public readonly defaultValueKey: symbol;
789803
public readonly defaultValue: U;
804+
public readonly invalidates: readonly Invalidation[] | undefined;
790805

791806
public overrideHandlers: (options: CssPropertyOptions<T, U>) => void;
792807

@@ -819,6 +834,7 @@ export class CssProperty<T extends Style, U> {
819834

820835
const defaultValue: U = options.defaultValue;
821836
this.defaultValue = defaultValue;
837+
this.invalidates = options.invalidates;
822838

823839
const eventName = propertyName + 'Change';
824840
let affectsLayout: boolean = options.affectsLayout;
@@ -1013,6 +1029,7 @@ export class CssAnimationProperty<T extends Style, U> implements CssAnimationPro
10131029
private readonly source: symbol;
10141030

10151031
public readonly defaultValue: U;
1032+
public readonly invalidates: readonly Invalidation[] | undefined;
10161033

10171034
public isStyleProperty: boolean;
10181035

@@ -1050,6 +1067,7 @@ export class CssAnimationProperty<T extends Style, U> implements CssAnimationPro
10501067
this.defaultValueKey = defaultValueKey;
10511068

10521069
this.defaultValue = options.defaultValue;
1070+
this.invalidates = options.invalidates;
10531071

10541072
const cssValue = Symbol(cssName);
10551073
const styleValue = Symbol(`local:${propertyName}`);

packages/core/ui/core/properties/property-shared.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Shared property types, interfaces, and value helpers for properties and view-base modules.
22
// Only put platform-agnostic logic here.
33

4+
import type { Invalidation } from '../native-updates/invalidation';
5+
46
/**
57
* Value specifying that Property should be set to its initial value.
68
*/
@@ -13,6 +15,11 @@ export interface PropertyOptions<T, U> {
1315
readonly equalityComparer?: (x: U, y: U) => boolean;
1416
readonly valueChanged?: (target: T, oldValue: U, newValue: U) => void;
1517
readonly valueConverter?: (value: string) => U;
18+
/**
19+
* Aggregate invalidations this property raises, applied by the node's commit after the
20+
* property's own `[setNative]`, once per commit however many properties raised them.
21+
*/
22+
readonly invalidates?: readonly Invalidation[];
1623
}
1724

1825
export interface CoerciblePropertyOptions<T, U> extends PropertyOptions<T, U> {
@@ -48,6 +55,8 @@ export interface CssAnimationPropertyOptions<T, U> {
4855
readonly equalityComparer?: (x: U, y: U) => boolean;
4956
readonly valueChanged?: (target: T, oldValue: U, newValue: U) => void;
5057
readonly valueConverter?: (value: string) => U;
58+
/** @see PropertyOptions.invalidates */
59+
readonly invalidates?: readonly Invalidation[];
5160
}
5261

5362
export function isCssUnsetValue(value: any): boolean {

0 commit comments

Comments
 (0)