Skip to content

fix: add force option to invalidate callback #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 31 additions & 25 deletions docs/basics/descriptor.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ get: (host: Element, lastValue: any) => {
}
```

* **arguments**:
* `host` - an element instance
* `lastValue` - last cached value of the property
* **returns (required)**:
* `nextValue` - a value of the current state of the property
- **arguments**:
- `host` - an element instance
- `lastValue` - last cached value of the property
- **returns (required)**:
- `nextValue` - a value of the current state of the property

`get` calculates the current property value. The returned value is always cached. The cache mechanism works between properties defined by the library (even between different elements). If your `get` method does not use other properties, it won't be called again (then, the only way to update the value is to assert new value or call `invalidate` from `connect` method).

Expand All @@ -79,8 +79,8 @@ In the following example, the `get` method of the `name` property is called agai

```javascript
const MyElement = {
firstName: 'John',
lastName: 'Smith',
firstName: "John",
lastName: "Smith",
name: {
get: ({ firstName, lastName }) => `${firstName} ${lastName}`,
},
Expand All @@ -102,12 +102,12 @@ set: (host: Element, value: any, lastValue: any) => {
}
```

* **arguments**:
* `host` - an element instance
* `value` - a value passed to assertion (ex., `el.myProperty = 'new value'`)
* `lastValue` - last cached value of the property
* **returns (required)**:
* `nextValue` - a value of the property, which replaces cached value
- **arguments**:
- `host` - an element instance
- `value` - a value passed to assertion (ex., `el.myProperty = 'new value'`)
- `lastValue` - last cached value of the property
- **returns (required)**:
- `nextValue` - a value of the property, which replaces cached value

Every assertion of the property calls `set` method (like `myElement.property = 'new value'`). If returned `nextValue` is not equal to `lastValue`, cache of the property invalidates. However, `set` method does not trigger `get` method automatically. Only the next access to the property (like `const value = myElement.property`) calls `get` method. Then `get` takes `nextValue` from `set` as the `lastValue` argument, calculates `value` and returns it.

Expand All @@ -118,7 +118,7 @@ const MyElement = {
power: {
set: (host, value) => value ** value,
},
}
};

myElement.power = 10; // calls 'set' method and set cache to 100
console.log(myElement.power); // Cache returns 100
Expand All @@ -142,12 +142,12 @@ connect: (host: Element, key: string, invalidate: Function) => {
}
```

* **arguments**:
* `host` - an element instance
* `key` - a property key name
* `invalidate` - a callback function, which invalidates cached value
* **returns (not required):**
* `disconnect` - a function (without arguments)
- **arguments**:
- `host` - an element instance
- `key` - a property key name
- `invalidate` - a callback function, which invalidates cached value
- **returns (not required):**
- `disconnect` - a function (without arguments)

When you insert, remove or relocate an element in the DOM tree, `connect` or `disconnect` is called synchronously (in the `connectedCallback` and `disconnectedCallback` callbacks of the Custom Elements API).

Expand All @@ -156,7 +156,7 @@ You can use `connect` to attach event listeners, initialize property value (usin
If the third party code is responsible for the property value, you can use `invalidate` callback to notify that value should be recalculated (within next access). For example, it can be used to connect to async web APIs or external libraries:

```javascript
import reduxStore from './store';
import reduxStore from "./store";

const MyElement = {
name: {
Expand All @@ -166,6 +166,12 @@ const MyElement = {
};
```

`invalidate` can take an options object argument.

| Option | Type | Default | Description |
| ------ | --------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| force | `boolean` | false | When true, the invalidate call will always trigger a rerender, even if the property's identity has not changed. |

> Click and play with [redux](redux.js.org) library integration example:
>
> [![Edit <redux-counter> web component built with hybrids library](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/redux-counter-web-component-built-with-hybrids-library-jrqzp?file=/src/ReduxCounter.js)
Expand All @@ -181,10 +187,10 @@ observe: (host: Element, value: any, lastValue: any) => {
}
```

* **arguments**:
* `host` - an element instance
* `value` - current value of the property
* `lastValue` - last cached value of the property
- **arguments**:
- `host` - an element instance
- `value` - current value of the property
- `lastValue` - last cached value of the property

When property cache invalidates (directly by the assertion or when one of the dependency invalidates) and `observe` method is set, the change detection mechanism adds the property to the internal queue. Within the next animation frame (using `requestAnimationFrame`) properties from the queue are checked if they have changed, and if they did, `observe` method of the property is called. It means, that `observe` method is asynchronous by default, and it is only called for properties, which value is different in the time of execution of the queue (in the `requestAnimationFrame` call).

Expand Down
18 changes: 11 additions & 7 deletions src/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,32 +189,36 @@ function deleteEntry(entry) {
gcList.add(entry);
}

function invalidateEntry(entry, clearValue, deleteValue) {
function invalidateEntry(entry, options) {
entry.depState = 0;
dispatchDeep(entry);

if (clearValue) {
if (options.clearValue) {
entry.value = undefined;
values.delete(entry);
}

if (deleteValue) {
if (options.deleteEntry) {
deleteEntry(entry);
}

if (options.force) {
entry.state += 1;
}
}

export function invalidate(target, key, clearValue, deleteValue) {
export function invalidate(target, key, options = {}) {
if (contexts.size) {
throw Error(
`Invalidating property in chain of get calls is forbidden: '${key}'`,
);
}

const entry = getEntry(target, key);
invalidateEntry(entry, clearValue, deleteValue);
invalidateEntry(entry, options);
}

export function invalidateAll(target, clearValue, deleteValue) {
export function invalidateAll(target, options = {}) {
if (contexts.size) {
throw Error(
"Invalidating all properties in chain of get calls is forbidden",
Expand All @@ -224,7 +228,7 @@ export function invalidateAll(target, clearValue, deleteValue) {
const targetMap = entries.get(target);
if (targetMap) {
targetMap.forEach(entry => {
invalidateEntry(entry, clearValue, deleteValue);
invalidateEntry(entry, options);
});
}
}
Expand Down
16 changes: 8 additions & 8 deletions src/define.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ function compile(Hybrid, descriptors) {

if (config.connect) {
callbacks.push(host =>
config.connect(host, key, () => {
cache.invalidate(host, key);
config.connect(host, key, options => {
cache.invalidate(host, key, {
force: options && options.force === true,
});
}),
);
}
Expand Down Expand Up @@ -102,13 +104,11 @@ function update(Hybrid, lastHybrids) {

Object.keys(hybrids).forEach(key => {
const type = typeof hybrids[key];
cache.invalidate(
node,
key,
const clearValue =
type !== "object" &&
type !== "function" &&
hybrids[key] !== prevHybrids[key],
);
type !== "function" &&
hybrids[key] !== prevHybrids[key];
cache.invalidate(node, key, { clearValue });
});

node.connectedCallback();
Expand Down
8 changes: 4 additions & 4 deletions src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ function setupModel(Model, nested) {
invalidate: () => {
if (!invalidatePromise) {
invalidatePromise = resolvedPromise.then(() => {
cache.invalidate(config, config, true);
cache.invalidate(config, config, { clearValue: true });
invalidatePromise = null;
});
}
Expand Down Expand Up @@ -1040,14 +1040,14 @@ function clear(model, clearValue = true) {
}

if (config) {
cache.invalidate(config, model.id, clearValue, true);
cache.invalidate(config, model.id, { clearValue, deleteEntry: true });
} else {
if (!configs.get(model) && !lists.get(model[0])) {
throw Error(
"Model definition must be used before - passed argument is probably not a model definition",
);
}
cache.invalidateAll(bootstrap(model), clearValue, true);
cache.invalidateAll(bootstrap(model), { clearValue, deleteEntry: true });
}
}

Expand Down Expand Up @@ -1271,7 +1271,7 @@ function store(Model, options = {}) {
},
connect: options.draft
? (host, key) => () => {
cache.invalidate(host, key, true);
cache.invalidate(host, key, { clearValue: true });
clear(Model, false);
}
: undefined,
Expand Down
17 changes: 15 additions & 2 deletions test/spec/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ describe("cache:", () => {
expect(spy).not.toHaveBeenCalled();
});

it("runs getter again if force option is true", () => {
Object.defineProperty(target, "otherKey", {
get: () => get(target, "otherKey", () => "value"),
});

get(target, "key", () => target.otherKey);
invalidate(target, "otherKey", { force: true });

get(target, "key", spy);

expect(spy).toHaveBeenCalledTimes(1);
});

it("forces getter to be called if validation fails", () => {
get(target, "key", () => get(target, "otherKey", () => "value"));
get(target, "key", spy, () => false);
Expand Down Expand Up @@ -140,7 +153,7 @@ describe("cache:", () => {

it("clears cached value", () => {
get(target, "key", () => "value");
invalidate(target, "key", true);
invalidate(target, "key", { clearValue: true });

get(target, "key", spy);
expect(spy).toHaveBeenCalledWith(target, undefined);
Expand Down Expand Up @@ -172,7 +185,7 @@ describe("cache:", () => {

expect(getEntries(target).length).toBe(1);

invalidateAll(target, true);
invalidateAll(target, { clearValue: true });
expect(getEntries(target)).toEqual([
jasmine.objectContaining({
value: undefined,
Expand Down
6 changes: 5 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ export = hybrids;
export as namespace hybrids;

declare namespace hybrids {
interface InvalidateOptions {
force?: boolean;
}

interface Descriptor<E, V> {
get?(host: E & HTMLElement, lastValue: V | undefined): V;
set?(host: E & HTMLElement, value: any, lastValue: V | undefined): V;
connect?(
host: E & HTMLElement & { [property in keyof E]: V },
key: keyof E,
invalidate: Function,
invalidate: (options?: InvalidateOptions) => void,
): Function | void;
observe?(host: E & HTMLElement, value: V, lastValue: V): void;
}
Expand Down