@signaltree/core 5.1.1 → 5.1.3
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 +116 -55
- package/dist/constants.js +6 -0
- package/dist/deep-clone.js +80 -0
- package/dist/deep-equal.js +41 -0
- package/dist/enhancers/batching/lib/batching.js +161 -0
- package/dist/enhancers/computed/lib/computed.js +21 -0
- package/dist/enhancers/devtools/lib/devtools.js +321 -0
- package/dist/enhancers/entities/lib/entities.js +93 -0
- package/dist/enhancers/index.js +72 -0
- package/dist/enhancers/memoization/lib/memoization.js +410 -0
- package/dist/enhancers/presets/lib/presets.js +87 -0
- package/dist/enhancers/serialization/constants.js +15 -0
- package/dist/enhancers/serialization/lib/serialization.js +662 -0
- package/dist/enhancers/time-travel/lib/time-travel.js +193 -0
- package/dist/index.js +19 -0
- package/dist/is-built-in-object.js +23 -0
- package/dist/lib/async-helpers.js +77 -0
- package/dist/lib/constants.js +56 -0
- package/dist/lib/entity-signal.js +280 -0
- package/dist/lib/memory/memory-manager.js +164 -0
- package/dist/lib/path-notifier.js +106 -0
- package/dist/lib/performance/diff-engine.js +156 -0
- package/dist/lib/performance/path-index.js +156 -0
- package/dist/lib/performance/update-engine.js +188 -0
- package/dist/lib/security/security-validator.js +121 -0
- package/dist/lib/signal-tree.js +626 -0
- package/dist/lib/types.js +9 -0
- package/dist/lib/utils.js +261 -0
- package/dist/lru-cache.js +64 -0
- package/dist/parse-path.js +13 -0
- package/package.json +1 -1
- package/src/async-helpers.d.ts +8 -0
- package/src/batching.d.ts +16 -0
- package/src/computed.d.ts +12 -0
- package/src/constants.d.ts +14 -0
- package/src/devtools.d.ts +77 -0
- package/src/diff-engine.d.ts +33 -0
- package/src/enhancers/batching/index.d.ts +1 -0
- package/src/enhancers/batching/lib/batching.d.ts +16 -0
- package/src/enhancers/batching/test-setup.d.ts +3 -0
- package/src/enhancers/computed/index.d.ts +1 -0
- package/src/enhancers/computed/lib/computed.d.ts +12 -0
- package/src/enhancers/devtools/index.d.ts +1 -0
- package/src/enhancers/devtools/lib/devtools.d.ts +77 -0
- package/src/enhancers/devtools/test-setup.d.ts +3 -0
- package/src/enhancers/entities/index.d.ts +1 -0
- package/src/enhancers/entities/lib/entities.d.ts +20 -0
- package/src/enhancers/entities/test-setup.d.ts +3 -0
- package/src/enhancers/index.d.ts +3 -0
- package/src/enhancers/memoization/index.d.ts +1 -0
- package/src/enhancers/memoization/lib/memoization.d.ts +65 -0
- package/src/enhancers/memoization/test-setup.d.ts +3 -0
- package/src/enhancers/presets/index.d.ts +1 -0
- package/src/enhancers/presets/lib/presets.d.ts +11 -0
- package/src/enhancers/presets/test-setup.d.ts +3 -0
- package/src/enhancers/serialization/constants.d.ts +14 -0
- package/src/enhancers/serialization/index.d.ts +2 -0
- package/src/enhancers/serialization/lib/serialization.d.ts +59 -0
- package/src/enhancers/serialization/test-setup.d.ts +3 -0
- package/src/enhancers/time-travel/index.d.ts +1 -0
- package/src/enhancers/time-travel/lib/time-travel.d.ts +36 -0
- package/src/enhancers/time-travel/lib/utils.d.ts +1 -0
- package/src/enhancers/time-travel/test-setup.d.ts +3 -0
- package/src/enhancers/types.d.ts +74 -0
- package/src/entities.d.ts +20 -0
- package/src/entity-signal.d.ts +1 -0
- package/src/index.d.ts +18 -0
- package/src/lib/async-helpers.d.ts +8 -0
- package/src/lib/constants.d.ts +41 -0
- package/src/lib/entity-signal.d.ts +1 -0
- package/src/lib/memory/memory-manager.d.ts +30 -0
- package/src/lib/path-notifier.d.ts +4 -0
- package/src/lib/performance/diff-engine.d.ts +33 -0
- package/src/lib/performance/path-index.d.ts +25 -0
- package/src/lib/performance/update-engine.d.ts +32 -0
- package/src/lib/security/security-validator.d.ts +33 -0
- package/src/lib/signal-tree.d.ts +8 -0
- package/src/lib/types.d.ts +278 -0
- package/src/lib/utils.d.ts +28 -0
- package/src/memoization.d.ts +65 -0
- package/src/memory-manager.d.ts +30 -0
- package/src/path-index.d.ts +25 -0
- package/src/path-notifier.d.ts +12 -0
- package/src/presets.d.ts +11 -0
- package/src/security-validator.d.ts +33 -0
- package/src/serialization.d.ts +59 -0
- package/src/signal-tree.d.ts +8 -0
- package/src/test-setup.d.ts +3 -0
- package/src/time-travel.d.ts +36 -0
- package/src/types.d.ts +278 -0
- package/src/update-engine.d.ts +32 -0
- package/src/utils.d.ts +1 -0
package/README.md
CHANGED
|
@@ -81,6 +81,73 @@ Performance and bundle size vary by app shape, build tooling, device, and runtim
|
|
|
81
81
|
- Use the **Benchmark Orchestrator** in the demo app to run calibrated, scenario-based benchmarks across supported libraries with **real-world frequency weighting**. It applies research-based multipliers derived from 40,000+ developer surveys and GitHub analysis, reports statistical summaries (median/p95/p99/stddev), alternates runs to reduce bias, and can export CSV/JSON. When available, memory usage is also reported.
|
|
82
82
|
- Use the bundle analysis scripts in `scripts/` to measure your min+gz sizes. Sizes are approximate and depend on tree-shaking and configuration.
|
|
83
83
|
|
|
84
|
+
## Best Practices (SignalTree-First)
|
|
85
|
+
|
|
86
|
+
> 📖 **Full guide**: [Implementation Patterns](https://github.com/JBorgia/signaltree/blob/main/docs/IMPLEMENTATION_PATTERNS.md)
|
|
87
|
+
|
|
88
|
+
Follow these principles for idiomatic SignalTree code:
|
|
89
|
+
|
|
90
|
+
### 1. Expose signals directly (no computed wrappers)
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const tree = signalTree(initialState).with(withEntities());
|
|
94
|
+
const $ = tree.$; // Shorthand for state access
|
|
95
|
+
|
|
96
|
+
// ✅ SignalTree-first: Direct signal exposure
|
|
97
|
+
return {
|
|
98
|
+
selectedUserId: $.selected.userId, // Direct from $ tree
|
|
99
|
+
loadingState: $.loading.state,
|
|
100
|
+
selectedUser, // Actual derived state (computed)
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ❌ Anti-pattern: Unnecessary computed wrappers
|
|
104
|
+
return {
|
|
105
|
+
selectedUserId: computed(() => $.selected.userId()), // Adds indirection
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 2. Use `ReturnType` inference (SignalTree-first)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Let SignalTree infer the type - no manual interface needed!
|
|
113
|
+
import type { createUserTree } from './user.tree';
|
|
114
|
+
export type UserTree = ReturnType<typeof createUserTree>;
|
|
115
|
+
|
|
116
|
+
// Factory function - no explicit return type needed
|
|
117
|
+
export function createUserTree() {
|
|
118
|
+
const tree = signalTree(initialState).with(withEntities());
|
|
119
|
+
return {
|
|
120
|
+
selectedUserId: tree.$.selected.userId, // Type inferred automatically
|
|
121
|
+
// ...
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3. Use `computed()` only for derived state
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// ✅ Correct: Derived from multiple signals
|
|
130
|
+
const selectedUser = computed(() => {
|
|
131
|
+
const id = $.selected.userId();
|
|
132
|
+
return id ? $.users.byId(id)() : null;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ❌ Wrong: Wrapping an existing signal
|
|
136
|
+
const selectedUserId = computed(() => $.selected.userId()); // Unnecessary!
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 4. Use EntitySignal API directly
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// ✅ SignalTree-native
|
|
143
|
+
const user = $.users.byId(123)(); // O(1) lookup
|
|
144
|
+
const allUsers = $.users.all()(); // Get all
|
|
145
|
+
$.users.setAll(usersFromApi); // Replace all
|
|
146
|
+
|
|
147
|
+
// ❌ NgRx-style (avoid)
|
|
148
|
+
const user = entityMap()[123]; // Requires intermediate object
|
|
149
|
+
```
|
|
150
|
+
|
|
84
151
|
## Quick start
|
|
85
152
|
|
|
86
153
|
### Installation
|
|
@@ -552,22 +619,20 @@ const tree = signalTree({ count: 0 }).with(
|
|
|
552
619
|
import { signalTree, withBatching, withMemoization, withEntities } from '@signaltree/core';
|
|
553
620
|
|
|
554
621
|
const tree = signalTree({
|
|
555
|
-
products:
|
|
622
|
+
products: entityMap<Product>(),
|
|
556
623
|
ui: { loading: false },
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
withEntities() // Efficient CRUD operations
|
|
561
|
-
);
|
|
624
|
+
})
|
|
625
|
+
.with(withEntities()) // Efficient CRUD operations (auto-detects entityMap)
|
|
626
|
+
.with(withBatching()); // Batch updates for optimal rendering
|
|
562
627
|
|
|
563
|
-
//
|
|
564
|
-
tree.
|
|
565
|
-
|
|
566
|
-
ui: { loading: false },
|
|
567
|
-
}));
|
|
628
|
+
// Entity CRUD operations
|
|
629
|
+
tree.$.products.addOne(newProduct);
|
|
630
|
+
tree.$.products.setAll(productsFromApi);
|
|
568
631
|
|
|
569
|
-
|
|
570
|
-
|
|
632
|
+
// Entity queries
|
|
633
|
+
const electronics = tree.$.products
|
|
634
|
+
.all()()
|
|
635
|
+
.filter((p) => p.category === 'electronics');
|
|
571
636
|
```
|
|
572
637
|
|
|
573
638
|
**Full-Stack Application:**
|
|
@@ -644,17 +709,20 @@ const customTree = signalTree(state, TREE_PRESETS.DASHBOARD);
|
|
|
644
709
|
SignalTree Core includes all enhancer functionality built-in. No separate packages needed:
|
|
645
710
|
|
|
646
711
|
```typescript
|
|
647
|
-
import { signalTree, withEntities } from '@signaltree/core';
|
|
712
|
+
import { signalTree, entityMap, withEntities } from '@signaltree/core';
|
|
648
713
|
|
|
649
|
-
|
|
714
|
+
// Without entityMap - use manual array updates
|
|
715
|
+
const basic = signalTree({ users: [] as User[] });
|
|
716
|
+
basic.$.users.update((users) => [...users, newUser]);
|
|
650
717
|
|
|
651
|
-
//
|
|
652
|
-
|
|
718
|
+
// With entityMap + withEntities - use entity helpers
|
|
719
|
+
const enhanced = signalTree({
|
|
720
|
+
users: entityMap<User>(),
|
|
721
|
+
}).with(withEntities());
|
|
653
722
|
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
users.add(newUser); // ✅ Advanced CRUD operations
|
|
723
|
+
enhanced.$.users.addOne(newUser); // ✅ Advanced CRUD operations
|
|
724
|
+
enhanced.$.users.byId(123)(); // ✅ O(1) lookups
|
|
725
|
+
enhanced.$.users.all()(); // ✅ Get all as array
|
|
658
726
|
```
|
|
659
727
|
|
|
660
728
|
Core includes several performance optimizations:
|
|
@@ -843,58 +911,49 @@ const filteredProducts = computed(() => {
|
|
|
843
911
|
### Data Management Composition
|
|
844
912
|
|
|
845
913
|
```typescript
|
|
846
|
-
import { signalTree, withEntities } from '@signaltree/core';
|
|
914
|
+
import { signalTree, entityMap, withEntities } from '@signaltree/core';
|
|
847
915
|
|
|
848
916
|
// Add data management capabilities (+2.77KB total)
|
|
849
917
|
const tree = signalTree({
|
|
850
|
-
users:
|
|
851
|
-
posts:
|
|
852
|
-
ui: { loading: false, error: null },
|
|
853
|
-
}).with(
|
|
854
|
-
withEntities() // Advanced CRUD operations
|
|
855
|
-
);
|
|
918
|
+
users: entityMap<User>(),
|
|
919
|
+
posts: entityMap<Post>(),
|
|
920
|
+
ui: { loading: false, error: null as string | null },
|
|
921
|
+
}).with(withEntities());
|
|
856
922
|
|
|
857
|
-
// Advanced entity operations
|
|
858
|
-
|
|
859
|
-
users.
|
|
860
|
-
users.
|
|
861
|
-
users.updateMany([{ id: '1', changes: { status: 'active' } }]);
|
|
923
|
+
// Advanced entity operations via tree.$ accessor
|
|
924
|
+
tree.$.users.addOne(newUser);
|
|
925
|
+
tree.$.users.selectBy((u) => u.active);
|
|
926
|
+
tree.$.users.updateMany([{ id: '1', changes: { status: 'active' } }]);
|
|
862
927
|
|
|
863
|
-
// Entity helpers
|
|
928
|
+
// Entity helpers work with nested structures
|
|
864
929
|
// Example: deeply nested entities in a domain-driven design pattern
|
|
865
930
|
const appTree = signalTree({
|
|
866
931
|
app: {
|
|
867
932
|
data: {
|
|
868
|
-
users:
|
|
869
|
-
products:
|
|
933
|
+
users: entityMap<User>(),
|
|
934
|
+
products: entityMap<Product>(),
|
|
870
935
|
},
|
|
871
936
|
},
|
|
872
937
|
admin: {
|
|
873
938
|
data: {
|
|
874
|
-
logs:
|
|
875
|
-
reports:
|
|
939
|
+
logs: entityMap<AuditLog>(),
|
|
940
|
+
reports: entityMap<Report>(),
|
|
876
941
|
},
|
|
877
942
|
},
|
|
878
|
-
});
|
|
943
|
+
}).with(withEntities());
|
|
879
944
|
|
|
880
|
-
// Access
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
// All entity methods work seamlessly with nested paths
|
|
887
|
-
appUsers.selectBy((u) => u.isAdmin); // Filtered signal
|
|
888
|
-
appProducts.selectTotal(); // Count signal
|
|
889
|
-
adminLogs.selectAll(); // All items signal
|
|
890
|
-
adminReports.selectIds(); // ID array signal
|
|
945
|
+
// Access nested entities using tree.$ accessor
|
|
946
|
+
appTree.$.app.data.users.selectBy((u) => u.isAdmin); // Filtered signal
|
|
947
|
+
appTree.$.app.data.products.selectTotal(); // Count signal
|
|
948
|
+
appTree.$.admin.data.logs.all()(); // All items as array
|
|
949
|
+
appTree.$.admin.data.reports.selectIds(); // ID array signal
|
|
891
950
|
|
|
892
951
|
// For async operations, use manual async or async helpers
|
|
893
952
|
async function fetchUsers() {
|
|
894
953
|
tree.$.ui.loading.set(true);
|
|
895
954
|
try {
|
|
896
955
|
const users = await api.getUsers();
|
|
897
|
-
tree.$.users.
|
|
956
|
+
tree.$.users.setAll(users);
|
|
898
957
|
} catch (error) {
|
|
899
958
|
tree.$.ui.error.set(error.message);
|
|
900
959
|
} finally {
|
|
@@ -936,10 +995,10 @@ const tree = signalTree({
|
|
|
936
995
|
);
|
|
937
996
|
|
|
938
997
|
// Rich feature set available
|
|
939
|
-
const users = tree.entities<User>('app.data.users');
|
|
940
998
|
async function fetchUser(id: string) {
|
|
941
999
|
return await api.getUser(id);
|
|
942
1000
|
}
|
|
1001
|
+
tree.$.app.data.users.byId(userId)(); // O(1) lookup
|
|
943
1002
|
tree.undo(); // Time travel
|
|
944
1003
|
tree.save(); // Persistence
|
|
945
1004
|
```
|
|
@@ -1308,9 +1367,11 @@ tree.effect(fn); // Create reactive effects
|
|
|
1308
1367
|
tree.subscribe(fn); // Manual subscriptions
|
|
1309
1368
|
tree.destroy(); // Cleanup resources
|
|
1310
1369
|
|
|
1311
|
-
//
|
|
1312
|
-
tree.
|
|
1313
|
-
//
|
|
1370
|
+
// Entity helpers (when using entityMap + withEntities)
|
|
1371
|
+
// tree.$.users.addOne(user); // Add single entity
|
|
1372
|
+
// tree.$.users.byId(id)(); // O(1) lookup by ID
|
|
1373
|
+
// tree.$.users.all()(); // Get all as array
|
|
1374
|
+
// tree.$.users.selectBy(pred); // Filtered signal
|
|
1314
1375
|
```
|
|
1315
1376
|
|
|
1316
1377
|
## Extending with enhancers
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const globalStructuredClone = typeof globalThis === 'object' && globalThis !== null ? globalThis.structuredClone : undefined;
|
|
2
|
+
function deepClone(value) {
|
|
3
|
+
if (globalStructuredClone) {
|
|
4
|
+
try {
|
|
5
|
+
return globalStructuredClone(value);
|
|
6
|
+
} catch (_a) {}
|
|
7
|
+
}
|
|
8
|
+
return cloneValue(value, new WeakMap());
|
|
9
|
+
}
|
|
10
|
+
function cloneValue(value, seen) {
|
|
11
|
+
if (value === null || typeof value !== 'object') {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'function') {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
const existing = seen.get(value);
|
|
18
|
+
if (existing) {
|
|
19
|
+
return existing;
|
|
20
|
+
}
|
|
21
|
+
if (value instanceof Date) {
|
|
22
|
+
return new Date(value.getTime());
|
|
23
|
+
}
|
|
24
|
+
if (value instanceof RegExp) {
|
|
25
|
+
return new RegExp(value.source, value.flags);
|
|
26
|
+
}
|
|
27
|
+
if (value instanceof Map) {
|
|
28
|
+
const result = new Map();
|
|
29
|
+
seen.set(value, result);
|
|
30
|
+
for (const [key, entryValue] of value) {
|
|
31
|
+
result.set(cloneValue(key, seen), cloneValue(entryValue, seen));
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
if (value instanceof Set) {
|
|
36
|
+
const result = new Set();
|
|
37
|
+
seen.set(value, result);
|
|
38
|
+
for (const entry of value) {
|
|
39
|
+
result.add(cloneValue(entry, seen));
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
const result = new Array(value.length);
|
|
45
|
+
seen.set(value, result);
|
|
46
|
+
for (let i = 0; i < value.length; i++) {
|
|
47
|
+
result[i] = cloneValue(value[i], seen);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
if (ArrayBuffer.isView(value)) {
|
|
52
|
+
if (value instanceof DataView) {
|
|
53
|
+
const bufferClone = cloneValue(value.buffer, seen);
|
|
54
|
+
return new DataView(bufferClone, value.byteOffset, value.byteLength);
|
|
55
|
+
}
|
|
56
|
+
const viewWithSlice = value;
|
|
57
|
+
if (typeof viewWithSlice.slice === 'function') {
|
|
58
|
+
return viewWithSlice.slice();
|
|
59
|
+
}
|
|
60
|
+
const bufferClone = cloneValue(value.buffer, seen);
|
|
61
|
+
return new value.constructor(bufferClone, value.byteOffset, value.length);
|
|
62
|
+
}
|
|
63
|
+
if (value instanceof ArrayBuffer) {
|
|
64
|
+
return value.slice(0);
|
|
65
|
+
}
|
|
66
|
+
const proto = Object.getPrototypeOf(value);
|
|
67
|
+
const result = proto ? Object.create(proto) : {};
|
|
68
|
+
seen.set(value, result);
|
|
69
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
70
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
71
|
+
if (!descriptor) continue;
|
|
72
|
+
if ('value' in descriptor) {
|
|
73
|
+
descriptor.value = cloneValue(descriptor.value, seen);
|
|
74
|
+
}
|
|
75
|
+
Object.defineProperty(result, key, descriptor);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { deepClone };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function deepEqual(a, b) {
|
|
2
|
+
if (a === b) return true;
|
|
3
|
+
if (a == null || b == null) return a === b;
|
|
4
|
+
const typeA = typeof a;
|
|
5
|
+
const typeB = typeof b;
|
|
6
|
+
if (typeA !== typeB) return false;
|
|
7
|
+
if (typeA !== 'object') return false;
|
|
8
|
+
if (a instanceof Date && b instanceof Date) {
|
|
9
|
+
return a.getTime() === b.getTime();
|
|
10
|
+
}
|
|
11
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
12
|
+
return a.source === b.source && a.flags === b.flags;
|
|
13
|
+
}
|
|
14
|
+
if (a instanceof Map && b instanceof Map) {
|
|
15
|
+
if (a.size !== b.size) return false;
|
|
16
|
+
for (const [key, value] of a) {
|
|
17
|
+
if (!b.has(key) || !deepEqual(value, b.get(key))) return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (a instanceof Set && b instanceof Set) {
|
|
22
|
+
if (a.size !== b.size) return false;
|
|
23
|
+
for (const value of a) {
|
|
24
|
+
if (!b.has(value)) return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(a)) {
|
|
29
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
30
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(b)) return false;
|
|
33
|
+
const objA = a;
|
|
34
|
+
const objB = b;
|
|
35
|
+
const keysA = Object.keys(objA);
|
|
36
|
+
const keysB = Object.keys(objB);
|
|
37
|
+
if (keysA.length !== keysB.length) return false;
|
|
38
|
+
return keysA.every(key => key in objB && deepEqual(objA[key], objB[key]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { deepEqual };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { isNodeAccessor } from '../../../lib/utils.js';
|
|
2
|
+
|
|
3
|
+
let updateQueue = [];
|
|
4
|
+
let isUpdating = false;
|
|
5
|
+
let flushTimeoutId;
|
|
6
|
+
let currentBatchingConfig = {};
|
|
7
|
+
function addToQueue(update, config = currentBatchingConfig) {
|
|
8
|
+
const maxSize = config.maxBatchSize ?? 100;
|
|
9
|
+
if (update.path) {
|
|
10
|
+
updateQueue = updateQueue.filter(existing => existing.path !== update.path);
|
|
11
|
+
}
|
|
12
|
+
updateQueue.push(update);
|
|
13
|
+
if (updateQueue.length > maxSize) {
|
|
14
|
+
flushUpdates();
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
scheduleFlush(config);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function scheduleFlush(config) {
|
|
21
|
+
if (flushTimeoutId !== undefined) {
|
|
22
|
+
clearTimeout(flushTimeoutId);
|
|
23
|
+
}
|
|
24
|
+
const delay = config.autoFlushDelay ?? 16;
|
|
25
|
+
flushTimeoutId = setTimeout(() => {
|
|
26
|
+
flushUpdates();
|
|
27
|
+
}, delay);
|
|
28
|
+
}
|
|
29
|
+
function flushUpdates() {
|
|
30
|
+
if (isUpdating) return;
|
|
31
|
+
let queue;
|
|
32
|
+
do {
|
|
33
|
+
if (updateQueue.length === 0) return;
|
|
34
|
+
isUpdating = true;
|
|
35
|
+
queue = updateQueue;
|
|
36
|
+
updateQueue = [];
|
|
37
|
+
if (flushTimeoutId !== undefined) {
|
|
38
|
+
clearTimeout(flushTimeoutId);
|
|
39
|
+
flushTimeoutId = undefined;
|
|
40
|
+
}
|
|
41
|
+
queue.sort((a, b) => (b.depth ?? 0) - (a.depth ?? 0));
|
|
42
|
+
try {
|
|
43
|
+
queue.forEach(({
|
|
44
|
+
fn
|
|
45
|
+
}) => fn());
|
|
46
|
+
} finally {
|
|
47
|
+
isUpdating = false;
|
|
48
|
+
}
|
|
49
|
+
} while (updateQueue.length > 0);
|
|
50
|
+
}
|
|
51
|
+
function batchUpdates(fn, path) {
|
|
52
|
+
const startTime = performance.now();
|
|
53
|
+
const depth = 0;
|
|
54
|
+
const update = {
|
|
55
|
+
fn,
|
|
56
|
+
startTime,
|
|
57
|
+
depth,
|
|
58
|
+
path
|
|
59
|
+
};
|
|
60
|
+
const wasFlushed = addToQueue(update, currentBatchingConfig);
|
|
61
|
+
if (!wasFlushed) {
|
|
62
|
+
const isTimedOut = currentBatchingConfig.batchTimeoutMs && updateQueue.length > 0 && startTime - updateQueue[0].startTime >= currentBatchingConfig.batchTimeoutMs;
|
|
63
|
+
if (isTimedOut) {
|
|
64
|
+
flushUpdates();
|
|
65
|
+
} else if (!isUpdating && updateQueue.length > 0) {
|
|
66
|
+
queueMicrotask(() => {
|
|
67
|
+
flushUpdates();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function withBatching(config = {}) {
|
|
73
|
+
const {
|
|
74
|
+
enabled = true
|
|
75
|
+
} = config;
|
|
76
|
+
currentBatchingConfig = {
|
|
77
|
+
...currentBatchingConfig,
|
|
78
|
+
...config
|
|
79
|
+
};
|
|
80
|
+
return tree => {
|
|
81
|
+
if (!enabled) {
|
|
82
|
+
return tree;
|
|
83
|
+
}
|
|
84
|
+
const originalTreeCall = tree.bind(tree);
|
|
85
|
+
const enhancedTree = function (...args) {
|
|
86
|
+
if (args.length === 0) {
|
|
87
|
+
return originalTreeCall();
|
|
88
|
+
} else {
|
|
89
|
+
batchUpdates(() => {
|
|
90
|
+
if (args.length === 1) {
|
|
91
|
+
const arg = args[0];
|
|
92
|
+
if (typeof arg === 'function') {
|
|
93
|
+
originalTreeCall(arg);
|
|
94
|
+
} else {
|
|
95
|
+
originalTreeCall(arg);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
Object.setPrototypeOf(enhancedTree, Object.getPrototypeOf(tree));
|
|
102
|
+
Object.assign(enhancedTree, tree);
|
|
103
|
+
if ('state' in tree) {
|
|
104
|
+
Object.defineProperty(enhancedTree, 'state', {
|
|
105
|
+
value: tree.state,
|
|
106
|
+
enumerable: false,
|
|
107
|
+
configurable: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if ('$' in tree) {
|
|
111
|
+
Object.defineProperty(enhancedTree, '$', {
|
|
112
|
+
value: tree['$'],
|
|
113
|
+
enumerable: false,
|
|
114
|
+
configurable: true
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
enhancedTree.batchUpdate = updater => {
|
|
118
|
+
batchUpdates(() => {
|
|
119
|
+
const current = originalTreeCall();
|
|
120
|
+
const updates = updater(current);
|
|
121
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
122
|
+
const property = enhancedTree.state[key];
|
|
123
|
+
if (property && 'set' in property) {
|
|
124
|
+
property.set(value);
|
|
125
|
+
} else if (isNodeAccessor(property)) {
|
|
126
|
+
property(value);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
return enhancedTree;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function withHighPerformanceBatching() {
|
|
135
|
+
return withBatching({
|
|
136
|
+
enabled: true,
|
|
137
|
+
maxBatchSize: 200,
|
|
138
|
+
batchTimeoutMs: 0
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function flushBatchedUpdates() {
|
|
142
|
+
if (updateQueue.length > 0) {
|
|
143
|
+
const queue = updateQueue.slice();
|
|
144
|
+
updateQueue = [];
|
|
145
|
+
isUpdating = false;
|
|
146
|
+
queue.sort((a, b) => (b.depth ?? 0) - (a.depth ?? 0));
|
|
147
|
+
queue.forEach(({
|
|
148
|
+
fn
|
|
149
|
+
}) => {
|
|
150
|
+
fn();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function hasPendingUpdates() {
|
|
155
|
+
return updateQueue.length > 0;
|
|
156
|
+
}
|
|
157
|
+
function getBatchQueueSize() {
|
|
158
|
+
return updateQueue.length;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, withBatching, withHighPerformanceBatching };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { computed } from '@angular/core';
|
|
2
|
+
import { createEnhancer } from '../../index.js';
|
|
3
|
+
|
|
4
|
+
function computedEnhancer(_config = {}) {
|
|
5
|
+
return createEnhancer({
|
|
6
|
+
name: 'computed',
|
|
7
|
+
provides: ['computed'],
|
|
8
|
+
requires: []
|
|
9
|
+
}, tree => {
|
|
10
|
+
const computedTree = tree;
|
|
11
|
+
computedTree.computed = function (computeFn) {
|
|
12
|
+
return computed(() => computeFn(tree.state));
|
|
13
|
+
};
|
|
14
|
+
return computedTree;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function createComputed(dependencies, computeFn) {
|
|
18
|
+
return computed(computeFn);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { computedEnhancer, createComputed };
|