@derivesome/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.README.md.~undo-tree~ +4 -0
- package/.package.json.~undo-tree~ +30 -0
- package/.tsconfig.cjs.json.~undo-tree~ +8 -0
- package/.tsconfig.json.~undo-tree~ +14 -0
- package/README.md +355 -0
- package/README.md~ +355 -0
- package/dist/cjs/array-proxy.d.ts +56 -0
- package/dist/cjs/array-proxy.d.ts.map +1 -0
- package/dist/cjs/array-proxy.js +72 -0
- package/dist/cjs/array-proxy.js.map +1 -0
- package/dist/cjs/common/array.d.ts +11 -0
- package/dist/cjs/common/array.d.ts.map +1 -0
- package/dist/cjs/common/array.js +57 -0
- package/dist/cjs/common/array.js.map +1 -0
- package/dist/cjs/common/compare.d.ts +2 -0
- package/dist/cjs/common/compare.d.ts.map +1 -0
- package/dist/cjs/common/compare.js +8 -0
- package/dist/cjs/common/compare.js.map +1 -0
- package/dist/cjs/common/diff.d.ts +9 -0
- package/dist/cjs/common/diff.d.ts.map +1 -0
- package/dist/cjs/common/diff.js +63 -0
- package/dist/cjs/common/diff.js.map +1 -0
- package/dist/cjs/common/has.d.ts +4 -0
- package/dist/cjs/common/has.d.ts.map +1 -0
- package/dist/cjs/common/has.js +11 -0
- package/dist/cjs/common/has.js.map +1 -0
- package/dist/cjs/common/index.d.ts +12 -0
- package/dist/cjs/common/index.d.ts.map +1 -0
- package/dist/cjs/common/index.js +28 -0
- package/dist/cjs/common/index.js.map +1 -0
- package/dist/cjs/common/is.d.ts +5 -0
- package/dist/cjs/common/is.d.ts.map +1 -0
- package/dist/cjs/common/is.js +18 -0
- package/dist/cjs/common/is.js.map +1 -0
- package/dist/cjs/common/match.d.ts +23 -0
- package/dist/cjs/common/match.d.ts.map +1 -0
- package/dist/cjs/common/match.js +49 -0
- package/dist/cjs/common/match.js.map +1 -0
- package/dist/cjs/common/patch.d.ts +3 -0
- package/dist/cjs/common/patch.d.ts.map +1 -0
- package/dist/cjs/common/patch.js +44 -0
- package/dist/cjs/common/patch.js.map +1 -0
- package/dist/cjs/common/stack.d.ts +8 -0
- package/dist/cjs/common/stack.d.ts.map +1 -0
- package/dist/cjs/common/stack.js +24 -0
- package/dist/cjs/common/stack.js.map +1 -0
- package/dist/cjs/common/string.d.ts +2 -0
- package/dist/cjs/common/string.d.ts.map +1 -0
- package/dist/cjs/common/string.js +36 -0
- package/dist/cjs/common/string.js.map +1 -0
- package/dist/cjs/common/types.d.ts +27 -0
- package/dist/cjs/common/types.d.ts.map +1 -0
- package/dist/cjs/common/types.js +3 -0
- package/dist/cjs/common/types.js.map +1 -0
- package/dist/cjs/common/unique-array.d.ts +17 -0
- package/dist/cjs/common/unique-array.d.ts.map +1 -0
- package/dist/cjs/common/unique-array.js +73 -0
- package/dist/cjs/common/unique-array.js.map +1 -0
- package/dist/cjs/context.d.ts +15 -0
- package/dist/cjs/context.d.ts.map +1 -0
- package/dist/cjs/context.js +33 -0
- package/dist/cjs/context.js.map +1 -0
- package/dist/cjs/derived.d.ts +11 -0
- package/dist/cjs/derived.d.ts.map +1 -0
- package/dist/cjs/derived.js +26 -0
- package/dist/cjs/derived.js.map +1 -0
- package/dist/cjs/effect.d.ts +7 -0
- package/dist/cjs/effect.d.ts.map +1 -0
- package/dist/cjs/effect.js +7 -0
- package/dist/cjs/effect.js.map +1 -0
- package/dist/cjs/index.d.ts +8 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +24 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/observable.d.ts +12 -0
- package/dist/cjs/observable.d.ts.map +1 -0
- package/dist/cjs/observable.js +24 -0
- package/dist/cjs/observable.js.map +1 -0
- package/dist/cjs/pubsub.d.ts +11 -0
- package/dist/cjs/pubsub.d.ts.map +1 -0
- package/dist/cjs/pubsub.js +50 -0
- package/dist/cjs/pubsub.js.map +1 -0
- package/dist/cjs/reference.d.ts +15 -0
- package/dist/cjs/reference.d.ts.map +1 -0
- package/dist/cjs/reference.js +49 -0
- package/dist/cjs/reference.js.map +1 -0
- package/dist/cjs/utils/findRefs.d.ts +3 -0
- package/dist/cjs/utils/findRefs.d.ts.map +1 -0
- package/dist/cjs/utils/findRefs.js +23 -0
- package/dist/cjs/utils/findRefs.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +2 -0
- package/dist/cjs/utils/index.d.ts.map +1 -0
- package/dist/cjs/utils/index.js +18 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/esm/array-proxy.d.ts +56 -0
- package/dist/esm/array-proxy.d.ts.map +1 -0
- package/dist/esm/array-proxy.js +68 -0
- package/dist/esm/array-proxy.js.map +1 -0
- package/dist/esm/common/array.d.ts +11 -0
- package/dist/esm/common/array.d.ts.map +1 -0
- package/dist/esm/common/array.js +48 -0
- package/dist/esm/common/array.js.map +1 -0
- package/dist/esm/common/compare.d.ts +2 -0
- package/dist/esm/common/compare.d.ts.map +1 -0
- package/dist/esm/common/compare.js +4 -0
- package/dist/esm/common/compare.js.map +1 -0
- package/dist/esm/common/diff.d.ts +9 -0
- package/dist/esm/common/diff.d.ts.map +1 -0
- package/dist/esm/common/diff.js +59 -0
- package/dist/esm/common/diff.js.map +1 -0
- package/dist/esm/common/has.d.ts +4 -0
- package/dist/esm/common/has.d.ts.map +1 -0
- package/dist/esm/common/has.js +8 -0
- package/dist/esm/common/has.js.map +1 -0
- package/dist/esm/common/index.d.ts +12 -0
- package/dist/esm/common/index.d.ts.map +1 -0
- package/dist/esm/common/index.js +12 -0
- package/dist/esm/common/index.js.map +1 -0
- package/dist/esm/common/is.d.ts +5 -0
- package/dist/esm/common/is.d.ts.map +1 -0
- package/dist/esm/common/is.js +13 -0
- package/dist/esm/common/is.js.map +1 -0
- package/dist/esm/common/match.d.ts +23 -0
- package/dist/esm/common/match.d.ts.map +1 -0
- package/dist/esm/common/match.js +46 -0
- package/dist/esm/common/match.js.map +1 -0
- package/dist/esm/common/patch.d.ts +3 -0
- package/dist/esm/common/patch.d.ts.map +1 -0
- package/dist/esm/common/patch.js +40 -0
- package/dist/esm/common/patch.js.map +1 -0
- package/dist/esm/common/stack.d.ts +8 -0
- package/dist/esm/common/stack.d.ts.map +1 -0
- package/dist/esm/common/stack.js +20 -0
- package/dist/esm/common/stack.js.map +1 -0
- package/dist/esm/common/string.d.ts +2 -0
- package/dist/esm/common/string.d.ts.map +1 -0
- package/dist/esm/common/string.js +32 -0
- package/dist/esm/common/string.js.map +1 -0
- package/dist/esm/common/types.d.ts +27 -0
- package/dist/esm/common/types.d.ts.map +1 -0
- package/dist/esm/common/types.js +2 -0
- package/dist/esm/common/types.js.map +1 -0
- package/dist/esm/common/unique-array.d.ts +17 -0
- package/dist/esm/common/unique-array.d.ts.map +1 -0
- package/dist/esm/common/unique-array.js +69 -0
- package/dist/esm/common/unique-array.js.map +1 -0
- package/dist/esm/context.d.ts +15 -0
- package/dist/esm/context.d.ts.map +1 -0
- package/dist/esm/context.js +28 -0
- package/dist/esm/context.js.map +1 -0
- package/dist/esm/derived.d.ts +11 -0
- package/dist/esm/derived.d.ts.map +1 -0
- package/dist/esm/derived.js +21 -0
- package/dist/esm/derived.js.map +1 -0
- package/dist/esm/effect.d.ts +7 -0
- package/dist/esm/effect.d.ts.map +1 -0
- package/dist/esm/effect.js +3 -0
- package/dist/esm/effect.js.map +1 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/observable.d.ts +12 -0
- package/dist/esm/observable.d.ts.map +1 -0
- package/dist/esm/observable.js +19 -0
- package/dist/esm/observable.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/pubsub.d.ts +11 -0
- package/dist/esm/pubsub.d.ts.map +1 -0
- package/dist/esm/pubsub.js +47 -0
- package/dist/esm/pubsub.js.map +1 -0
- package/dist/esm/reference.d.ts +15 -0
- package/dist/esm/reference.d.ts.map +1 -0
- package/dist/esm/reference.js +44 -0
- package/dist/esm/reference.js.map +1 -0
- package/dist/esm/utils/findRefs.d.ts +3 -0
- package/dist/esm/utils/findRefs.d.ts.map +1 -0
- package/dist/esm/utils/findRefs.js +19 -0
- package/dist/esm/utils/findRefs.js.map +1 -0
- package/dist/esm/utils/index.d.ts +2 -0
- package/dist/esm/utils/index.d.ts.map +1 -0
- package/dist/esm/utils/index.js +2 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/package.json +41 -0
- package/package.json~ +42 -0
- package/src/array-proxy.ts +94 -0
- package/src/common/array.ts +72 -0
- package/src/common/compare.ts +3 -0
- package/src/common/diff.test.ts +114 -0
- package/src/common/diff.ts +85 -0
- package/src/common/has.ts +7 -0
- package/src/common/index.ts +11 -0
- package/src/common/is.ts +14 -0
- package/src/common/match.ts +90 -0
- package/src/common/patch.test.ts +118 -0
- package/src/common/patch.ts +47 -0
- package/src/common/stack.ts +22 -0
- package/src/common/string.test.ts +46 -0
- package/src/common/string.ts +35 -0
- package/src/common/types.ts +61 -0
- package/src/common/unique-array.ts +80 -0
- package/src/context.ts +36 -0
- package/src/derived.test.ts +45 -0
- package/src/derived.ts +35 -0
- package/src/effect.ts +9 -0
- package/src/index.ts +7 -0
- package/src/observable.ts +40 -0
- package/src/pubsub.test.ts +29 -0
- package/src/pubsub.ts +71 -0
- package/src/reference.test.ts +32 -0
- package/src/reference.ts +63 -0
- package/src/utils/findRefs.ts +22 -0
- package/src/utils/index.ts +1 -0
- package/tsconfig.cjs.json +8 -0
- package/tsconfig.cjs.json~ +0 -0
- package/tsconfig.json +24 -0
- package/tsconfig.json~ +24 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function has<Keys extends [...PropertyKey[]]>(x: unknown, ...keys: Keys): x is ({
|
|
2
|
+
[Prop in keyof Keys as [Keys[Prop]] extends [string] ? Keys[Prop] : never]: unknown
|
|
3
|
+
}) {
|
|
4
|
+
if (typeof x === 'undefined' || x === null) return false;
|
|
5
|
+
if (keys.every(key => Object.hasOwn(x, key) || (typeof x === 'object' && (key in x)))) return true;
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './is';
|
|
3
|
+
export * from './stack';
|
|
4
|
+
export * from './compare';
|
|
5
|
+
export * from './unique-array';
|
|
6
|
+
export * from './has';
|
|
7
|
+
export * from './array';
|
|
8
|
+
export * from './diff';
|
|
9
|
+
export * from './patch';
|
|
10
|
+
export * from './match';
|
|
11
|
+
export * from './string';
|
package/src/common/is.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Loose, LooseFunction } from "./types";
|
|
2
|
+
|
|
3
|
+
export function isFunction(x: unknown): x is LooseFunction;
|
|
4
|
+
export function isFunction(x: Loose): x is LooseFunction;
|
|
5
|
+
export function isFunction(x: unknown): x is LooseFunction {
|
|
6
|
+
return x instanceof Function || typeof x === "function";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const isPlainObject = (x: any): x is Record<PropertyKey, unknown> => {
|
|
10
|
+
if (x === null || typeof x === "undefined") return false;
|
|
11
|
+
if (Array.isArray(x)) return false;
|
|
12
|
+
if (x instanceof Date) return false;
|
|
13
|
+
return Object.getPrototypeOf(x) === Object.prototype;
|
|
14
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isPlainObject } from "./is";
|
|
2
|
+
import { IsLiteral } from "./types";
|
|
3
|
+
|
|
4
|
+
export type ToPattern<
|
|
5
|
+
T,
|
|
6
|
+
U extends Exclude<T, undefined> = Exclude<T, undefined>,
|
|
7
|
+
> =
|
|
8
|
+
[IsLiteral<T>] extends [true] ? U
|
|
9
|
+
: [U] extends [string] ? string
|
|
10
|
+
: [U] extends [number] ? number
|
|
11
|
+
: [U] extends [boolean] ? boolean
|
|
12
|
+
: [U] extends [BigInt] ? BigInt
|
|
13
|
+
: [U] extends [null] ? null
|
|
14
|
+
: [U] extends [Array<infer A>] ? Array<ToPattern<A>>
|
|
15
|
+
: [U] extends [Record<PropertyKey, unknown>] ?
|
|
16
|
+
{
|
|
17
|
+
[P in keyof U]?: ToPattern<U[P]>;
|
|
18
|
+
}
|
|
19
|
+
: T;
|
|
20
|
+
|
|
21
|
+
export type Unpattern<I, P> =
|
|
22
|
+
[IsLiteral<I>, IsLiteral<P>] extends [true, true] ?
|
|
23
|
+
[I] extends [P] ?
|
|
24
|
+
P
|
|
25
|
+
: never
|
|
26
|
+
: [I, P] extends [string, string] ? P
|
|
27
|
+
: [I, P] extends [number, number] ? P
|
|
28
|
+
: [I, P] extends [boolean, boolean] ? P
|
|
29
|
+
: [I, P] extends [BigInt, BigInt] ? P
|
|
30
|
+
: [I, P] extends [Array<infer AI>, Array<infer AP>] ? Array<Unpattern<AI, AP>>
|
|
31
|
+
: [[I], [P]] extends (
|
|
32
|
+
[
|
|
33
|
+
[infer AI extends Record<PropertyKey, unknown>],
|
|
34
|
+
[infer AP extends Record<PropertyKey, unknown>],
|
|
35
|
+
]
|
|
36
|
+
) ?
|
|
37
|
+
Extract<
|
|
38
|
+
AI,
|
|
39
|
+
{
|
|
40
|
+
[KP in keyof AP]: AP[KP];
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
: "ERROR";
|
|
44
|
+
|
|
45
|
+
export function match<Input, const Pattern extends ToPattern<Input>>(
|
|
46
|
+
input: Input | Unpattern<Input, Pattern>,
|
|
47
|
+
pattern: Pattern,
|
|
48
|
+
): input is Unpattern<Input, Pattern> {
|
|
49
|
+
if (input === pattern) return true;
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(pattern)) {
|
|
52
|
+
if (!Array.isArray(input)) return false;
|
|
53
|
+
return pattern.every((p, i) => match(input[i], p));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
switch (typeof pattern) {
|
|
57
|
+
case 'string':
|
|
58
|
+
case 'boolean':
|
|
59
|
+
case 'undefined':
|
|
60
|
+
case 'function':
|
|
61
|
+
case 'symbol':
|
|
62
|
+
case 'number': return input === pattern;
|
|
63
|
+
case 'bigint': return (typeof input === 'bigint') && input.toString() === pattern.toString();
|
|
64
|
+
case 'object': {
|
|
65
|
+
if (isPlainObject(input) && isPlainObject(pattern)) {
|
|
66
|
+
for (const [k, pv] of Object.entries(pattern)) {
|
|
67
|
+
const iv = input[k];
|
|
68
|
+
if (!match(iv, pv)) return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const symbols = Object.getOwnPropertySymbols(pattern);
|
|
72
|
+
|
|
73
|
+
for (const symbol of symbols) {
|
|
74
|
+
if (!(symbol in input)) return false;
|
|
75
|
+
const iv = input[symbol];
|
|
76
|
+
const pv = pattern[symbol];
|
|
77
|
+
if (!match(iv, pv)) return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
} else {
|
|
82
|
+
console.error(`Tried to match`, input, pattern);
|
|
83
|
+
throw new Error(`Cannot match`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, assert } from 'vitest';
|
|
2
|
+
import { diff } from './diff';
|
|
3
|
+
import { patch } from './patch';
|
|
4
|
+
|
|
5
|
+
describe('patch', () => {
|
|
6
|
+
it('should patch a changed primitive at root', () => {
|
|
7
|
+
const diffs = diff(1, 2);
|
|
8
|
+
const result = patch(1, diffs);
|
|
9
|
+
assert.equal(result, 2);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should patch changed object properties in-place', () => {
|
|
13
|
+
const target = { a: 1, b: 2 };
|
|
14
|
+
const ref = target;
|
|
15
|
+
const diffs = diff(target, { a: 1, b: 99 });
|
|
16
|
+
const result = patch(target, diffs);
|
|
17
|
+
assert.strictEqual(result, ref);
|
|
18
|
+
assert.deepEqual(result, { a: 1, b: 99 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should add new properties in-place', () => {
|
|
22
|
+
const target: Record<string, number> = { a: 1 };
|
|
23
|
+
const ref = target;
|
|
24
|
+
const diffs = diff(target, { a: 1, b: 2 });
|
|
25
|
+
const result = patch(target, diffs);
|
|
26
|
+
assert.strictEqual(result, ref);
|
|
27
|
+
assert.deepEqual(result, { a: 1, b: 2 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should remove properties in-place', () => {
|
|
31
|
+
const target: Record<string, number> = { a: 1, b: 2 };
|
|
32
|
+
const ref = target;
|
|
33
|
+
const diffs = diff(target, { a: 1 });
|
|
34
|
+
const result = patch(target, diffs);
|
|
35
|
+
assert.strictEqual(result, ref);
|
|
36
|
+
assert.deepEqual(result, { a: 1 });
|
|
37
|
+
assert.isFalse('b' in result);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should patch nested objects in-place', () => {
|
|
41
|
+
const target = { x: { y: { z: 1 } } };
|
|
42
|
+
const innerRef = target.x.y;
|
|
43
|
+
const diffs = diff(target, { x: { y: { z: 42 } } });
|
|
44
|
+
const result = patch(target, diffs);
|
|
45
|
+
assert.strictEqual(result.x.y, innerRef);
|
|
46
|
+
assert.deepEqual(result, { x: { y: { z: 42 } } });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should patch array elements in-place', () => {
|
|
50
|
+
const target = [1, 2, 3];
|
|
51
|
+
const ref = target;
|
|
52
|
+
const diffs = diff(target, [1, 99, 3]);
|
|
53
|
+
const result = patch(target, diffs);
|
|
54
|
+
assert.strictEqual(result, ref);
|
|
55
|
+
assert.deepEqual(result, [1, 99, 3]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should add array elements in-place', () => {
|
|
59
|
+
const target = [1, 2];
|
|
60
|
+
const ref = target;
|
|
61
|
+
const diffs = diff(target, [1, 2, 3]);
|
|
62
|
+
const result = patch(target, diffs);
|
|
63
|
+
assert.strictEqual(result, ref);
|
|
64
|
+
assert.deepEqual(result, [1, 2, 3]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should remove array elements in-place', () => {
|
|
68
|
+
const target = [1, 2, 3];
|
|
69
|
+
const ref = target;
|
|
70
|
+
const diffs = diff(target, [1, 2]);
|
|
71
|
+
const result = patch(target, diffs);
|
|
72
|
+
assert.strictEqual(result, ref);
|
|
73
|
+
assert.deepEqual(result, [1, 2]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should patch nested arrays inside objects in-place', () => {
|
|
77
|
+
const target = { items: [10, 20, 30] };
|
|
78
|
+
const arrRef = target.items;
|
|
79
|
+
const diffs = diff(target, { items: [10, 25, 30] });
|
|
80
|
+
const result = patch(target, diffs);
|
|
81
|
+
assert.strictEqual(result.items, arrRef);
|
|
82
|
+
assert.deepEqual(result, { items: [10, 25, 30] });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should patch deeply nested mixed structures in-place', () => {
|
|
86
|
+
const target = { users: [{ name: 'Alice', scores: [10, 20] }] };
|
|
87
|
+
const scoresRef = target.users[0]!.scores;
|
|
88
|
+
const diffs = diff(target, { users: [{ name: 'Alice', scores: [10, 30] }] });
|
|
89
|
+
const result = patch(target, diffs);
|
|
90
|
+
assert.strictEqual(result.users[0]!.scores, scoresRef);
|
|
91
|
+
assert.deepEqual(result, { users: [{ name: 'Alice', scores: [10, 30] }] });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle type change from object to primitive', () => {
|
|
95
|
+
const target: any = { a: { b: 1 } };
|
|
96
|
+
const diffs = diff(target, { a: 42 } as any);
|
|
97
|
+
const result = patch(target, diffs);
|
|
98
|
+
assert.deepEqual(result, { a: 42 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle symbol keys', () => {
|
|
102
|
+
const sym = Symbol('test');
|
|
103
|
+
const target = { [sym]: 1 };
|
|
104
|
+
const ref = target;
|
|
105
|
+
const diffs = diff(target, { [sym]: 2 });
|
|
106
|
+
const result = patch(target, diffs);
|
|
107
|
+
assert.strictEqual(result, ref);
|
|
108
|
+
assert.equal(result[sym], 2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should produce the same result as b when diffing a->b and patching a', () => {
|
|
112
|
+
const a = { x: 1, y: [1, 2, 3], z: { nested: true, value: 'hello' } };
|
|
113
|
+
const b = { x: 2, y: [1, 3], z: { nested: false, extra: 'world' } };
|
|
114
|
+
const diffs = diff(a, b as any);
|
|
115
|
+
const result = patch(a, diffs);
|
|
116
|
+
assert.deepEqual(result, b as unknown);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Diff } from './diff';
|
|
2
|
+
|
|
3
|
+
export const patch = <T>(target: T, diffs: Diff[]): T => {
|
|
4
|
+
for (const d of diffs) {
|
|
5
|
+
target = applyDiff(target, d, 0);
|
|
6
|
+
}
|
|
7
|
+
return target;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const applyDiff = <T>(target: T, d: Diff, depth: number): T => {
|
|
11
|
+
if (depth === d.path.length) {
|
|
12
|
+
if (d.type === 'changed' || d.type === 'added') {
|
|
13
|
+
return d.newValue as T;
|
|
14
|
+
}
|
|
15
|
+
return undefined as T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const key = d.path[depth]!;
|
|
19
|
+
|
|
20
|
+
if (Array.isArray(target)) {
|
|
21
|
+
const index = key as number;
|
|
22
|
+
|
|
23
|
+
if (d.type === 'removed' && depth === d.path.length - 1) {
|
|
24
|
+
target.splice(index, 1);
|
|
25
|
+
} else if (d.type === 'added' && depth === d.path.length - 1) {
|
|
26
|
+
target.splice(index, 0, d.newValue);
|
|
27
|
+
} else {
|
|
28
|
+
target[index] = applyDiff(target[index], d, depth + 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return target;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof target === 'object' && target !== null) {
|
|
35
|
+
const obj = target as Record<PropertyKey, unknown>;
|
|
36
|
+
|
|
37
|
+
if (d.type === 'removed' && depth === d.path.length - 1) {
|
|
38
|
+
delete obj[key];
|
|
39
|
+
} else {
|
|
40
|
+
obj[key] = applyDiff(obj[key], d, depth + 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return target;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return target;
|
|
47
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class Stack<T> {
|
|
2
|
+
items: T[];
|
|
3
|
+
|
|
4
|
+
constructor(items: T[] = []) {
|
|
5
|
+
this.items = items;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get current(): T {
|
|
9
|
+
const item = this.items[this.items.length - 1];
|
|
10
|
+
if (!item) throw new Error(`Stack is empty`);
|
|
11
|
+
return item;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
push(item: T): this {
|
|
15
|
+
this.items.push(item);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pop(): T | null {
|
|
20
|
+
return this.items.pop() || null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, assert } from 'vitest';
|
|
2
|
+
import { unjoin } from './string';
|
|
3
|
+
|
|
4
|
+
describe("unjoin", (it) => {
|
|
5
|
+
it("unjoins a '/' joined string", () => {
|
|
6
|
+
const path: string = 'foo/bar/baz';
|
|
7
|
+
const parts = unjoin(path, '/');
|
|
8
|
+
|
|
9
|
+
console.log(parts);
|
|
10
|
+
|
|
11
|
+
const expected: string[] = ['foo', '/', 'bar', '/', 'baz'];
|
|
12
|
+
assert.strictEqual(parts.length, expected.length);
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < expected.length; i++) {
|
|
15
|
+
assert.strictEqual(parts[i], expected[i]);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("unjoins a '/' joined string starting with '/'", () => {
|
|
20
|
+
const path: string = '/foo/bar/baz';
|
|
21
|
+
const parts = unjoin(path, '/');
|
|
22
|
+
|
|
23
|
+
console.log(parts);
|
|
24
|
+
|
|
25
|
+
const expected: string[] = ['/', 'foo', '/', 'bar', '/', 'baz'];
|
|
26
|
+
assert.strictEqual(parts.length, expected.length);
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < expected.length; i++) {
|
|
29
|
+
assert.strictEqual(parts[i], expected[i]);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("unjoins a non-joined string", () => {
|
|
34
|
+
const path: string = 'foobar';
|
|
35
|
+
const parts = unjoin(path, '/');
|
|
36
|
+
|
|
37
|
+
console.log(parts);
|
|
38
|
+
|
|
39
|
+
const expected: string[] = ['foobar'];
|
|
40
|
+
assert.strictEqual(parts.length, expected.length);
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < expected.length; i++) {
|
|
43
|
+
assert.strictEqual(parts[i], expected[i]);
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const unjoin = (value: string, delim: string): string[] => {
|
|
2
|
+
const parts: string[] = [];
|
|
3
|
+
|
|
4
|
+
const len = value.length;
|
|
5
|
+
if (len <= 0) return parts;
|
|
6
|
+
|
|
7
|
+
let i = 0;
|
|
8
|
+
let c = value[i++]!;
|
|
9
|
+
|
|
10
|
+
let current: string = "";
|
|
11
|
+
|
|
12
|
+
while (c && i <= len) {
|
|
13
|
+
if (c === delim) {
|
|
14
|
+
while (c && i <= len && c === delim) {
|
|
15
|
+
parts.push(delim);
|
|
16
|
+
c = value[i++]!;
|
|
17
|
+
}
|
|
18
|
+
continue;
|
|
19
|
+
} else {
|
|
20
|
+
current = "";
|
|
21
|
+
while (c && i <= len && c !== delim) {
|
|
22
|
+
current += c;
|
|
23
|
+
c = value[i++]!;
|
|
24
|
+
}
|
|
25
|
+
parts.push(current);
|
|
26
|
+
|
|
27
|
+
while (c && i <= len && c === delim) {
|
|
28
|
+
parts.push(delim);
|
|
29
|
+
c = value[i++]!;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return parts;
|
|
35
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type Loose = any;
|
|
2
|
+
export type Boxed<T> = { value: T };
|
|
3
|
+
export type Subscription<T> = (value: T) => void;
|
|
4
|
+
export type SubscriptionCaller<T> = (fn: Subscription<T>, value: T) => void;
|
|
5
|
+
export type ReturningFunction<T> = () => T;
|
|
6
|
+
export type VoidFunction = () => void;
|
|
7
|
+
export type VoidAsyncFunction = () => void;
|
|
8
|
+
export type VoidSyncOrAsyncFunction = () => void | Promise<void>;
|
|
9
|
+
export type LooseFunction = (...args: Loose[]) => Loose;
|
|
10
|
+
export type SetStateAction<S> = S | ((prevState: S) => S);
|
|
11
|
+
export type Dispatch<A> = (value: A) => void;
|
|
12
|
+
export type UnknownDict = Record<string, unknown>;
|
|
13
|
+
export type LooseDict = Record<string, Loose>;
|
|
14
|
+
|
|
15
|
+
export type SubscribeOptions = {
|
|
16
|
+
cleanup?: VoidFunction;
|
|
17
|
+
immediate?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type Distribute<T> = T extends T ? T : never;
|
|
21
|
+
|
|
22
|
+
export type UnionToIntersection<union> =
|
|
23
|
+
(union extends Loose ? (k: union) => void : never) extends (
|
|
24
|
+
(k: infer intersection) => void
|
|
25
|
+
) ?
|
|
26
|
+
intersection
|
|
27
|
+
: never;
|
|
28
|
+
|
|
29
|
+
export type IsUnion<a> = [a] extends [UnionToIntersection<a>] ? false : true;
|
|
30
|
+
|
|
31
|
+
export type UnionToTuple<union, output extends Loose[] = []> =
|
|
32
|
+
UnionToIntersection<
|
|
33
|
+
union extends Loose ? (t: union) => union : never
|
|
34
|
+
> extends (_: Loose) => infer elem ?
|
|
35
|
+
UnionToTuple<Exclude<union, elem>, [elem, ...output]>
|
|
36
|
+
: output;
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export type IsLiteral<a> =
|
|
40
|
+
[a] extends [null | undefined] ? true
|
|
41
|
+
: [a] extends [string] ?
|
|
42
|
+
string extends a ?
|
|
43
|
+
false
|
|
44
|
+
: true
|
|
45
|
+
: [a] extends [number] ?
|
|
46
|
+
number extends a ?
|
|
47
|
+
false
|
|
48
|
+
: true
|
|
49
|
+
: [a] extends [boolean] ?
|
|
50
|
+
boolean extends a ?
|
|
51
|
+
false
|
|
52
|
+
: true
|
|
53
|
+
: [a] extends [symbol] ?
|
|
54
|
+
symbol extends a ?
|
|
55
|
+
false
|
|
56
|
+
: true
|
|
57
|
+
: [a] extends [bigint] ?
|
|
58
|
+
bigint extends a ?
|
|
59
|
+
false
|
|
60
|
+
: true
|
|
61
|
+
: false;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ArrayProxy } from "../array-proxy";
|
|
2
|
+
|
|
3
|
+
export class UniqueArray<T> {
|
|
4
|
+
private lookup: Set<T> = new Set();
|
|
5
|
+
items: ArrayProxy<T> = new ArrayProxy<T>();
|
|
6
|
+
|
|
7
|
+
constructor(items: T[] = []) {
|
|
8
|
+
this.push(...items);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get length(): number {
|
|
12
|
+
if (this.items.length !== this.lookup.size) {
|
|
13
|
+
throw new Error("UniqueArray: array length is unsynced with lookup");
|
|
14
|
+
}
|
|
15
|
+
return this.items.length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
values(): Array<T> {
|
|
19
|
+
return this.items;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
copyValues(): Array<T> {
|
|
23
|
+
return [...this.items];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
includes(searchElement: T): boolean {
|
|
27
|
+
return this.lookup.has(searchElement);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
push(...items: T[]): number {
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
if (this.includes(item)) continue;
|
|
33
|
+
this.items.push(item);
|
|
34
|
+
this.lookup.add(item);
|
|
35
|
+
}
|
|
36
|
+
return this.lookup.size;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pop(): T | undefined {
|
|
40
|
+
const item = this.items.pop();
|
|
41
|
+
if (item) {
|
|
42
|
+
this.lookup.delete(item);
|
|
43
|
+
}
|
|
44
|
+
return item;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
indexOf(item: T): number {
|
|
48
|
+
const idx = this.items.indexOf(item);
|
|
49
|
+
if (
|
|
50
|
+
(idx >= 0 && !this.includes(item)) ||
|
|
51
|
+
(idx < 0 && this.includes(item))
|
|
52
|
+
) {
|
|
53
|
+
throw new Error("UniqueArray: array is unsynced with lookup");
|
|
54
|
+
}
|
|
55
|
+
return idx;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
delete(item: T) {
|
|
59
|
+
const idx = this.indexOf(item);
|
|
60
|
+
if (idx < 0) return;
|
|
61
|
+
this.items.splice(idx, 1);
|
|
62
|
+
this.lookup.delete(item);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
clear() {
|
|
66
|
+
this.items.splice(0);
|
|
67
|
+
this.lookup.clear();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
insert(offset: number, values: T[]) {
|
|
71
|
+
if (offset < 0 || offset >= this.length) {
|
|
72
|
+
return this.push(...values);
|
|
73
|
+
}
|
|
74
|
+
const items = Array.from(new Set(values.filter(x => !this.includes(x))));
|
|
75
|
+
const left = this.items.slice(0, offset);
|
|
76
|
+
const right = this.items.slice(offset);
|
|
77
|
+
this.clear();
|
|
78
|
+
this.push(...left, ...items, ...right);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isFunction } from "./common/is";
|
|
2
|
+
import { Stack } from "./common/stack";
|
|
3
|
+
import { VoidFunction } from "./common/types";
|
|
4
|
+
import { Effect } from "./effect";
|
|
5
|
+
|
|
6
|
+
export type ScopeData = {
|
|
7
|
+
effect: Effect | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class Scope {
|
|
11
|
+
current: ScopeData = {
|
|
12
|
+
effect: null,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Context {
|
|
17
|
+
static scopes: Stack<Scope> = new Stack([new Scope()]);
|
|
18
|
+
|
|
19
|
+
static get scope(): Scope {
|
|
20
|
+
return this.scopes.current;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static runEffect(effect: VoidFunction | Effect) {
|
|
24
|
+
if (isFunction(effect)) {
|
|
25
|
+
const prev = this.scope.current.effect;
|
|
26
|
+
this.scope.current.effect = { run: effect };
|
|
27
|
+
effect();
|
|
28
|
+
this.scope.current.effect = prev;
|
|
29
|
+
} else {
|
|
30
|
+
const prev = this.scope.current.effect;
|
|
31
|
+
this.scope.current.effect = effect;
|
|
32
|
+
effect.run();
|
|
33
|
+
this.scope.current.effect = prev;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, assert } from "vitest";
|
|
2
|
+
import { ref } from "./reference";
|
|
3
|
+
import { derived } from "./derived";
|
|
4
|
+
|
|
5
|
+
describe("derived", (it) => {
|
|
6
|
+
it("computes a derived value", () => {
|
|
7
|
+
const r = ref<number>(0);
|
|
8
|
+
const d = derived(() => r.get() * 2);
|
|
9
|
+
const ds = derived(() => `number is: ${d.get()}`);
|
|
10
|
+
|
|
11
|
+
assert.strictEqual(d.peek(), 0);
|
|
12
|
+
assert.strictEqual(ds.peek(), `number is: ${0}`);
|
|
13
|
+
r.set(1);
|
|
14
|
+
assert.strictEqual(d.peek(), 2);
|
|
15
|
+
assert.strictEqual(ds.peek(), `number is: ${2}`);
|
|
16
|
+
r.set(2);
|
|
17
|
+
assert.strictEqual(d.peek(), 4);
|
|
18
|
+
assert.strictEqual(ds.peek(), `number is: ${4}`);
|
|
19
|
+
r.set(3);
|
|
20
|
+
assert.strictEqual(d.peek(), 6);
|
|
21
|
+
assert.strictEqual(ds.peek(), `number is: ${6}`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("tracks conditional dependencies", () => {
|
|
25
|
+
const selected = ref<boolean>(false);
|
|
26
|
+
const counter = ref<number>(0);
|
|
27
|
+
|
|
28
|
+
let numCalls: number = 0;
|
|
29
|
+
|
|
30
|
+
const thing = derived(() => {
|
|
31
|
+
numCalls += 1;
|
|
32
|
+
if (!selected.get()) return -1;
|
|
33
|
+
return counter.get() * 2;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.strictEqual(numCalls, 1);
|
|
37
|
+
selected.set(true);
|
|
38
|
+
assert.strictEqual(numCalls, 2);
|
|
39
|
+
counter.set(1);
|
|
40
|
+
assert.strictEqual(numCalls, 3);
|
|
41
|
+
assert.strictEqual(selected.effects.size, 1);
|
|
42
|
+
assert.strictEqual(counter.effects.size, 1);
|
|
43
|
+
assert.strictEqual(thing.effects.size, 0);
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/derived.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { compare } from "./common/compare";
|
|
2
|
+
import { has } from "./common/has";
|
|
3
|
+
import { Context } from "./context";
|
|
4
|
+
import { ref, Reference } from "./reference";
|
|
5
|
+
|
|
6
|
+
export const DERIVED = Symbol('Derived');
|
|
7
|
+
export type DERIVED = typeof DERIVED;
|
|
8
|
+
|
|
9
|
+
export type DeriveComputeFn<T> = () => T;
|
|
10
|
+
|
|
11
|
+
export interface Derived<T> extends Reference<T> {
|
|
12
|
+
[DERIVED]: true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function derived<T>(fn: DeriveComputeFn<T>): Derived<T> {
|
|
16
|
+
const state = ref<T>(null as T);
|
|
17
|
+
|
|
18
|
+
const compute = () => {
|
|
19
|
+
const prev = state.peek();
|
|
20
|
+
const next = fn();
|
|
21
|
+
if (!compare(next, prev)) {
|
|
22
|
+
state.set(next);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
Context.runEffect(compute);
|
|
27
|
+
|
|
28
|
+
return Object.assign(state, { [DERIVED]: true as const });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isDerived<X extends Derived<unknown>>(x: X): x is X;
|
|
32
|
+
export function isDerived(x: unknown): x is Derived<unknown>;
|
|
33
|
+
export function isDerived(x: unknown): x is Derived<unknown> {
|
|
34
|
+
return has(x, DERIVED);
|
|
35
|
+
}
|
package/src/effect.ts
ADDED