@actdim/utico 1.1.3 → 1.1.6
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 +502 -63
- package/dist/arrayExtensions.es.js +29 -29
- package/dist/arrayExtensions.es.js.map +1 -1
- package/dist/cache/memoryCache.d.ts +2 -3
- package/dist/cache/memoryCache.d.ts.map +1 -1
- package/dist/cache/memoryCache.es.js +2 -4
- package/dist/cache/memoryCache.es.js.map +1 -1
- package/dist/cache/persistentCache.d.ts +4 -2
- package/dist/cache/persistentCache.d.ts.map +1 -1
- package/dist/cache/persistentCache.es.js +25 -26
- package/dist/cache/persistentCache.es.js.map +1 -1
- package/dist/dataFormats.d.ts +6 -1
- package/dist/dataFormats.d.ts.map +1 -1
- package/dist/dataFormats.es.js +6 -4
- package/dist/dataFormats.es.js.map +1 -1
- package/dist/dateTimeDataFormat.d.ts +62 -25
- package/dist/dateTimeDataFormat.d.ts.map +1 -1
- package/dist/dateTimeDataFormat.es.js +179 -82
- package/dist/dateTimeDataFormat.es.js.map +1 -1
- package/dist/i18n/cultures.d.ts +36 -8
- package/dist/i18n/cultures.d.ts.map +1 -1
- package/dist/i18n/cultures.es.js +8 -6
- package/dist/i18n/cultures.es.js.map +1 -1
- package/dist/i18n/enUsCulture.d.ts +4 -4
- package/dist/i18n/enUsCulture.d.ts.map +1 -1
- package/dist/i18n/enUsCulture.es.js +17 -18
- package/dist/i18n/enUsCulture.es.js.map +1 -1
- package/dist/i18n/euCulture.d.ts +30 -0
- package/dist/i18n/euCulture.d.ts.map +1 -0
- package/dist/i18n/euCulture.es.js +32 -0
- package/dist/i18n/euCulture.es.js.map +1 -0
- package/dist/i18n/invariantCulture.d.ts +30 -0
- package/dist/i18n/invariantCulture.d.ts.map +1 -0
- package/dist/i18n/invariantCulture.es.js +32 -0
- package/dist/i18n/invariantCulture.es.js.map +1 -0
- package/dist/store/storeContracts.d.ts +2 -0
- package/dist/store/storeContracts.d.ts.map +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -2,20 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
A modern foundation toolkit for complex TypeScript apps.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/@actdim/utico)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
5
9
|
## Table of Contents
|
|
6
10
|
|
|
7
11
|
- [Installation](#installation)
|
|
8
12
|
- [Modules](#modules)
|
|
9
13
|
- [typeCore — Expressive Type Composition](#typecore--expressive-type-composition)
|
|
10
14
|
- [typeUtils — Runtime Type Utilities](#typeutils--runtime-type-utilities)
|
|
15
|
+
- [stringCore — Locale-Aware String Utilities](#stringcore--locale-aware-string-utilities)
|
|
16
|
+
- [metadata — Property Metadata](#metadata--property-metadata)
|
|
17
|
+
- [decorators — Property Decorators](#decorators--property-decorators)
|
|
18
|
+
- [dateTimeDataFormat — Date/Time Serialisation](#datetimedataformat--datetime-serialisation)
|
|
11
19
|
- [StructEvent — Typed DOM Events](#structevent--typed-dom-events)
|
|
12
20
|
- [watchable — Promise & Function Tracking](#watchable--promise--function-tracking)
|
|
13
21
|
- [asyncMutex — Async Mutual Exclusion](#asyncmutex--async-mutual-exclusion)
|
|
14
22
|
- [store — Structured Persistence](#store--structured-persistence)
|
|
23
|
+
- [cache — Persistent Cache](#cache--persistent-cache)
|
|
15
24
|
- [License](#license)
|
|
16
25
|
|
|
17
26
|
---
|
|
18
27
|
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
Try @actdim/utico instantly in your browser without any installation:
|
|
31
|
+
|
|
32
|
+
[](https://stackblitz.com/~/github.com/actdim/utico)
|
|
33
|
+
|
|
34
|
+
Once the project loads, run the tests to see it in action:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm run test
|
|
38
|
+
```
|
|
39
|
+
|
|
19
40
|
## Installation
|
|
20
41
|
|
|
21
42
|
```bash
|
|
@@ -27,10 +48,10 @@ pnpm add @actdim/utico
|
|
|
27
48
|
**Peer dependencies** (install only what you use):
|
|
28
49
|
|
|
29
50
|
```bash
|
|
30
|
-
pnpm add dexie uuid
|
|
51
|
+
pnpm add dexie uuid luxon
|
|
31
52
|
```
|
|
32
53
|
|
|
33
|
-
> `dexie` and `uuid` are required for the `store` module. `
|
|
54
|
+
> `dexie` and `uuid` are required for the `store` module. `luxon` is required for the `dateTimeDataFormat` module.
|
|
34
55
|
|
|
35
56
|
---
|
|
36
57
|
|
|
@@ -196,7 +217,7 @@ generic class without repeating its type arguments everywhere.
|
|
|
196
217
|
(see [StructEvent](#structevent--typed-dom-events)).
|
|
197
218
|
Inside `PersistentCache` you want to work with
|
|
198
219
|
`StructEvent<PersistentCacheEventStruct, PersistentCache>` as if it were its own named type.
|
|
199
|
-
TypeScript offers four ways to achieve this; each has different trade-offs.
|
|
220
|
+
TypeScript offers four ways to achieve this; each has different trade-offs — see the [full comparison](#comparison-4-ways-to-bind-a-generic-constructor) at the end of this section.
|
|
200
221
|
|
|
201
222
|
---
|
|
202
223
|
|
|
@@ -248,63 +269,6 @@ const evt = PersistentCacheEvent("evict", { detail: { records }, target: this })
|
|
|
248
269
|
|
|
249
270
|
---
|
|
250
271
|
|
|
251
|
-
#### Comparison: 4 ways to bind a generic constructor
|
|
252
|
-
|
|
253
|
-
All four examples produce a bound alias for `StructEvent<PersistentCacheEventStruct, PersistentCache>`.
|
|
254
|
-
|
|
255
|
-
**1. Subclass**
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
class PersistentCacheEvent
|
|
259
|
-
extends StructEvent<PersistentCacheEventStruct, PersistentCache> {}
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
**2. Manual cast**
|
|
263
|
-
|
|
264
|
-
```ts
|
|
265
|
-
type PersistentCacheEvent = StructEvent<PersistentCacheEventStruct, PersistentCache>;
|
|
266
|
-
const PersistentCacheEvent = StructEvent as new (
|
|
267
|
-
...args: ConstructorParameters<typeof StructEvent<PersistentCacheEventStruct, PersistentCache>>
|
|
268
|
-
) => PersistentCacheEvent;
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
**3. `typed()` + Instantiation Expression** *(recommended)*
|
|
272
|
-
|
|
273
|
-
```ts
|
|
274
|
-
const PersistentCacheEvent = typed(StructEvent<PersistentCacheEventStruct, PersistentCache>);
|
|
275
|
-
const evt = new PersistentCacheEvent("evict", { detail: { records }, target: this });
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
**4. `createConstructor()` — callable without `new`**
|
|
279
|
-
|
|
280
|
-
```ts
|
|
281
|
-
const PersistentCacheEvent = createConstructor(StructEvent<PersistentCacheEventStruct, PersistentCache>);
|
|
282
|
-
const evt = PersistentCacheEvent("evict", { detail: { records }, target: this }); // no new
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
**Feature comparison**
|
|
288
|
-
|
|
289
|
-
| | Subclass | Manual cast | `typed()` | `createConstructor()` |
|
|
290
|
-
|---|:---:|:---:|:---:|:---:|
|
|
291
|
-
| Runtime overhead | new class | none | **none** | wrapper function |
|
|
292
|
-
| `new` required | yes | yes | yes | **no** |
|
|
293
|
-
| `instanceof` | **yes** | no | no | no |
|
|
294
|
-
| Can add methods | **yes** | no | no | no |
|
|
295
|
-
| Verbosity | medium | **high** | **low** | **low** |
|
|
296
|
-
| Requires TS | any | any | **4.7+** | **4.7+** |
|
|
297
|
-
|
|
298
|
-
**When to choose:**
|
|
299
|
-
|
|
300
|
-
- **Subclass** — when you need `instanceof` checks, want to add methods, or need a distinct runtime type.
|
|
301
|
-
- **Manual cast** — when TS < 4.7 is required, or you prefer zero dependencies (verbose but explicit).
|
|
302
|
-
- **`typed()`** — the default choice: concise, zero runtime cost. Requires TS 4.7+.
|
|
303
|
-
- **`createConstructor()`** — same as `typed()`, but the constructor must be callable without `new`
|
|
304
|
-
(e.g. factory patterns, functional-style code).
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
272
|
#### Object / Key Utilities
|
|
309
273
|
|
|
310
274
|
| Function | Description |
|
|
@@ -313,6 +277,7 @@ const evt = PersistentCacheEvent("evict", { detail: { records }, target: this })
|
|
|
313
277
|
| `keyOf<T>(key)` | Returns a property name literal narrowed to `keyof T`. No object required — useful for building typed key references |
|
|
314
278
|
| `nameOf<T>(f)` | Extracts a property name from a lambda `x => x.prop` at runtime via `Proxy` |
|
|
315
279
|
| `entry(obj, name, caseInsensitive?)` | Looks up a key (optionally case-insensitive) and returns `[resolvedKey, value]` |
|
|
280
|
+
| `getPrototypes(obj)` | Returns the prototype chain as an array, from the object's direct prototype up to (but not including) `null` |
|
|
316
281
|
|
|
317
282
|
```ts
|
|
318
283
|
keysOf({ a: 1, b: 2 }) // => ["a", "b"] typed as ("a" | "b")[]
|
|
@@ -370,6 +335,35 @@ combinePropertyPath(["server", "port"]) // => '["server"]["port"]'
|
|
|
370
335
|
| `toReadOnly<T>(obj, throwOnSet?)` | Deep read-only proxy; silently ignores writes (or throws if `throwOnSet: true`). Toggle with the `[$lock]` symbol |
|
|
371
336
|
| `createDeepProxy<T>(target, handler)` | Deep-change proxy: `handler.set` and `handler.deleteProperty` receive the full `DeepPropertyKey` path |
|
|
372
337
|
|
|
338
|
+
```ts
|
|
339
|
+
// proxify — lazy proxy that re-evaluates source on every get/set
|
|
340
|
+
let config = { theme: 'dark' };
|
|
341
|
+
const proxy = proxify(() => config);
|
|
342
|
+
proxy.theme; // => 'dark'
|
|
343
|
+
config = { theme: 'light' };
|
|
344
|
+
proxy.theme; // => 'light' — picks up the new object
|
|
345
|
+
|
|
346
|
+
// toReadOnly — deep read-only proxy (writes silently ignored by default)
|
|
347
|
+
const opts = toReadOnly({ server: { port: 3000 } });
|
|
348
|
+
opts.server.port; // => 3000
|
|
349
|
+
opts.server.port = 80; // silently ignored
|
|
350
|
+
// pass true as second arg to throw on write attempts instead
|
|
351
|
+
|
|
352
|
+
// createDeepProxy — intercept deep mutations with the full property path
|
|
353
|
+
const state = createDeepProxy({ user: { name: 'Alice' } }, {
|
|
354
|
+
set(target, path, value) {
|
|
355
|
+
console.log('set', path.map(String).join('.'), '=', value);
|
|
356
|
+
return true;
|
|
357
|
+
},
|
|
358
|
+
deleteProperty(target, path) {
|
|
359
|
+
console.log('deleted', path.map(String).join('.'));
|
|
360
|
+
return true;
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
state.user.name = 'Bob'; // logs: "set user.name = Bob"
|
|
364
|
+
delete state.user.name; // logs: "deleted user.name"
|
|
365
|
+
```
|
|
366
|
+
|
|
373
367
|
---
|
|
374
368
|
|
|
375
369
|
#### JSON Utilities
|
|
@@ -405,6 +399,309 @@ getEnumValue(Color, "Purple", Color.Red) // => 0 (default)
|
|
|
405
399
|
|
|
406
400
|
---
|
|
407
401
|
|
|
402
|
+
#### Comparison: 4 ways to bind a generic constructor
|
|
403
|
+
|
|
404
|
+
All four examples produce a bound alias for `StructEvent<PersistentCacheEventStruct, PersistentCache>`.
|
|
405
|
+
|
|
406
|
+
**1. Subclass**
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
class PersistentCacheEvent
|
|
410
|
+
extends StructEvent<PersistentCacheEventStruct, PersistentCache> {}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**2. Manual cast**
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
type PersistentCacheEvent = StructEvent<PersistentCacheEventStruct, PersistentCache>;
|
|
417
|
+
const PersistentCacheEvent = StructEvent as new (
|
|
418
|
+
...args: ConstructorParameters<typeof StructEvent<PersistentCacheEventStruct, PersistentCache>>
|
|
419
|
+
) => PersistentCacheEvent;
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**3. `typed()` + Instantiation Expression** *(recommended)*
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
const PersistentCacheEvent = typed(StructEvent<PersistentCacheEventStruct, PersistentCache>);
|
|
426
|
+
const evt = new PersistentCacheEvent("evict", { detail: { records }, target: this });
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**4. `createConstructor()` — callable without `new`**
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
const PersistentCacheEvent = createConstructor(StructEvent<PersistentCacheEventStruct, PersistentCache>);
|
|
433
|
+
const evt = PersistentCacheEvent("evict", { detail: { records }, target: this }); // no new
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
**Feature comparison**
|
|
439
|
+
|
|
440
|
+
| | Subclass | Manual cast | `typed()` | `createConstructor()` |
|
|
441
|
+
|---|:---:|:---:|:---:|:---:|
|
|
442
|
+
| Runtime overhead | new class | none | **none** | wrapper function |
|
|
443
|
+
| `new` required | yes | yes | yes | **no** |
|
|
444
|
+
| `instanceof` | **yes** | no | no | no |
|
|
445
|
+
| Can add methods | **yes** | no | no | no |
|
|
446
|
+
| Verbosity | medium | **high** | **low** | **low** |
|
|
447
|
+
| Requires TS | any | any | **4.7+** | **4.7+** |
|
|
448
|
+
|
|
449
|
+
**When to choose:**
|
|
450
|
+
|
|
451
|
+
- **Subclass** — when you need `instanceof` checks, want to add methods, or need a distinct runtime type.
|
|
452
|
+
- **Manual cast** — when TS < 4.7 is required, or you prefer zero dependencies (verbose but explicit).
|
|
453
|
+
- **`typed()`** — the default choice: concise, zero runtime cost. Requires TS 4.7+.
|
|
454
|
+
- **`createConstructor()`** — same as `typed()`, but the constructor must be callable without `new`
|
|
455
|
+
(e.g. factory patterns, functional-style code).
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
### stringCore — Locale-Aware String Utilities
|
|
460
|
+
|
|
461
|
+
**Import:** `@actdim/utico/stringCore`
|
|
462
|
+
|
|
463
|
+
Locale-aware string comparison and search utilities built on `Intl.Collator`. All functions accept an optional `locale` parameter (defaults to `navigator.language`). Non-string inputs fall back to reference equality or a default collator instead of throwing.
|
|
464
|
+
|
|
465
|
+
#### Functions
|
|
466
|
+
|
|
467
|
+
| Function | Description |
|
|
468
|
+
|----------|-------------|
|
|
469
|
+
| `equals(strA, strB, ignoreCase?, locale?)` | Returns `true` when strings are equal. Case-sensitive by default. Uses `Intl.Collator` for locale-correct comparison. |
|
|
470
|
+
| `compare(strA, strB, ignoreCase?, locale?)` | Returns a negative, zero, or positive number — same contract as `Array.prototype.sort`. |
|
|
471
|
+
| `ciCompare(strA, strB, locale?)` | Case-insensitive `compare`. Uses `sensitivity: "accent"` when available, falls back to `toLocaleUpperCase`. |
|
|
472
|
+
| `ciStartsWith(str, searchStr, locale?)` | Case-insensitive `String.prototype.startsWith`. Returns `false` for non-string inputs. |
|
|
473
|
+
| `ciEndsWith(str, searchStr, locale?)` | Case-insensitive `String.prototype.endsWith`. Returns `false` for non-string inputs. |
|
|
474
|
+
| `ciIndexOf(str, searchStr, locale?)` | Case-insensitive `String.prototype.indexOf`. Returns `-1` for non-string inputs or no match. |
|
|
475
|
+
| `ciIncludes(str, searchStr, locale?)` | Case-insensitive `String.prototype.includes`. Returns `false` for non-string inputs. |
|
|
476
|
+
|
|
477
|
+
#### Usage Examples
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { equals, compare, ciCompare, ciStartsWith, ciEndsWith, ciIndexOf, ciIncludes } from '@actdim/utico/stringCore';
|
|
481
|
+
|
|
482
|
+
// equals
|
|
483
|
+
equals('Hello', 'hello') // false (case-sensitive)
|
|
484
|
+
equals('Hello', 'hello', true) // true (case-insensitive)
|
|
485
|
+
equals('café', 'CAFÉ', true, 'fr') // true (locale-aware)
|
|
486
|
+
|
|
487
|
+
// compare — for sorting
|
|
488
|
+
['banana', 'Apple', 'cherry'].sort((a, b) => compare(a, b, true));
|
|
489
|
+
// => ['Apple', 'banana', 'cherry']
|
|
490
|
+
|
|
491
|
+
// ciStartsWith / ciEndsWith
|
|
492
|
+
ciStartsWith('Hello World', 'hello') // true
|
|
493
|
+
ciEndsWith('Hello World', 'WORLD') // true
|
|
494
|
+
|
|
495
|
+
// ciIndexOf / ciIncludes
|
|
496
|
+
ciIndexOf('Hello World', 'WORLD') // 6
|
|
497
|
+
ciIncludes('Hello World', 'WORLD') // true
|
|
498
|
+
ciIncludes('Hello World', 'xyz') // false
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
### metadata — Property Metadata
|
|
504
|
+
|
|
505
|
+
**Import:** `@actdim/utico/metadata`
|
|
506
|
+
|
|
507
|
+
A lightweight property metadata system backed by `WeakMap`. Attach arbitrary named slots of metadata to class properties via the `@metadata` decorator or the imperative API. Metadata is resolved through the prototype chain, so subclasses automatically inherit base-class metadata.
|
|
508
|
+
|
|
509
|
+
#### Functions
|
|
510
|
+
|
|
511
|
+
| Function | Description |
|
|
512
|
+
|----------|-------------|
|
|
513
|
+
| `metadata(value, slotName)` | Property decorator factory. Attaches `value` to the `slotName` slot of the decorated property. |
|
|
514
|
+
| `getPropertyMetadata<T>(target, propertyName, slotName?)` | Reads metadata for a property. If `slotName` is omitted, returns all slots for that property. Walks the prototype chain. |
|
|
515
|
+
| `updatePropertyMetadata(target, propertyName, value, slotName)` | Imperative equivalent of `@metadata`. |
|
|
516
|
+
| `getPropertyMetadataItem(metadata, obj)` | Low-level: resolves a `WeakMap` entry for `obj` by walking its prototype chain. |
|
|
517
|
+
|
|
518
|
+
#### Usage Examples
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import { metadata, getPropertyMetadata, updatePropertyMetadata } from '@actdim/utico/metadata';
|
|
522
|
+
|
|
523
|
+
// --- Decorator API ---
|
|
524
|
+
|
|
525
|
+
class Article {
|
|
526
|
+
@metadata('Title of the article', 'label')
|
|
527
|
+
@metadata(true, 'required')
|
|
528
|
+
title: string;
|
|
529
|
+
|
|
530
|
+
@metadata('Publication date', 'label')
|
|
531
|
+
publishedAt: number;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const article = new Article();
|
|
535
|
+
|
|
536
|
+
getPropertyMetadata(article, 'title', 'label') // => 'Title of the article'
|
|
537
|
+
getPropertyMetadata(article, 'title', 'required') // => true
|
|
538
|
+
getPropertyMetadata(article, 'title') // => { label: '...', required: true }
|
|
539
|
+
|
|
540
|
+
// --- Imperative API ---
|
|
541
|
+
|
|
542
|
+
class Product {
|
|
543
|
+
price: number;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
updatePropertyMetadata(Product.prototype, 'price', 'EUR price in cents', 'label');
|
|
547
|
+
getPropertyMetadata(new Product(), 'price', 'label'); // => 'EUR price in cents'
|
|
548
|
+
|
|
549
|
+
// --- Prototype chain inheritance ---
|
|
550
|
+
|
|
551
|
+
class SpecialArticle extends Article {}
|
|
552
|
+
|
|
553
|
+
// SpecialArticle inherits metadata from Article.prototype
|
|
554
|
+
getPropertyMetadata(new SpecialArticle(), 'title', 'label'); // => 'Title of the article'
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
### decorators — Property Decorators
|
|
560
|
+
|
|
561
|
+
**Import:** `@actdim/utico/decorators`
|
|
562
|
+
|
|
563
|
+
| Decorator | Description |
|
|
564
|
+
|-----------|-------------|
|
|
565
|
+
| `@nonEnumerable` | Makes a class property non-enumerable: it is hidden from `Object.keys`, `for...in`, `JSON.stringify`, and spread (`{...obj}`), while remaining fully readable and writable via direct access. |
|
|
566
|
+
|
|
567
|
+
#### Usage Examples
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
import { nonEnumerable } from '@actdim/utico/decorators';
|
|
571
|
+
|
|
572
|
+
class User {
|
|
573
|
+
name: string;
|
|
574
|
+
|
|
575
|
+
@nonEnumerable
|
|
576
|
+
passwordHash: string;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const user = new User();
|
|
580
|
+
user.name = 'Alice';
|
|
581
|
+
user.passwordHash = 'abc123';
|
|
582
|
+
|
|
583
|
+
Object.keys(user) // => ['name'] — passwordHash is hidden
|
|
584
|
+
JSON.stringify(user) // => '{"name":"Alice"}'
|
|
585
|
+
user.passwordHash // => 'abc123' — still directly accessible
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
> **How it works:** the decorator replaces the property with an accessor on the prototype. On the first assignment, the accessor redefines the property as a non-enumerable own value on the specific instance, so subsequent reads are direct (no getter overhead).
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
### dateTimeDataFormat — Date/Time Serialisation
|
|
593
|
+
|
|
594
|
+
**Import:** `@actdim/utico/dateTimeDataFormat`
|
|
595
|
+
|
|
596
|
+
**Peer dependency:** `luxon ^3`
|
|
597
|
+
|
|
598
|
+
Date/time conversion utilities built on [Luxon](https://moment.github.io/luxon/).
|
|
599
|
+
The module converts values from string/number/`Date`/`DateTime`, tracks precision, and exports values either as local ISO (without suffix) or UTC ISO (`Z` suffix).
|
|
600
|
+
|
|
601
|
+
#### Core Types
|
|
602
|
+
|
|
603
|
+
| Type | Description |
|
|
604
|
+
|------|-------------|
|
|
605
|
+
| `DateTimeNumberFormat` | Numeric input/output format: Unix ms, Unix seconds, OADate |
|
|
606
|
+
| `DateTimePrecision` | `auto \| date \| minute \| second \| millisecond` |
|
|
607
|
+
| `DateTimeKind` | `local \| utc` |
|
|
608
|
+
| `DateTimeStringInterpretation` | `auto \| local \| utc` for parsing strings |
|
|
609
|
+
| `DateTimeExportInterpretation` | `original \| local \| utc \| match` for `exportToString` |
|
|
610
|
+
| `DateTimeExtended` | Luxon `DateTime` with extra fields: `precision`, `exportToString(...)` |
|
|
611
|
+
| `ToDateTimeOptions` | Options for `toDateTime(...)` |
|
|
612
|
+
| `DateTimeTransport` | `{ serialize(...), deserialize(...) }` pair for wire transport |
|
|
613
|
+
|
|
614
|
+
#### `DateTimeNumberFormat`
|
|
615
|
+
|
|
616
|
+
| Member | Description |
|
|
617
|
+
|--------|-------------|
|
|
618
|
+
| `UnixTimeMilliseconds` | Default — milliseconds since Unix epoch |
|
|
619
|
+
| `UnixTimeSeconds` | Seconds since Unix epoch |
|
|
620
|
+
| `OADate` | Microsoft OLE Automation date (fractional days since 1899-12-30) |
|
|
621
|
+
|
|
622
|
+
#### Functions
|
|
623
|
+
|
|
624
|
+
| Function | Description |
|
|
625
|
+
|----------|-------------|
|
|
626
|
+
| `toDateTime(source, options?)` | Converts `string \| number \| Date \| DateTime` to `DateTimeExtended` |
|
|
627
|
+
| `getDateTimeFromString(value, format?, precision?, interpretAs?)` | Parses string using explicit format or ISO |
|
|
628
|
+
| `getDateTimeFromNumber(value, numberFormat?, interpretAs?, precision?)` | Parses number into `DateTimeExtended` |
|
|
629
|
+
| `getDateTimeNumber(dt, numberFormat?)` | Converts `DateTime`/`DateTimeExtended` to number |
|
|
630
|
+
| `isDateTimeExtended(obj)` | Type guard for `DateTimeExtended` |
|
|
631
|
+
|
|
632
|
+
#### Ready-to-use transports
|
|
633
|
+
|
|
634
|
+
| Transport | Serialize | Deserialize |
|
|
635
|
+
|-----------|-----------|-------------|
|
|
636
|
+
| `invariantLocalDateTimeTransport` | local ISO without zone suffix | strings as auto, numbers as local Unix seconds |
|
|
637
|
+
| `utcDateTimeTransport` | UTC ISO with `Z` suffix | strings as auto, numbers as UTC Unix seconds |
|
|
638
|
+
|
|
639
|
+
#### Usage Examples
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
import {
|
|
643
|
+
toDateTime,
|
|
644
|
+
getDateTimeFromString,
|
|
645
|
+
getDateTimeFromNumber,
|
|
646
|
+
getDateTimeNumber,
|
|
647
|
+
DateTimeKind,
|
|
648
|
+
DateTimePrecision,
|
|
649
|
+
DateTimeNumberFormat,
|
|
650
|
+
DateTimeStringInterpretation,
|
|
651
|
+
DateTimeExportInterpretation,
|
|
652
|
+
invariantLocalDateTimeTransport,
|
|
653
|
+
utcDateTimeTransport
|
|
654
|
+
} from '@actdim/utico/dateTimeDataFormat';
|
|
655
|
+
|
|
656
|
+
// Parse from string (ISO auto-detect)
|
|
657
|
+
const a = toDateTime("2024-03-15T10:30:45.123");
|
|
658
|
+
|
|
659
|
+
// Parse from custom string format
|
|
660
|
+
const b = toDateTime("03/15/2024", { stringFormat: "MM/dd/yyyy" });
|
|
661
|
+
|
|
662
|
+
// Parse from number
|
|
663
|
+
const c = getDateTimeFromNumber(1710496245000, DateTimeNumberFormat.UnixTimeMilliseconds);
|
|
664
|
+
|
|
665
|
+
// Parse string and force interpretation as UTC
|
|
666
|
+
const d = getDateTimeFromString(
|
|
667
|
+
"2024-03-15T10:30:45.123",
|
|
668
|
+
undefined,
|
|
669
|
+
DateTimePrecision.Auto,
|
|
670
|
+
DateTimeStringInterpretation.Utc
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
// Export as ISO local (without zone suffix)
|
|
674
|
+
a.exportToString(DateTimeKind.Local); // "2024-03-15T..."
|
|
675
|
+
|
|
676
|
+
// Export as ISO UTC (with Z)
|
|
677
|
+
a.exportToString(DateTimeKind.Utc); // "2024-03-15T...Z"
|
|
678
|
+
|
|
679
|
+
// Export with explicit interpretation
|
|
680
|
+
a.exportToString(
|
|
681
|
+
DateTimeKind.Local,
|
|
682
|
+
DateTimeExportInterpretation.Local
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
// Precision truncation
|
|
686
|
+
const minute = getDateTimeFromString(
|
|
687
|
+
"2024-03-15T10:30:45.123",
|
|
688
|
+
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
|
689
|
+
DateTimePrecision.Minute
|
|
690
|
+
);
|
|
691
|
+
minute.second; // 0
|
|
692
|
+
minute.millisecond; // 0
|
|
693
|
+
|
|
694
|
+
// Number conversion back
|
|
695
|
+
getDateTimeNumber(a, DateTimeNumberFormat.UnixTimeSeconds);
|
|
696
|
+
|
|
697
|
+
// Transport helpers
|
|
698
|
+
invariantLocalDateTimeTransport.serialize(a); // local string
|
|
699
|
+
utcDateTimeTransport.serialize(a); // UTC string with Z
|
|
700
|
+
utcDateTimeTransport.serialize(null); // null
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
408
705
|
### StructEvent — Typed DOM Events
|
|
409
706
|
|
|
410
707
|
**Import:** `@actdim/utico/structEvent`
|
|
@@ -502,8 +799,8 @@ const PersistentCacheEvent = typed(StructEvent<PersistentCacheEventStruct, Persi
|
|
|
502
799
|
const cache = await PersistentCache.open("my-cache");
|
|
503
800
|
|
|
504
801
|
cache.addEventListener("evict", (e) => {
|
|
505
|
-
// e.detail
|
|
506
|
-
// e.target
|
|
802
|
+
// e.detail -> { records: CacheMetadataRecord[] } (typed)
|
|
803
|
+
// e.target -> PersistentCache (typed)
|
|
507
804
|
console.log("Evicted records:", e.detail.records);
|
|
508
805
|
});
|
|
509
806
|
```
|
|
@@ -525,9 +822,17 @@ Track the execution state of promises and functions — useful for loading indic
|
|
|
525
822
|
| Type | Description |
|
|
526
823
|
| ------------------------- | ---------------------------------------------------------------- |
|
|
527
824
|
| `PromiseStatus` | `"pending" \| "fulfilled" \| "rejected"` |
|
|
528
|
-
| `WatchablePromise<T>` | `PromiseLike<T>`
|
|
825
|
+
| `WatchablePromise<T>` | `PromiseLike<T>` with observable state fields (see below) |
|
|
529
826
|
| `WatchableFunc<TArgs, T>` | Function extended with an `executing` flag |
|
|
530
827
|
|
|
828
|
+
`WatchablePromise<T>` adds three read-only fields to the underlying promise:
|
|
829
|
+
|
|
830
|
+
| Field | Type | Description |
|
|
831
|
+
| ---------- | --------------- | --------------------------------------------------------------------------- |
|
|
832
|
+
| `status` | `PromiseStatus` | `"pending"` immediately; becomes `"fulfilled"` or `"rejected"` when settled |
|
|
833
|
+
| `settled` | `boolean` | Computed getter — `true` once `status` is no longer `"pending"` |
|
|
834
|
+
| `result` | `T \| undefined`| The resolved value after fulfillment; `undefined` after rejection |
|
|
835
|
+
|
|
531
836
|
#### Functions
|
|
532
837
|
|
|
533
838
|
| Function | Signature | Description |
|
|
@@ -714,6 +1019,8 @@ The main entry point for structured persistence. Key features:
|
|
|
714
1019
|
| `getOrSet<TValue>(metadata, factory)` | Get existing or create via factory |
|
|
715
1020
|
| `bulkGet<TValue>(keys)` | Get multiple items |
|
|
716
1021
|
| `bulkSet<TValue>(metadataRecords, dataRecords)` | Insert multiple items |
|
|
1022
|
+
| `update<TValue>(key, metadataChanges, valueChanges?)` | Patch fields of a single item by key; returns count of records modified (0 if key not found) |
|
|
1023
|
+
| `bulkUpdate(metadataChangeSets, dataChangeSets?)` | Patch fields of multiple items; each change set is `{ key, changes: KeyPathValueMap<T> }` |
|
|
717
1024
|
| `delete(key)` | Delete an item |
|
|
718
1025
|
| `bulkDelete(keys)` | Delete multiple items |
|
|
719
1026
|
| `clear()` | Delete all items |
|
|
@@ -788,6 +1095,15 @@ const items = await store.bulkGet<{ x: number }>(['item:1', 'item:2']);
|
|
|
788
1095
|
await store.delete('user:42');
|
|
789
1096
|
await store.bulkDelete(['item:1', 'item:2']);
|
|
790
1097
|
await store.clear();
|
|
1098
|
+
|
|
1099
|
+
// Patch individual fields without rewriting the whole record
|
|
1100
|
+
await store.update('user:42', { tags: ['admin', 'verified'] });
|
|
1101
|
+
|
|
1102
|
+
// Patch multiple records in one transaction
|
|
1103
|
+
await store.bulkUpdate([
|
|
1104
|
+
{ key: 'item:1', changes: { tags: ['sale'] } },
|
|
1105
|
+
{ key: 'item:2', changes: { tags: ['new'] } },
|
|
1106
|
+
]);
|
|
791
1107
|
```
|
|
792
1108
|
|
|
793
1109
|
```typescript
|
|
@@ -833,6 +1149,129 @@ const page = await store
|
|
|
833
1149
|
|
|
834
1150
|
---
|
|
835
1151
|
|
|
1152
|
+
### cache — Persistent Cache
|
|
1153
|
+
|
|
1154
|
+
**Imports:**
|
|
1155
|
+
|
|
1156
|
+
- `@actdim/utico/cache/persistentCache` — `PersistentCache`, `CacheOptions`, `PersistentCacheOptions`
|
|
1157
|
+
- `@actdim/utico/cache/cacheContracts` — `CacheMetadataRecord`
|
|
1158
|
+
|
|
1159
|
+
Built on top of the `store` module. Adds expiration semantics (TTL, absolute expiration, sliding expiration) and a background cleanup job that evicts expired entries automatically.
|
|
1160
|
+
|
|
1161
|
+
#### Types
|
|
1162
|
+
|
|
1163
|
+
| Type | Description |
|
|
1164
|
+
|------|-------------|
|
|
1165
|
+
| `CacheMetadataRecord` | Extends `MetadataRecord` with `slidingExpiration`, `absoluteExpiration`, and `expiresAt` |
|
|
1166
|
+
| `CacheOptions` | Per-entry expiration options (see below) |
|
|
1167
|
+
| `PersistentCacheOptions` | Cache-level options: `cleanupTimeout` (ms between background cleanup runs) |
|
|
1168
|
+
| `CacheEvictionEvent` | `{ records: CacheMetadataRecord[] }` — payload of the `"evict"` event |
|
|
1169
|
+
|
|
1170
|
+
#### `CacheOptions`
|
|
1171
|
+
|
|
1172
|
+
| Field | Type | Description |
|
|
1173
|
+
|-------|------|-------------|
|
|
1174
|
+
| `slidingExpiration` | `number` (ms) | Extends `expiresAt` by this duration on every `get()`. Recommended: combined with `absoluteExpiration` as a cap. |
|
|
1175
|
+
| `absoluteExpiration` | `Date \| number` | Hard deadline — `expiresAt` is never pushed past this value. |
|
|
1176
|
+
| `ttl` | `number \| { seconds?, minutes?, hours? }` | Sets `absoluteExpiration` relative to the creation time. |
|
|
1177
|
+
|
|
1178
|
+
#### Static Methods
|
|
1179
|
+
|
|
1180
|
+
| Method | Description |
|
|
1181
|
+
|--------|-------------|
|
|
1182
|
+
| `PersistentCache.open(name, options?)` | Open or create a named cache. Returns a `PersistentCache` instance. |
|
|
1183
|
+
| `PersistentCache.exists(name)` | Check if a named cache database exists. |
|
|
1184
|
+
| `PersistentCache.delete(name)` | Delete a named cache database. |
|
|
1185
|
+
|
|
1186
|
+
#### Instance Methods
|
|
1187
|
+
|
|
1188
|
+
| Method | Description |
|
|
1189
|
+
|--------|-------------|
|
|
1190
|
+
| `get<TValue>(key)` | Get an item and update `accessedAt` (and `expiresAt` if `slidingExpiration` is set). |
|
|
1191
|
+
| `set(metadata, value, options)` | Create or overwrite an item with expiration options. Auto-generates a UUID key if `metadata.key` is absent. |
|
|
1192
|
+
| `getOrSet(metadata, factory, options)` | Return the existing item or create it via `factory`. `metadata.key` is required. |
|
|
1193
|
+
| `bulkGet<TValue>(keys)` | Get multiple items by key. |
|
|
1194
|
+
| `bulkSet(metadataRecords, dataRecords, optionsProvider)` | Insert multiple items. `optionsProvider` is called per-record to produce `CacheOptions`. |
|
|
1195
|
+
| `contains(key)` | Check if a key exists. |
|
|
1196
|
+
| `getKeys()` | Return all stored keys. |
|
|
1197
|
+
| `delete(key)` | Delete a single item. |
|
|
1198
|
+
| `bulkDelete(keys)` | Delete multiple items. |
|
|
1199
|
+
| `clear()` | Delete all items. |
|
|
1200
|
+
| `deleteExpired(ts?)` | Evict entries whose `expiresAt < ts` (defaults to `Date.now()`). Fires the `"evict"` event if anything was removed. |
|
|
1201
|
+
| `[Symbol.dispose]()` | Cancel the background cleanup timer and close the database. |
|
|
1202
|
+
|
|
1203
|
+
#### Events
|
|
1204
|
+
|
|
1205
|
+
`PersistentCache` extends `StructEventTarget`. Subscribe with `addEventListener`.
|
|
1206
|
+
|
|
1207
|
+
| Event | Detail type | Description |
|
|
1208
|
+
|-------|-------------|-------------|
|
|
1209
|
+
| `"evict"` | `{ records: CacheMetadataRecord[] }` | Fired after `deleteExpired` removes at least one entry. |
|
|
1210
|
+
|
|
1211
|
+
#### Usage Examples
|
|
1212
|
+
|
|
1213
|
+
```typescript
|
|
1214
|
+
import { PersistentCache } from '@actdim/utico/cache/persistentCache';
|
|
1215
|
+
|
|
1216
|
+
// --- Open a cache ---
|
|
1217
|
+
|
|
1218
|
+
const cache = await PersistentCache.open('my-cache');
|
|
1219
|
+
|
|
1220
|
+
// --- set / get ---
|
|
1221
|
+
|
|
1222
|
+
await cache.set({ key: 'user:42' }, { name: 'Alice' }, {});
|
|
1223
|
+
|
|
1224
|
+
const item = await cache.get<{ name: string }>('user:42');
|
|
1225
|
+
item.metadata.key // 'user:42'
|
|
1226
|
+
item.metadata.createdAt // timestamp set automatically
|
|
1227
|
+
item.data.value.name // 'Alice'
|
|
1228
|
+
|
|
1229
|
+
// --- Auto-generated key ---
|
|
1230
|
+
|
|
1231
|
+
const meta = {}; // no key provided
|
|
1232
|
+
await cache.set(meta, { x: 1 }, {});
|
|
1233
|
+
meta.key; // UUID filled in by set()
|
|
1234
|
+
|
|
1235
|
+
// --- getOrSet ---
|
|
1236
|
+
|
|
1237
|
+
const item2 = await cache.getOrSet(
|
|
1238
|
+
{ key: 'session:abc' },
|
|
1239
|
+
() => ({ token: crypto.randomUUID() }),
|
|
1240
|
+
{}
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
// --- Sliding expiration (renewed on every get) ---
|
|
1244
|
+
|
|
1245
|
+
await cache.set({ key: 'live' }, 'data', { slidingExpiration: 30_000 });
|
|
1246
|
+
// expiresAt = now + 30 s; reset to now + 30 s on each get()
|
|
1247
|
+
|
|
1248
|
+
// --- Sliding expiration with absolute cap ---
|
|
1249
|
+
|
|
1250
|
+
await cache.set({ key: 'bounded' }, 'data', {
|
|
1251
|
+
slidingExpiration: 5 * 60_000, // renew up to 5 min on each get
|
|
1252
|
+
absoluteExpiration: Date.now() + 3_600_000, // but never past 1 hour from now
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
// --- Manual eviction ---
|
|
1256
|
+
|
|
1257
|
+
await cache.deleteExpired(); // uses Date.now()
|
|
1258
|
+
await cache.deleteExpired(Date.now() + 60_000); // treat everything expiring in the next minute as expired
|
|
1259
|
+
|
|
1260
|
+
// --- Eviction event ---
|
|
1261
|
+
|
|
1262
|
+
cache.addEventListener('evict', (e) => {
|
|
1263
|
+
console.log('Evicted:', e.detail.records.map(r => r.key));
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// --- Cleanup ---
|
|
1267
|
+
|
|
1268
|
+
cache[Symbol.dispose](); // stops background timer, closes DB
|
|
1269
|
+
// or:
|
|
1270
|
+
using c = await PersistentCache.open('temp'); // auto-disposed at block exit (TS 5.2+)
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
---
|
|
1274
|
+
|
|
836
1275
|
## License
|
|
837
1276
|
|
|
838
1277
|
Proprietary — © Pavel Borodaev
|