@sylphx/lens-signals 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/dist/index.d.ts +327 -0
- package/dist/index.js +528 -0
- package/package.json +44 -0
- package/src/index.ts +58 -0
- package/src/reactive-store.ts +805 -0
- package/src/signal.ts +159 -0
- package/src/store-types.ts +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// src/signal.ts
|
|
2
|
+
import {
|
|
3
|
+
batch as preactBatch,
|
|
4
|
+
computed as preactComputed,
|
|
5
|
+
effect as preactEffect,
|
|
6
|
+
signal as preactSignal
|
|
7
|
+
} from "@preact/signals-core";
|
|
8
|
+
function signal(initial) {
|
|
9
|
+
return preactSignal(initial);
|
|
10
|
+
}
|
|
11
|
+
function computed(compute) {
|
|
12
|
+
return preactComputed(compute);
|
|
13
|
+
}
|
|
14
|
+
function effect(fn) {
|
|
15
|
+
return preactEffect(fn);
|
|
16
|
+
}
|
|
17
|
+
function batch(fn) {
|
|
18
|
+
return preactBatch(fn);
|
|
19
|
+
}
|
|
20
|
+
function isSignal(value) {
|
|
21
|
+
return value !== null && typeof value === "object" && "value" in value && "peek" in value && "subscribe" in value;
|
|
22
|
+
}
|
|
23
|
+
function toPromise(sig) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let isFirst = true;
|
|
26
|
+
let unsub;
|
|
27
|
+
unsub = sig.subscribe((value) => {
|
|
28
|
+
if (isFirst) {
|
|
29
|
+
isFirst = false;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
unsub();
|
|
33
|
+
resolve(value);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function derive(signals, fn) {
|
|
38
|
+
return computed(() => fn(signals.map((s) => s.value)));
|
|
39
|
+
}
|
|
40
|
+
// src/reactive-store.ts
|
|
41
|
+
import { applyUpdate, makeEntityKey } from "@sylphx/lens-core";
|
|
42
|
+
import {
|
|
43
|
+
createCachePlugin,
|
|
44
|
+
execute,
|
|
45
|
+
registerPlugin,
|
|
46
|
+
unregisterPlugin
|
|
47
|
+
} from "@sylphx/reify";
|
|
48
|
+
class ReactiveStore {
|
|
49
|
+
entities = new Map;
|
|
50
|
+
lists = new Map;
|
|
51
|
+
optimisticUpdates = new Map;
|
|
52
|
+
optimisticTransactions = new Map;
|
|
53
|
+
config;
|
|
54
|
+
tagIndex = new Map;
|
|
55
|
+
constructor(config = {}) {
|
|
56
|
+
this.config = {
|
|
57
|
+
optimistic: config.optimistic ?? true,
|
|
58
|
+
cacheTTL: config.cacheTTL ?? 5 * 60 * 1000,
|
|
59
|
+
maxCacheSize: config.maxCacheSize ?? 1000,
|
|
60
|
+
cascadeRules: config.cascadeRules ?? []
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
getEntity(entityName, entityId) {
|
|
64
|
+
const key = this.makeKey(entityName, entityId);
|
|
65
|
+
if (!this.entities.has(key)) {
|
|
66
|
+
this.entities.set(key, signal({
|
|
67
|
+
data: null,
|
|
68
|
+
loading: true,
|
|
69
|
+
error: null,
|
|
70
|
+
stale: false,
|
|
71
|
+
refCount: 0
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
return this.entities.get(key);
|
|
75
|
+
}
|
|
76
|
+
setEntity(entityName, entityId, data, tags) {
|
|
77
|
+
const key = this.makeKey(entityName, entityId);
|
|
78
|
+
const entitySignal = this.entities.get(key);
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
if (entitySignal) {
|
|
81
|
+
entitySignal.value = {
|
|
82
|
+
...entitySignal.value,
|
|
83
|
+
data,
|
|
84
|
+
loading: false,
|
|
85
|
+
error: null,
|
|
86
|
+
stale: false,
|
|
87
|
+
cachedAt: now,
|
|
88
|
+
tags: tags ?? entitySignal.value.tags
|
|
89
|
+
};
|
|
90
|
+
} else {
|
|
91
|
+
this.entities.set(key, signal({
|
|
92
|
+
data,
|
|
93
|
+
loading: false,
|
|
94
|
+
error: null,
|
|
95
|
+
stale: false,
|
|
96
|
+
refCount: 0,
|
|
97
|
+
cachedAt: now,
|
|
98
|
+
tags
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
if (tags) {
|
|
102
|
+
for (const tag of tags) {
|
|
103
|
+
if (!this.tagIndex.has(tag)) {
|
|
104
|
+
this.tagIndex.set(tag, new Set);
|
|
105
|
+
}
|
|
106
|
+
this.tagIndex.get(tag).add(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
applyServerUpdate(entityName, entityId, update) {
|
|
111
|
+
const key = this.makeKey(entityName, entityId);
|
|
112
|
+
const entitySignal = this.entities.get(key);
|
|
113
|
+
if (entitySignal && entitySignal.value.data != null) {
|
|
114
|
+
const newData = applyUpdate(entitySignal.value.data, update);
|
|
115
|
+
entitySignal.value = {
|
|
116
|
+
...entitySignal.value,
|
|
117
|
+
data: newData,
|
|
118
|
+
stale: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
setEntityError(entityName, entityId, error) {
|
|
123
|
+
const key = this.makeKey(entityName, entityId);
|
|
124
|
+
const entitySignal = this.entities.get(key);
|
|
125
|
+
if (entitySignal) {
|
|
126
|
+
entitySignal.value = {
|
|
127
|
+
...entitySignal.value,
|
|
128
|
+
loading: false,
|
|
129
|
+
error
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
setEntityLoading(entityName, entityId, loading) {
|
|
134
|
+
const key = this.makeKey(entityName, entityId);
|
|
135
|
+
const entitySignal = this.entities.get(key);
|
|
136
|
+
if (entitySignal) {
|
|
137
|
+
entitySignal.value = {
|
|
138
|
+
...entitySignal.value,
|
|
139
|
+
loading
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
removeEntity(entityName, entityId) {
|
|
144
|
+
const key = this.makeKey(entityName, entityId);
|
|
145
|
+
this.entities.delete(key);
|
|
146
|
+
}
|
|
147
|
+
hasEntity(entityName, entityId) {
|
|
148
|
+
const key = this.makeKey(entityName, entityId);
|
|
149
|
+
return this.entities.has(key);
|
|
150
|
+
}
|
|
151
|
+
getList(queryKey) {
|
|
152
|
+
if (!this.lists.has(queryKey)) {
|
|
153
|
+
this.lists.set(queryKey, signal({
|
|
154
|
+
data: null,
|
|
155
|
+
loading: true,
|
|
156
|
+
error: null,
|
|
157
|
+
stale: false,
|
|
158
|
+
refCount: 0
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
return this.lists.get(queryKey);
|
|
162
|
+
}
|
|
163
|
+
setList(queryKey, data) {
|
|
164
|
+
const listSignal = this.lists.get(queryKey);
|
|
165
|
+
if (listSignal) {
|
|
166
|
+
listSignal.value = {
|
|
167
|
+
...listSignal.value,
|
|
168
|
+
data,
|
|
169
|
+
loading: false,
|
|
170
|
+
error: null,
|
|
171
|
+
stale: false
|
|
172
|
+
};
|
|
173
|
+
} else {
|
|
174
|
+
this.lists.set(queryKey, signal({
|
|
175
|
+
data,
|
|
176
|
+
loading: false,
|
|
177
|
+
error: null,
|
|
178
|
+
stale: false,
|
|
179
|
+
refCount: 0
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
applyOptimistic(entityName, type, data) {
|
|
184
|
+
if (!this.config.optimistic) {
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
const optimisticId = `opt_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
188
|
+
const entityId = data.id;
|
|
189
|
+
const key = this.makeKey(entityName, entityId);
|
|
190
|
+
const entitySignal = this.entities.get(key);
|
|
191
|
+
const originalData = entitySignal?.value.data ?? null;
|
|
192
|
+
batch(() => {
|
|
193
|
+
switch (type) {
|
|
194
|
+
case "create":
|
|
195
|
+
this.setEntity(entityName, entityId, data);
|
|
196
|
+
break;
|
|
197
|
+
case "update":
|
|
198
|
+
if (entitySignal?.value.data) {
|
|
199
|
+
this.setEntity(entityName, entityId, {
|
|
200
|
+
...entitySignal.value.data,
|
|
201
|
+
...data
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
case "delete":
|
|
206
|
+
if (entitySignal) {
|
|
207
|
+
entitySignal.value = {
|
|
208
|
+
...entitySignal.value,
|
|
209
|
+
data: null
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
this.optimisticUpdates.set(optimisticId, {
|
|
216
|
+
id: optimisticId,
|
|
217
|
+
entityName,
|
|
218
|
+
entityId,
|
|
219
|
+
type,
|
|
220
|
+
originalData,
|
|
221
|
+
optimisticData: data,
|
|
222
|
+
timestamp: Date.now()
|
|
223
|
+
});
|
|
224
|
+
return optimisticId;
|
|
225
|
+
}
|
|
226
|
+
confirmOptimistic(optimisticId, serverData) {
|
|
227
|
+
const entry = this.optimisticUpdates.get(optimisticId);
|
|
228
|
+
if (!entry)
|
|
229
|
+
return;
|
|
230
|
+
if (serverData !== undefined && entry.type !== "delete") {
|
|
231
|
+
this.setEntity(entry.entityName, entry.entityId, serverData);
|
|
232
|
+
}
|
|
233
|
+
this.optimisticUpdates.delete(optimisticId);
|
|
234
|
+
}
|
|
235
|
+
rollbackOptimistic(optimisticId) {
|
|
236
|
+
const entry = this.optimisticUpdates.get(optimisticId);
|
|
237
|
+
if (!entry)
|
|
238
|
+
return;
|
|
239
|
+
batch(() => {
|
|
240
|
+
switch (entry.type) {
|
|
241
|
+
case "create":
|
|
242
|
+
this.removeEntity(entry.entityName, entry.entityId);
|
|
243
|
+
break;
|
|
244
|
+
case "update":
|
|
245
|
+
case "delete":
|
|
246
|
+
if (entry.originalData !== null) {
|
|
247
|
+
this.setEntity(entry.entityName, entry.entityId, entry.originalData);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
this.optimisticUpdates.delete(optimisticId);
|
|
253
|
+
}
|
|
254
|
+
getPendingOptimistic() {
|
|
255
|
+
return Array.from(this.optimisticUpdates.values());
|
|
256
|
+
}
|
|
257
|
+
async applyPipelineOptimistic(pipeline, input) {
|
|
258
|
+
if (!this.config.optimistic) {
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
const txId = `tx_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
262
|
+
const originalData = new Map;
|
|
263
|
+
const cacheAdapter = {
|
|
264
|
+
get: (key) => {
|
|
265
|
+
const [entityName, entityId] = key.split(":");
|
|
266
|
+
const entitySignal = this.entities.get(this.makeKey(entityName, entityId));
|
|
267
|
+
return entitySignal?.value.data ?? undefined;
|
|
268
|
+
},
|
|
269
|
+
set: (key, value) => {
|
|
270
|
+
const [entityName, entityId] = key.split(":");
|
|
271
|
+
const storeKey = this.makeKey(entityName, entityId);
|
|
272
|
+
if (!originalData.has(storeKey)) {
|
|
273
|
+
const entitySignal = this.entities.get(storeKey);
|
|
274
|
+
originalData.set(storeKey, entitySignal?.value.data ?? null);
|
|
275
|
+
}
|
|
276
|
+
this.setEntity(entityName, entityId, value);
|
|
277
|
+
},
|
|
278
|
+
delete: (key) => {
|
|
279
|
+
const [entityName, entityId] = key.split(":");
|
|
280
|
+
const storeKey = this.makeKey(entityName, entityId);
|
|
281
|
+
if (!originalData.has(storeKey)) {
|
|
282
|
+
const entitySignal2 = this.entities.get(storeKey);
|
|
283
|
+
originalData.set(storeKey, entitySignal2?.value.data ?? null);
|
|
284
|
+
}
|
|
285
|
+
const entitySignal = this.entities.get(storeKey);
|
|
286
|
+
if (entitySignal) {
|
|
287
|
+
entitySignal.value = { ...entitySignal.value, data: null };
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
},
|
|
291
|
+
has: (key) => {
|
|
292
|
+
const [entityName, entityId] = key.split(":");
|
|
293
|
+
return this.entities.has(this.makeKey(entityName, entityId));
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const cachePlugin = createCachePlugin(cacheAdapter);
|
|
297
|
+
registerPlugin(cachePlugin);
|
|
298
|
+
let results;
|
|
299
|
+
try {
|
|
300
|
+
results = await execute(pipeline, input);
|
|
301
|
+
} finally {
|
|
302
|
+
unregisterPlugin("entity");
|
|
303
|
+
}
|
|
304
|
+
this.optimisticTransactions.set(txId, {
|
|
305
|
+
id: txId,
|
|
306
|
+
results,
|
|
307
|
+
originalData,
|
|
308
|
+
timestamp: Date.now()
|
|
309
|
+
});
|
|
310
|
+
return txId;
|
|
311
|
+
}
|
|
312
|
+
confirmPipelineOptimistic(txId, serverResults) {
|
|
313
|
+
const tx = this.optimisticTransactions.get(txId);
|
|
314
|
+
if (!tx)
|
|
315
|
+
return;
|
|
316
|
+
if (serverResults) {
|
|
317
|
+
batch(() => {
|
|
318
|
+
for (const result of serverResults) {
|
|
319
|
+
this.removeEntity(result.entity, result.tempId);
|
|
320
|
+
const realData = result.data;
|
|
321
|
+
if (realData?.id) {
|
|
322
|
+
this.setEntity(result.entity, realData.id, realData);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
this.optimisticTransactions.delete(txId);
|
|
328
|
+
}
|
|
329
|
+
rollbackPipelineOptimistic(txId) {
|
|
330
|
+
const tx = this.optimisticTransactions.get(txId);
|
|
331
|
+
if (!tx)
|
|
332
|
+
return;
|
|
333
|
+
batch(() => {
|
|
334
|
+
for (const [key, originalData] of tx.originalData) {
|
|
335
|
+
const [entityName, entityId] = key.split(":");
|
|
336
|
+
if (originalData === null) {
|
|
337
|
+
this.removeEntity(entityName, entityId);
|
|
338
|
+
} else {
|
|
339
|
+
this.setEntity(entityName, entityId, originalData);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
this.optimisticTransactions.delete(txId);
|
|
344
|
+
}
|
|
345
|
+
getPendingTransactions() {
|
|
346
|
+
return Array.from(this.optimisticTransactions.values());
|
|
347
|
+
}
|
|
348
|
+
invalidate(entityName, entityId, options) {
|
|
349
|
+
const key = this.makeKey(entityName, entityId);
|
|
350
|
+
this.markStale(key);
|
|
351
|
+
if (options?.cascade !== false) {
|
|
352
|
+
this.cascadeInvalidate(entityName, "update");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
invalidateEntity(entityName, options) {
|
|
356
|
+
for (const key of this.entities.keys()) {
|
|
357
|
+
if (key.startsWith(`${entityName}:`)) {
|
|
358
|
+
this.markStale(key);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const listKey of this.lists.keys()) {
|
|
362
|
+
if (listKey.includes(entityName)) {
|
|
363
|
+
const listSignal = this.lists.get(listKey);
|
|
364
|
+
if (listSignal) {
|
|
365
|
+
listSignal.value = { ...listSignal.value, stale: true };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (options?.cascade !== false) {
|
|
370
|
+
this.cascadeInvalidate(entityName, "update");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
invalidateByTags(tags) {
|
|
374
|
+
let count = 0;
|
|
375
|
+
for (const tag of tags) {
|
|
376
|
+
const keys = this.tagIndex.get(tag);
|
|
377
|
+
if (keys) {
|
|
378
|
+
for (const key of keys) {
|
|
379
|
+
this.markStale(key);
|
|
380
|
+
count++;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return count;
|
|
385
|
+
}
|
|
386
|
+
invalidateByPattern(pattern) {
|
|
387
|
+
const regex = this.patternToRegex(pattern);
|
|
388
|
+
let count = 0;
|
|
389
|
+
for (const key of this.entities.keys()) {
|
|
390
|
+
if (regex.test(key)) {
|
|
391
|
+
this.markStale(key);
|
|
392
|
+
count++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return count;
|
|
396
|
+
}
|
|
397
|
+
tagEntity(entityName, entityId, tags) {
|
|
398
|
+
const key = this.makeKey(entityName, entityId);
|
|
399
|
+
const entitySignal = this.entities.get(key);
|
|
400
|
+
if (entitySignal) {
|
|
401
|
+
entitySignal.value = {
|
|
402
|
+
...entitySignal.value,
|
|
403
|
+
tags: [...new Set([...entitySignal.value.tags ?? [], ...tags])]
|
|
404
|
+
};
|
|
405
|
+
for (const tag of tags) {
|
|
406
|
+
if (!this.tagIndex.has(tag)) {
|
|
407
|
+
this.tagIndex.set(tag, new Set);
|
|
408
|
+
}
|
|
409
|
+
this.tagIndex.get(tag).add(key);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
isStale(entityName, entityId) {
|
|
414
|
+
const key = this.makeKey(entityName, entityId);
|
|
415
|
+
const entitySignal = this.entities.get(key);
|
|
416
|
+
if (!entitySignal)
|
|
417
|
+
return true;
|
|
418
|
+
if (entitySignal.value.stale)
|
|
419
|
+
return true;
|
|
420
|
+
if (!entitySignal.value.cachedAt)
|
|
421
|
+
return false;
|
|
422
|
+
return Date.now() - entitySignal.value.cachedAt > this.config.cacheTTL;
|
|
423
|
+
}
|
|
424
|
+
getStaleWhileRevalidate(entityName, entityId, revalidate) {
|
|
425
|
+
const key = this.makeKey(entityName, entityId);
|
|
426
|
+
const entitySignal = this.entities.get(key);
|
|
427
|
+
const isStale = this.isStale(entityName, entityId);
|
|
428
|
+
let revalidating = null;
|
|
429
|
+
if (isStale && entitySignal?.value.data != null) {
|
|
430
|
+
revalidating = revalidate().then((newData) => {
|
|
431
|
+
this.setEntity(entityName, entityId, newData);
|
|
432
|
+
return newData;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
data: entitySignal?.value.data ?? null,
|
|
437
|
+
isStale,
|
|
438
|
+
revalidating
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
markStale(key) {
|
|
442
|
+
const entitySignal = this.entities.get(key);
|
|
443
|
+
if (entitySignal) {
|
|
444
|
+
entitySignal.value = { ...entitySignal.value, stale: true };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
cascadeInvalidate(entityName, operation) {
|
|
448
|
+
for (const rule of this.config.cascadeRules) {
|
|
449
|
+
if (rule.source !== entityName)
|
|
450
|
+
continue;
|
|
451
|
+
if (rule.operations && !rule.operations.includes(operation))
|
|
452
|
+
continue;
|
|
453
|
+
for (const target of rule.targets) {
|
|
454
|
+
this.invalidateEntity(target, { cascade: false });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
patternToRegex(pattern) {
|
|
459
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
460
|
+
return new RegExp(`^${escaped}$`);
|
|
461
|
+
}
|
|
462
|
+
retain(entityName, entityId) {
|
|
463
|
+
const key = this.makeKey(entityName, entityId);
|
|
464
|
+
const entitySignal = this.entities.get(key);
|
|
465
|
+
if (entitySignal) {
|
|
466
|
+
entitySignal.value = {
|
|
467
|
+
...entitySignal.value,
|
|
468
|
+
refCount: entitySignal.value.refCount + 1
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
release(entityName, entityId) {
|
|
473
|
+
const key = this.makeKey(entityName, entityId);
|
|
474
|
+
const entitySignal = this.entities.get(key);
|
|
475
|
+
if (entitySignal) {
|
|
476
|
+
const newRefCount = Math.max(0, entitySignal.value.refCount - 1);
|
|
477
|
+
entitySignal.value = {
|
|
478
|
+
...entitySignal.value,
|
|
479
|
+
refCount: newRefCount
|
|
480
|
+
};
|
|
481
|
+
if (newRefCount === 0) {
|
|
482
|
+
entitySignal.value = {
|
|
483
|
+
...entitySignal.value,
|
|
484
|
+
stale: true
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
gc() {
|
|
490
|
+
let cleared = 0;
|
|
491
|
+
for (const [key, entitySignal] of this.entities) {
|
|
492
|
+
if (entitySignal.value.stale && entitySignal.value.refCount === 0) {
|
|
493
|
+
this.entities.delete(key);
|
|
494
|
+
cleared++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return cleared;
|
|
498
|
+
}
|
|
499
|
+
clear() {
|
|
500
|
+
this.entities.clear();
|
|
501
|
+
this.lists.clear();
|
|
502
|
+
this.optimisticUpdates.clear();
|
|
503
|
+
}
|
|
504
|
+
makeKey(entityName, entityId) {
|
|
505
|
+
return makeEntityKey(entityName, entityId);
|
|
506
|
+
}
|
|
507
|
+
getStats() {
|
|
508
|
+
return {
|
|
509
|
+
entities: this.entities.size,
|
|
510
|
+
lists: this.lists.size,
|
|
511
|
+
pendingOptimistic: this.optimisticUpdates.size
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function createStore(config) {
|
|
516
|
+
return new ReactiveStore(config);
|
|
517
|
+
}
|
|
518
|
+
export {
|
|
519
|
+
toPromise,
|
|
520
|
+
signal,
|
|
521
|
+
isSignal,
|
|
522
|
+
effect,
|
|
523
|
+
derive,
|
|
524
|
+
createStore,
|
|
525
|
+
computed,
|
|
526
|
+
batch,
|
|
527
|
+
ReactiveStore
|
|
528
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sylphx/lens-signals",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Signals-based reactive store for Lens client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bunup",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "echo 'no tests yet'",
|
|
18
|
+
"prepack": "[ -d dist ] || bun run build"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"lens",
|
|
26
|
+
"signals",
|
|
27
|
+
"reactive",
|
|
28
|
+
"store",
|
|
29
|
+
"preact"
|
|
30
|
+
],
|
|
31
|
+
"author": "SylphxAI",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@sylphx/lens-core": "^2.0.1",
|
|
35
|
+
"@sylphx/reify": "^0.1.2"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@preact/signals-core": ">=1.8.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@preact/signals-core": "^1.8.0",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-signals
|
|
3
|
+
*
|
|
4
|
+
* Signals-based reactive store for Lens client.
|
|
5
|
+
* Uses Preact Signals for fine-grained reactivity.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createStore, signal, computed, effect } from "@sylphx/lens-signals";
|
|
10
|
+
*
|
|
11
|
+
* // Create reactive store
|
|
12
|
+
* const store = createStore();
|
|
13
|
+
*
|
|
14
|
+
* // Get entity signal
|
|
15
|
+
* const user = store.getEntity<User>("User", "123");
|
|
16
|
+
*
|
|
17
|
+
* // React to changes
|
|
18
|
+
* effect(() => {
|
|
19
|
+
* console.log("User:", user.value.data);
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Signals
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
batch,
|
|
30
|
+
computed,
|
|
31
|
+
derive,
|
|
32
|
+
effect,
|
|
33
|
+
isSignal,
|
|
34
|
+
// Types
|
|
35
|
+
type Signal,
|
|
36
|
+
type Subscriber,
|
|
37
|
+
// Functions
|
|
38
|
+
signal,
|
|
39
|
+
toPromise,
|
|
40
|
+
type Unsubscribe,
|
|
41
|
+
type WritableSignal,
|
|
42
|
+
} from "./signal.js";
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Store
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
type CascadeRule,
|
|
50
|
+
createStore,
|
|
51
|
+
type EntityKey,
|
|
52
|
+
type EntityState,
|
|
53
|
+
type InvalidationOptions,
|
|
54
|
+
type OptimisticEntry,
|
|
55
|
+
type OptimisticTransaction,
|
|
56
|
+
ReactiveStore,
|
|
57
|
+
type StoreConfig,
|
|
58
|
+
} from "./reactive-store.js";
|