@owlmeans/static-resource 0.1.18-rc.2 → 0.1.18-rc.20
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 +7 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/static-resource/SKILL.md +46 -11
- package/build/resource.d.ts +1 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +74 -101
- package/build/resource.js.map +1 -1
- package/package.json +4 -4
- package/src/resource.ts +91 -112
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ In-memory `Resource<T>` backed by static config data — for read-only data load
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add @owlmeans/static-resource
|
|
15
|
+
bun add @owlmeans/static-resource@^0.1.18-rc.19
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
@@ -26,8 +26,13 @@ appendStaticResource<C, T>(context, 'products')
|
|
|
26
26
|
// Access from context
|
|
27
27
|
const resource = context.getStaticResource<Product>('products')
|
|
28
28
|
const product = await resource.load('product-sku')
|
|
29
|
+
const active = await resource.list({ status: 'active' })
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
Reads are unpaged: `list(where)` returns every match, and `list(where, { page })` without a `size`
|
|
33
|
+
throws `UnsupportedArgumentError('page-without-size')`. Criteria and sorting are the shared
|
|
34
|
+
vocabulary from [`@owlmeans/resource`](../resource), evaluated in memory.
|
|
35
|
+
|
|
31
36
|
## API
|
|
32
37
|
|
|
33
38
|
### `createStaticResource(alias?, key?): Resource<ResourceRecord>`
|
|
@@ -58,7 +63,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
58
63
|
your project's skill store (`.agents/skills/`):
|
|
59
64
|
|
|
60
65
|
```sh
|
|
61
|
-
npx @owlmeans/agent-skills
|
|
66
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.23
|
|
62
67
|
```
|
|
63
68
|
|
|
64
69
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/static-resource",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.20",
|
|
5
|
+
"generatedAt": "2026-09-15T12:50:32.658Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: static-resource
|
|
3
|
-
description: How to use @owlmeans/static-resource — Resource for
|
|
3
|
+
description: How to use @owlmeans/static-resource — in-process Resource over a module-scope Map, for records an app holds in memory (fixtures, caches, short-lived handshake state). Auto-invoked when registering an in-memory resource on a context.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,27 +8,62 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/static-resource
|
|
9
9
|
|
|
10
10
|
**Layer:** Infra
|
|
11
|
-
**Install:** `"@owlmeans/static-resource": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/static-resource": "^0.1.18-rc.20"` in `dependencies`
|
|
12
|
+
|
|
13
|
+
The whole `Resource` contract over a `Map` held at module scope. Use it where records must be
|
|
14
|
+
reachable through the context like any other resource but have no database behind them — fixtures,
|
|
15
|
+
a process-local cache, short-lived handshake state.
|
|
12
16
|
|
|
13
17
|
## Key Exports
|
|
14
18
|
|
|
15
19
|
| Export | Description |
|
|
16
20
|
|--------|-------------|
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
|
|
|
21
|
+
| `appendStaticResource(context, alias?, key?)` | Register one on the context and add `getStaticResource(alias?)` to it |
|
|
22
|
+
| `createStaticResource<T>(alias?, key?)` | The bare `Resource<T>`, when you register it yourself |
|
|
23
|
+
| `StaticResourceAppend` | The `getStaticResource` accessor the append adds |
|
|
24
|
+
| `DEFAULT_ALIAS` (`static`) | Constant |
|
|
25
|
+
|
|
26
|
+
`key` names the underlying store, defaulting to the alias — two resources sharing a `key` share
|
|
27
|
+
records, which is how two contexts in one process see the same set.
|
|
20
28
|
|
|
21
29
|
## Usage
|
|
22
30
|
|
|
23
31
|
```typescript
|
|
24
|
-
import {
|
|
32
|
+
import { appendStaticResource } from '@owlmeans/static-resource'
|
|
33
|
+
|
|
34
|
+
appendStaticResource(context, 'fixtures')
|
|
25
35
|
|
|
26
|
-
context.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}))
|
|
36
|
+
const fixtures = context.getStaticResource<Fixture>('fixtures')
|
|
37
|
+
await fixtures.save({ id: 'seed', name: 'Owl' })
|
|
38
|
+
await fixtures.list({ name: { $startsWith: 'Owl' } }, { sort: ['name'] })
|
|
30
39
|
```
|
|
31
40
|
|
|
41
|
+
## Semantics
|
|
42
|
+
|
|
43
|
+
- The store is a map keyed by id and there is nothing here to mint one from, so **every write
|
|
44
|
+
needs an id**: `create`, `update` and `save` all throw `MisshapedRecord('id')` without one.
|
|
45
|
+
`create` additionally throws `RecordExists`, `update` `UnknownRecordError`; `save` is the upsert.
|
|
46
|
+
- `load(id)` / `get(id)` / `delete(id)` / `take(id)` hit the map directly. `take(id)` **deletes**
|
|
47
|
+
the record it returns and throws `UnknownRecordError` on a miss.
|
|
48
|
+
- `load(where)`, `get(where)`, `list`, `count` and `purge` go through the shared in-memory engine
|
|
49
|
+
from `@owlmeans/resource`, so criteria and `{ sort }` mean exactly what they mean against a
|
|
50
|
+
database. `purge(where)` refuses an empty criteria object
|
|
51
|
+
(`UnsupportedArgumentError('purge:empty-criteria')` — the database backends spell the same
|
|
52
|
+
refusal `purge:no-criteria`, so match on the error type rather than the message).
|
|
53
|
+
- **Unpaged**: `list(where)` with no `size` returns every match — the whole store is already in
|
|
54
|
+
memory; `size: 0` says the same explicitly, and `page` without `size` throws
|
|
55
|
+
`UnsupportedArgumentError('page-without-size')`.
|
|
56
|
+
- `opts.ttl` on a write arms a timer that deletes the id when it elapses — a number is seconds from
|
|
57
|
+
now, a `Date` the instant to expire at.
|
|
58
|
+
- **An armed expiry cannot be postponed.** Nothing ever clears a timer, so a later write — with a
|
|
59
|
+
ttl or without one — leaves the original running, and when it fires it deletes whatever record now
|
|
60
|
+
sits under that id. Passing the ttl again renews nothing; it only adds a second timer behind the
|
|
61
|
+
first. A record that has to outlive its first ttl needs a fresh id.
|
|
62
|
+
- **The map holds the caller's own object.** `create`/`update`/`save` store the object handed in and
|
|
63
|
+
`load`/`get`/`list` hand it straight back — no clone, no serialization. Mutating a record you
|
|
64
|
+
saved or read rewrites the store in place, bypassing `update` entirely, which is not how the
|
|
65
|
+
database backends behave. Copy before mutating, or treat what comes out of here as frozen.
|
|
66
|
+
|
|
32
67
|
## Depends On
|
|
33
68
|
|
|
34
|
-
- `@owlmeans/resource`, `@owlmeans/
|
|
69
|
+
- `@owlmeans/resource`, `@owlmeans/context`
|
package/build/resource.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Resource, ResourceRecord } from '@owlmeans/resource';
|
|
2
2
|
import type { Config, Context, StaticResourceAppend } from './types.js';
|
|
3
|
-
export declare const createStaticResource: (alias?: string, key?: string) => Resource<
|
|
3
|
+
export declare const createStaticResource: <T extends ResourceRecord = ResourceRecord>(alias?: string, key?: string) => Resource<T>;
|
|
4
4
|
export declare const appendStaticResource: <C extends Config, T extends Context<C>>(ctx: T, alias?: string, key?: string) => T & StaticResourceAppend;
|
|
5
5
|
//# sourceMappingURL=resource.d.ts.map
|
package/build/resource.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAC2B,QAAQ,EAAE,cAAc,EAC9D,MAAM,oBAAoB,CAAA;AAK3B,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAYvE,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,cAAc,GAAG,cAAc,UACrE,MAAM,QAAwB,MAAM,KAC1C,QAAQ,CAAC,CAAC,CA4HZ,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OACpE,CAAC,UAAS,MAAM,QAAwB,MAAM,KAClD,CAAC,GAAG,oBAWN,CAAA"}
|
package/build/resource.js
CHANGED
|
@@ -1,134 +1,107 @@
|
|
|
1
1
|
import { appendContextual } from '@owlmeans/context';
|
|
2
2
|
import { DEFAULT_ALIAS } from './consts.js';
|
|
3
|
-
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource';
|
|
3
|
+
import { applyQuery, filterRecords, firstMatch, MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource';
|
|
4
4
|
const stores = {};
|
|
5
|
+
/**
|
|
6
|
+
* Milliseconds until the record expires, which is what `setTimeout` wants. A number is seconds
|
|
7
|
+
* from now and a Date is the instant to expire at — the two spellings have to land on the same
|
|
8
|
+
* moment, or the same ttl means an hour through one and a second through the other.
|
|
9
|
+
*/
|
|
10
|
+
const expiresIn = (ttl) => ttl instanceof Date ? ttl.getTime() - Date.now() : ttl * 1000;
|
|
5
11
|
export const createStaticResource = (alias = DEFAULT_ALIAS, key) => {
|
|
6
|
-
|
|
12
|
+
const storeKey = key ?? alias;
|
|
7
13
|
const getStore = () => {
|
|
8
|
-
if (stores[
|
|
9
|
-
stores[
|
|
14
|
+
if (stores[storeKey] == null) {
|
|
15
|
+
stores[storeKey] = new Map();
|
|
10
16
|
}
|
|
11
|
-
return stores[
|
|
17
|
+
return stores[storeKey];
|
|
18
|
+
};
|
|
19
|
+
const records = () => [...getStore().values()];
|
|
20
|
+
const expire = (id, opts) => {
|
|
21
|
+
if (opts?.ttl == null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const store = getStore();
|
|
25
|
+
setTimeout(() => void store.delete(id), expiresIn(opts.ttl));
|
|
26
|
+
};
|
|
27
|
+
/** An id hits the map directly; criteria go through the shared in-memory engine. */
|
|
28
|
+
const first = (idOrWhere, opts) => typeof idOrWhere === 'string'
|
|
29
|
+
? getStore().get(idOrWhere) ?? null
|
|
30
|
+
: firstMatch(records(), idOrWhere, opts);
|
|
31
|
+
const write = (record, opts) => {
|
|
32
|
+
if (record.id == null) {
|
|
33
|
+
/**
|
|
34
|
+
* The store is a map keyed by id and there is nothing here to mint one from, so a record
|
|
35
|
+
* without an id is misshaped rather than new — `save` cannot create it.
|
|
36
|
+
*/
|
|
37
|
+
throw new MisshapedRecord('id');
|
|
38
|
+
}
|
|
39
|
+
getStore().set(record.id, record);
|
|
40
|
+
expire(record.id, opts);
|
|
41
|
+
return record;
|
|
12
42
|
};
|
|
13
43
|
const resource = appendContextual(alias, {
|
|
14
|
-
get: async (
|
|
15
|
-
const record =
|
|
44
|
+
get: async (idOrWhere, opts) => {
|
|
45
|
+
const record = first(idOrWhere, opts);
|
|
16
46
|
if (record == null) {
|
|
17
|
-
throw new UnknownRecordError(
|
|
47
|
+
throw new UnknownRecordError(typeof idOrWhere === 'string' ? idOrWhere : 'criteria');
|
|
18
48
|
}
|
|
19
49
|
return record;
|
|
20
50
|
},
|
|
21
|
-
load: async (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
load: async (idOrWhere, opts) => first(idOrWhere, opts),
|
|
52
|
+
/** Unpaged unless a size is asked for — the whole store is already in memory. */
|
|
53
|
+
list: async (where, opts) => {
|
|
54
|
+
if (opts?.page != null && opts.size == null) {
|
|
55
|
+
throw new UnsupportedArgumentError('page-without-size');
|
|
25
56
|
}
|
|
26
|
-
|
|
27
|
-
const record = field === 'id'
|
|
28
|
-
? store.get(id)
|
|
29
|
-
: [...store.values()].find(record => record[field] === id);
|
|
30
|
-
if (record == null) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
return record;
|
|
34
|
-
},
|
|
35
|
-
list: async (criteria, opts) => {
|
|
36
|
-
if (criteria != null) {
|
|
37
|
-
throw new UnsupportedArgumentError('static:list:criteria');
|
|
38
|
-
}
|
|
39
|
-
if (opts != null) {
|
|
40
|
-
throw new UnsupportedArgumentError('static:list:opts');
|
|
41
|
-
}
|
|
42
|
-
const result = {
|
|
43
|
-
items: [...getStore().values()]
|
|
44
|
-
};
|
|
45
|
-
return result;
|
|
46
|
-
},
|
|
47
|
-
save: async (record, opts) => {
|
|
48
|
-
const store = getStore();
|
|
49
|
-
if (record.id == null) {
|
|
50
|
-
throw new MisshapedRecord('id');
|
|
51
|
-
}
|
|
52
|
-
store.set(record.id, record);
|
|
53
|
-
if (typeof opts === 'object' && opts?.ttl != null) {
|
|
54
|
-
const ttl = opts.ttl instanceof Date
|
|
55
|
-
? (opts.ttl.getTime() - Date.now()) / 1000
|
|
56
|
-
: typeof opts.ttl === 'number'
|
|
57
|
-
? opts.ttl * 1000
|
|
58
|
-
: null;
|
|
59
|
-
if (ttl == null) {
|
|
60
|
-
throw new UnsupportedArgumentError('static:opts:ttl-string');
|
|
61
|
-
}
|
|
62
|
-
setTimeout(() => record.id != null && void store.delete(record.id), ttl);
|
|
63
|
-
}
|
|
64
|
-
return record;
|
|
57
|
+
return applyQuery(records(), where, opts);
|
|
65
58
|
},
|
|
59
|
+
count: async (where) => filterRecords(records(), where).length,
|
|
66
60
|
create: async (record, opts) => {
|
|
67
|
-
const store = getStore();
|
|
68
61
|
if (record.id == null) {
|
|
69
|
-
throw new
|
|
62
|
+
throw new MisshapedRecord('id');
|
|
70
63
|
}
|
|
71
|
-
if (
|
|
64
|
+
if (getStore().has(record.id)) {
|
|
72
65
|
throw new RecordExists(record.id);
|
|
73
66
|
}
|
|
74
|
-
|
|
75
|
-
if (opts?.ttl != null) {
|
|
76
|
-
const ttl = opts.ttl instanceof Date
|
|
77
|
-
? (opts.ttl.getTime() - Date.now()) / 1000
|
|
78
|
-
: typeof opts.ttl === 'number'
|
|
79
|
-
? opts.ttl * 1000
|
|
80
|
-
: null;
|
|
81
|
-
if (ttl == null) {
|
|
82
|
-
throw new UnsupportedArgumentError('static:opts:ttl-string');
|
|
83
|
-
}
|
|
84
|
-
setTimeout(() => record.id != null && void store.delete(record.id), ttl);
|
|
85
|
-
}
|
|
86
|
-
return record;
|
|
67
|
+
return write(record, opts);
|
|
87
68
|
},
|
|
88
69
|
update: async (record, opts) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
delete: async (id, opts) => {
|
|
92
|
-
let record = null;
|
|
93
|
-
if (typeof id === 'object') {
|
|
94
|
-
if (id.id == null) {
|
|
95
|
-
throw new MisshapedRecord('id');
|
|
96
|
-
}
|
|
97
|
-
record = await resource.load(id.id);
|
|
98
|
-
}
|
|
99
|
-
else if (typeof opts === 'string') {
|
|
100
|
-
record = await resource.load(id, opts);
|
|
70
|
+
if (record.id == null) {
|
|
71
|
+
throw new MisshapedRecord('id');
|
|
101
72
|
}
|
|
102
|
-
|
|
103
|
-
|
|
73
|
+
if (!getStore().has(record.id)) {
|
|
74
|
+
throw new UnknownRecordError(record.id);
|
|
104
75
|
}
|
|
76
|
+
return write(record, opts);
|
|
77
|
+
},
|
|
78
|
+
save: async (record, opts) => write(record, opts),
|
|
79
|
+
delete: async (id) => {
|
|
80
|
+
const store = getStore();
|
|
81
|
+
const record = store.get(id) ?? null;
|
|
105
82
|
if (record == null) {
|
|
106
83
|
return null;
|
|
107
84
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
field = opts;
|
|
111
|
-
opts = undefined;
|
|
112
|
-
}
|
|
113
|
-
else if (typeof opts === 'object') {
|
|
114
|
-
field = opts.field ?? field;
|
|
115
|
-
}
|
|
116
|
-
id = record.id ?? '';
|
|
117
|
-
if (id === '') {
|
|
118
|
-
throw new MisshapedRecord('id');
|
|
119
|
-
}
|
|
120
|
-
const store = getStore();
|
|
121
|
-
if (store.delete(id)) {
|
|
122
|
-
return record;
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
85
|
+
store.delete(id);
|
|
86
|
+
return record;
|
|
125
87
|
},
|
|
126
|
-
|
|
127
|
-
const
|
|
88
|
+
take: async (id) => {
|
|
89
|
+
const store = getStore();
|
|
90
|
+
const record = store.get(id);
|
|
128
91
|
if (record == null) {
|
|
129
|
-
throw new UnknownRecordError(
|
|
92
|
+
throw new UnknownRecordError(id);
|
|
130
93
|
}
|
|
94
|
+
store.delete(id);
|
|
131
95
|
return record;
|
|
96
|
+
},
|
|
97
|
+
purge: async (where) => {
|
|
98
|
+
if (where == null || Object.keys(where).length < 1) {
|
|
99
|
+
throw new UnsupportedArgumentError('purge:empty-criteria');
|
|
100
|
+
}
|
|
101
|
+
const store = getStore();
|
|
102
|
+
const matched = filterRecords(records(), where);
|
|
103
|
+
matched.forEach(record => record.id != null && store.delete(record.id));
|
|
104
|
+
return matched.length;
|
|
132
105
|
}
|
|
133
106
|
});
|
|
134
107
|
return resource;
|
package/build/resource.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/static-resource",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
25
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
24
|
+
"@owlmeans/context": "^0.1.18-rc.19",
|
|
25
|
+
"@owlmeans/error": "^0.1.18-rc.19",
|
|
26
|
+
"@owlmeans/resource": "^0.1.18-rc.20"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@owlmeans/dep-config": "workspace:*",
|
package/src/resource.ts
CHANGED
|
@@ -1,167 +1,146 @@
|
|
|
1
1
|
import { appendContextual } from '@owlmeans/context'
|
|
2
2
|
import { DEFAULT_ALIAS } from './consts.js'
|
|
3
3
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Criteria, FirstOptions, ListOptions, Resource, ResourceRecord, Ttl, WriteOptions
|
|
5
|
+
} from '@owlmeans/resource'
|
|
6
|
+
import {
|
|
7
|
+
applyQuery, filterRecords, firstMatch, MisshapedRecord, RecordExists, UnknownRecordError,
|
|
8
|
+
UnsupportedArgumentError
|
|
6
9
|
} from '@owlmeans/resource'
|
|
7
|
-
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource'
|
|
8
10
|
import type { Config, Context, StaticResourceAppend } from './types.js'
|
|
9
11
|
|
|
10
|
-
type Getter = string | GetterOptions
|
|
11
|
-
type ResourceType = Resource<ResourceRecord>
|
|
12
|
-
|
|
13
12
|
const stores: Record<string, Map<string, ResourceRecord>> = {}
|
|
14
|
-
export const createStaticResource = (alias: string = DEFAULT_ALIAS, key?: string) => {
|
|
15
|
-
key = key ?? alias
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Milliseconds until the record expires, which is what `setTimeout` wants. A number is seconds
|
|
16
|
+
* from now and a Date is the instant to expire at — the two spellings have to land on the same
|
|
17
|
+
* moment, or the same ttl means an hour through one and a second through the other.
|
|
18
|
+
*/
|
|
19
|
+
const expiresIn = (ttl: Ttl): number =>
|
|
20
|
+
ttl instanceof Date ? ttl.getTime() - Date.now() : ttl * 1000
|
|
21
|
+
|
|
22
|
+
export const createStaticResource = <T extends ResourceRecord = ResourceRecord>(
|
|
23
|
+
alias: string = DEFAULT_ALIAS, key?: string
|
|
24
|
+
): Resource<T> => {
|
|
25
|
+
const storeKey = key ?? alias
|
|
26
|
+
|
|
27
|
+
const getStore = (): Map<string, T> => {
|
|
28
|
+
if (stores[storeKey] == null) {
|
|
29
|
+
stores[storeKey] = new Map()
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
return stores[
|
|
32
|
+
return stores[storeKey] as Map<string, T>
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
const
|
|
26
|
-
get: async <T extends ResourceRecord>(id: string, field?: Getter, opts?: LifecycleOptions) => {
|
|
27
|
-
const record = await resource.load<T>(id, field, opts)
|
|
28
|
-
if (record == null) {
|
|
29
|
-
throw new UnknownRecordError(id)
|
|
30
|
-
}
|
|
35
|
+
const records = (): T[] => [...getStore().values()]
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
const expire = (id: string, opts?: WriteOptions): void => {
|
|
38
|
+
if (opts?.ttl == null) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const store = getStore()
|
|
42
|
+
setTimeout(() => void store.delete(id), expiresIn(opts.ttl))
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
/** An id hits the map directly; criteria go through the shared in-memory engine. */
|
|
46
|
+
const first = (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): T | null =>
|
|
47
|
+
typeof idOrWhere === 'string'
|
|
48
|
+
? getStore().get(idOrWhere) ?? null
|
|
49
|
+
: firstMatch(records(), idOrWhere, opts)
|
|
50
|
+
|
|
51
|
+
const write = (record: Partial<T>, opts?: WriteOptions): T => {
|
|
52
|
+
if (record.id == null) {
|
|
53
|
+
/**
|
|
54
|
+
* The store is a map keyed by id and there is nothing here to mint one from, so a record
|
|
55
|
+
* without an id is misshaped rather than new — `save` cannot create it.
|
|
56
|
+
*/
|
|
57
|
+
throw new MisshapedRecord('id')
|
|
58
|
+
}
|
|
59
|
+
getStore().set(record.id, record as T)
|
|
60
|
+
expire(record.id, opts)
|
|
46
61
|
|
|
62
|
+
return record as T
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resource: Resource<T> = appendContextual<Resource<T>>(alias, {
|
|
66
|
+
get: async (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): Promise<T> => {
|
|
67
|
+
const record = first(idOrWhere, opts)
|
|
47
68
|
if (record == null) {
|
|
48
|
-
|
|
69
|
+
throw new UnknownRecordError(typeof idOrWhere === 'string' ? idOrWhere : 'criteria')
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
return record
|
|
52
73
|
},
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new UnsupportedArgumentError('static:list:criteria')
|
|
57
|
-
}
|
|
58
|
-
if (opts != null) {
|
|
59
|
-
throw new UnsupportedArgumentError('static:list:opts')
|
|
60
|
-
}
|
|
75
|
+
load: async (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): Promise<T | null> =>
|
|
76
|
+
first(idOrWhere, opts),
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
/** Unpaged unless a size is asked for — the whole store is already in memory. */
|
|
79
|
+
list: async (where?: Criteria<T>, opts?: ListOptions<T>) => {
|
|
80
|
+
if (opts?.page != null && opts.size == null) {
|
|
81
|
+
throw new UnsupportedArgumentError('page-without-size')
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
return
|
|
84
|
+
return applyQuery(records(), where, opts)
|
|
67
85
|
},
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
count: async (where?: Criteria<T>) => filterRecords(records(), where).length,
|
|
88
|
+
|
|
89
|
+
create: async (record: Partial<T>, opts?: WriteOptions) => {
|
|
71
90
|
if (record.id == null) {
|
|
72
91
|
throw new MisshapedRecord('id')
|
|
73
92
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (typeof opts === 'object' && opts?.ttl != null) {
|
|
77
|
-
const ttl = opts.ttl instanceof Date
|
|
78
|
-
? (opts.ttl.getTime() - Date.now()) / 1000
|
|
79
|
-
: typeof opts.ttl === 'number'
|
|
80
|
-
? opts.ttl * 1000
|
|
81
|
-
: null
|
|
82
|
-
if (ttl == null) {
|
|
83
|
-
throw new UnsupportedArgumentError('static:opts:ttl-string')
|
|
84
|
-
}
|
|
85
|
-
setTimeout(() => record.id != null && void store.delete(record.id), ttl)
|
|
93
|
+
if (getStore().has(record.id)) {
|
|
94
|
+
throw new RecordExists(record.id)
|
|
86
95
|
}
|
|
87
96
|
|
|
88
|
-
return record
|
|
97
|
+
return write(record, opts)
|
|
89
98
|
},
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
const store = getStore()
|
|
100
|
+
update: async (record: Partial<T>, opts?: WriteOptions) => {
|
|
93
101
|
if (record.id == null) {
|
|
94
|
-
throw new
|
|
95
|
-
}
|
|
96
|
-
if (store.has(record.id)) {
|
|
97
|
-
throw new RecordExists(record.id)
|
|
102
|
+
throw new MisshapedRecord('id')
|
|
98
103
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const ttl = opts.ttl instanceof Date
|
|
102
|
-
? (opts.ttl.getTime() - Date.now()) / 1000
|
|
103
|
-
: typeof opts.ttl === 'number'
|
|
104
|
-
? opts.ttl * 1000
|
|
105
|
-
: null
|
|
106
|
-
if (ttl == null) {
|
|
107
|
-
throw new UnsupportedArgumentError('static:opts:ttl-string')
|
|
108
|
-
}
|
|
109
|
-
setTimeout(() => record.id != null && void store.delete(record.id), ttl)
|
|
104
|
+
if (!getStore().has(record.id)) {
|
|
105
|
+
throw new UnknownRecordError(record.id)
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
return record
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
update: async (record, opts) => {
|
|
116
|
-
return resource.save(record, opts)
|
|
108
|
+
return write(record, opts)
|
|
117
109
|
},
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
let record: ResourceRecord | null = null
|
|
121
|
-
if (typeof id === 'object') {
|
|
122
|
-
if (id.id == null) {
|
|
123
|
-
throw new MisshapedRecord('id')
|
|
124
|
-
}
|
|
125
|
-
record = await resource.load(id.id)
|
|
126
|
-
} else if (typeof opts === 'string') {
|
|
127
|
-
record = await resource.load(id, opts)
|
|
128
|
-
} else if (typeof id === 'string') {
|
|
129
|
-
record = await resource.load(id)
|
|
130
|
-
}
|
|
111
|
+
save: async (record: Partial<T>, opts?: WriteOptions) => write(record, opts),
|
|
131
112
|
|
|
113
|
+
delete: async (id: string) => {
|
|
114
|
+
const store = getStore()
|
|
115
|
+
const record = store.get(id) ?? null
|
|
132
116
|
if (record == null) {
|
|
133
117
|
return null
|
|
134
118
|
}
|
|
119
|
+
store.delete(id)
|
|
135
120
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
field = opts
|
|
139
|
-
opts = undefined
|
|
140
|
-
} else if (typeof opts === 'object') {
|
|
141
|
-
field = opts.field ?? field
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
id = record.id ?? ''
|
|
145
|
-
|
|
146
|
-
if (id === '') {
|
|
147
|
-
throw new MisshapedRecord('id')
|
|
148
|
-
}
|
|
121
|
+
return record
|
|
122
|
+
},
|
|
149
123
|
|
|
124
|
+
take: async (id: string) => {
|
|
150
125
|
const store = getStore()
|
|
151
|
-
|
|
152
|
-
|
|
126
|
+
const record = store.get(id)
|
|
127
|
+
if (record == null) {
|
|
128
|
+
throw new UnknownRecordError(id)
|
|
153
129
|
}
|
|
130
|
+
store.delete(id)
|
|
154
131
|
|
|
155
|
-
return
|
|
132
|
+
return record
|
|
156
133
|
},
|
|
157
134
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new UnknownRecordError((typeof id === 'string' ? id : id.id) ?? 'unknown')
|
|
135
|
+
purge: async (where: Criteria<T>) => {
|
|
136
|
+
if (where == null || Object.keys(where).length < 1) {
|
|
137
|
+
throw new UnsupportedArgumentError('purge:empty-criteria')
|
|
162
138
|
}
|
|
139
|
+
const store = getStore()
|
|
140
|
+
const matched = filterRecords(records(), where)
|
|
141
|
+
matched.forEach(record => record.id != null && store.delete(record.id))
|
|
163
142
|
|
|
164
|
-
return
|
|
143
|
+
return matched.length
|
|
165
144
|
}
|
|
166
145
|
})
|
|
167
146
|
|
package/build/.gitkeep
DELETED
|
File without changes
|