Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/smooth-jars-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/task': major
---

Add pluggable task args equality functions and deepArrayEquals. Breaking: this removes performTask() and shouldRun() protected methods.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,10 @@ packages/labs/ssr-react/enable-lit-ssr.*
packages/labs/task/development/
packages/labs/task/test/
packages/labs/task/node_modules/
packages/labs/task/deep-equals.*
packages/labs/task/index.*
packages/labs/task/task.*

packages/labs/testing/index.*
packages/labs/testing/fixtures.*
packages/labs/testing/web-test-runner-ssr-plugin.*
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,10 @@ packages/labs/ssr-react/enable-lit-ssr.*
packages/labs/task/development/
packages/labs/task/test/
packages/labs/task/node_modules/
packages/labs/task/deep-equals.*
packages/labs/task/index.*
packages/labs/task/task.*

packages/labs/testing/index.*
packages/labs/testing/fixtures.*
packages/labs/testing/web-test-runner-ssr-plugin.*
Expand Down
3 changes: 2 additions & 1 deletion packages/labs/task/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/development/
/test/
/node_modules/
/deep-equals.*
/index.*
/task.*
/task.*
8 changes: 8 additions & 0 deletions packages/labs/task/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ class MyElement extends LitElement {
}
```

### Argument equality

Task accepts an `argsEqual` to determine if a task should auto-run by testing a new arguments array for equality against the previous run's arguments array.

The default equality function is `shallowArrayEquals`, which compares each argument in the array against the previous array with `notEqual` from `@lit/reactive-element` (which itself uses `===`). This works well if your arguments are primitive values like strings.

If your arguments are objects, you will want to use a more sophisticated equality function. Task provides `deepArrayEquals` in the `deep-equals.js` module, which compares each argument with a `deepEquals` function that can handle primitives, objects, Arrays, Maps, Sets, RegExps, or objects that implement `toString()` or `toValue()`.

## Contributing

Please see [CONTRIBUTING.md](../../../CONTRIBUTING.md).
7 changes: 7 additions & 0 deletions packages/labs/task/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
"development": "./development/index.js",
"default": "./index.js"
},
"./deep-equals.js": {
"types": "./development/deep-equals.d.ts",
"development": "./development/deep-equals.js",
"default": "./deep-equals.js"
},
"./task.js": {
"types": "./development/task.d.ts",
"development": "./development/task.js",
Expand All @@ -31,6 +36,7 @@
"files": [
"/development/",
"!/development/test/",
"/deep-equals.{d.ts,d.ts.map,js,js.map}",
"/index.{d.ts,d.ts.map,js,js.map}",
"/task.{d.ts,d.ts.map,js,js.map}"
],
Expand Down Expand Up @@ -92,6 +98,7 @@
"../../../rollup-common.js"
],
"output": [
"deep-equals.js{,.map}",
"index.js{,.map}",
"task.js{,.map}",
"test/**/*.js{,.map}"
Expand Down
2 changes: 1 addition & 1 deletion packages/labs/task/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import {createRequire} from 'module';

export default litProdConfig({
packageName: createRequire(import.meta.url)('./package.json').name,
entryPoints: ['index', 'task'],
entryPoints: ['deep-equals', 'index', 'task'],
external: ['@lit/reactive-element'],
});
124 changes: 124 additions & 0 deletions packages/labs/task/src/deep-equals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

export const deepArrayEquals = <T extends ReadonlyArray<unknown>>(
oldArgs: T,
newArgs: T
) =>
oldArgs === newArgs ||
(oldArgs.length === newArgs.length &&
oldArgs.every((v, i) => deepEquals(v, newArgs[i])));

const objectValueOf = Object.prototype.valueOf;
const objectToString = Object.prototype.toString;
const {keys: objectKeys} = Object;
const {isArray} = Array;

/**
* Recursively checks two objects for equality.
*
* This function handles the following cases:
* - Primitives: primitives compared with Object.is()
* - Objects: to be equal, two objects must:
* - have the same constructor
* - have same set of own property names
* - have each own property be deeply equal
* - Arrays, Maps, Sets, and RegExps
* - Objects with custom valueOf() (ex: Date)
* - Objects with custom toString() (ex: URL)
*
* Important: Objects must be free of cycles, otherwise this function will
* run infinitely!
*/
export const deepEquals = (a: unknown, b: unknown): boolean => {
if (Object.is(a, b)) {
return true;
}

if (
a !== null &&
b !== null &&
typeof a === 'object' &&
typeof b === 'object'
) {
// Object must have the same prototype / constructor
if (a.constructor !== b.constructor) {
return false;
}

// Arrays must have the same length and recursively equal items
if (isArray(a)) {
if (a.length !== (b as Array<unknown>).length) {
return false;
}
return a.every((v, i) => deepEquals(v, (b as Array<unknown>)[i]));
}

// Defer to custom valueOf implementations. This handles Dates which return
// ms since epoch: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/valueOf
if (a.valueOf !== objectValueOf) {
return a.valueOf() === b.valueOf();
}

// Defer to custom toString implementations. This should handle
// TrustedTypes, URLs, and such. This might be a bit risky, but
// fast-deep-equals does it.
if (a.toString !== objectToString) {
return a.toString() === b.toString();
}

if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) {
return false;
}
for (const [k, v] of a.entries()) {
if (
deepEquals(v, b.get(k)) === false ||
(v === undefined && b.has(k) === false)
) {
return false;
}
}
return true;
}

if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) {
return false;
}
for (const k of a.keys()) {
if (b.has(k) === false) {
return false;
}
}
return true;
}

if (a instanceof RegExp) {
return (
a.source === (b as RegExp).source && a.flags === (b as RegExp).flags
);
}

// We have two objects, check every key
const keys = objectKeys(a) as Array<keyof typeof a>;

if (keys.length !== objectKeys(b).length) {
return false;
}

for (const key of keys) {
if (!b.hasOwnProperty(key) || !deepEquals(a[key], b[key])) {
return false;
}
}

// All keys in the two objects have been compared!
return true;
}

return false;
};
74 changes: 51 additions & 23 deletions packages/labs/task/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ export interface TaskConfig<T extends ReadonlyArray<unknown>, R> {
*/
autoRun?: boolean | 'afterUpdate';

/**
* A function that determines if the current arg and previous args arrays are
* equal. If the argsEqual function returns true, the task will not auto-run.
*
* The default is {@linkcode shallowArrayEquals}. {@linkcode deepArrayEquals}
* is also available.
*/
argsEqual?: (oldArgs: T, newArgs: T) => boolean;

/**
* If initialValue is provided, the task is initialized to the COMPLETE
* status and the value is set to initialData.
Expand Down Expand Up @@ -162,7 +171,8 @@ export class Task<
> {
private _previousArgs?: T;
private _task: TaskFunction<T, R>;
private _getArgs?: ArgsFunction<T>;
private _argsFn?: ArgsFunction<T>;
private _argsEqual: (oldArgs: T, newArgs: T) => boolean;
private _callId = 0;
private _host: ReactiveControllerHost;
private _value?: R;
Expand Down Expand Up @@ -232,7 +242,8 @@ export class Task<
const taskConfig =
typeof task === 'object' ? task : ({task, args} as TaskConfig<T, R>);
this._task = taskConfig.task;
this._getArgs = taskConfig.args;
this._argsFn = taskConfig.args;
this._argsEqual = taskConfig.argsEqual ?? shallowArrayEquals;
this._onComplete = taskConfig.onComplete;
this._onError = taskConfig.onError;
this.autoRun = taskConfig.autoRun ?? true;
Expand All @@ -247,34 +258,47 @@ export class Task<

hostUpdate() {
if (this.autoRun === true) {
this.performTask();
this._performTask();
}
}

hostUpdated() {
if (this.autoRun === 'afterUpdate') {
this.performTask();
this._performTask();
}
}

protected async performTask() {
const args = this._getArgs?.();
if (this.shouldRun(args)) {
await this.run(args);
private _getArgs() {
if (this._argsFn === undefined) {
return undefined;
}
const args = this._argsFn();
if (!Array.isArray(args)) {
throw new Error('The args function must return an array');
}
return args;
}

/**
* Determines if the task should run when it's triggered because of a
* host update. A task should run when its arguments change from the
* previous run.
* host update, and runs the task if it should.
*
* Note: this is not checked when `run` is explicitly called.
* A task should run when its arguments change from the previous run, based on
* the args equality function.
*
* @param args The task's arguments
* This method is side-effectful: it stores the new args as the previous args.
*/
protected shouldRun(args?: T) {
return this._argsDirty(args);
private async _performTask() {
const args = this._getArgs();
const prev = this._previousArgs;
this._previousArgs = args;
if (
args !== prev &&
args !== undefined &&
(prev === undefined || !this._argsEqual(prev, args))
) {
await this.run(args);
}
}

/**
Expand All @@ -288,7 +312,11 @@ export class Task<
* this run.
*/
async run(args?: T) {
args ??= this._getArgs?.();
args ??= this._getArgs();

// Remember the args for potential future automatic runs.
// TODO (justinfagnani): add test
this._previousArgs = args;

if (this.status === TaskStatus.PENDING) {
this._abortController?.abort();
Expand Down Expand Up @@ -400,16 +428,16 @@ export class Task<
throw new Error(`Unexpected status: ${this.status}`);
}
}

private _argsDirty(args?: T) {
const prev = this._previousArgs;
this._previousArgs = args;
return Array.isArray(args) && Array.isArray(prev)
? args.length === prev.length && args.some((v, i) => notEqual(v, prev[i]))
: args !== prev;
}
}

type MaybeReturnType<F> = F extends (...args: unknown[]) => infer R
? R
: undefined;

export const shallowArrayEquals = <T extends ReadonlyArray<unknown>>(
oldArgs: T,
newArgs: T
) =>
oldArgs === newArgs ||
(oldArgs.length === newArgs.length &&
oldArgs.every((v, i) => !notEqual(v, newArgs[i])));
Loading