@telorun/cache-memory 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -141
- package/dist/store-controller.d.ts +1 -0
- package/dist/store-controller.js +21 -0
- package/package.json +3 -3
- package/src/store-controller.ts +21 -0
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Built to be language-agnostic and infinitely extensible.
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# Reconcile your manifest into a running backend
|
|
26
|
-
$ telo ./examples/hello-api
|
|
26
|
+
$ telo ./examples/hello-api
|
|
27
27
|
|
|
28
28
|
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
|
|
29
29
|
```
|
|
@@ -44,146 +44,7 @@ $ telo ./examples/hello-api.yaml
|
|
|
44
44
|
|
|
45
45
|
## Example manifest
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```yaml
|
|
50
|
-
kind: Telo.Application
|
|
51
|
-
metadata:
|
|
52
|
-
name: feedback
|
|
53
|
-
version: 1.0.0
|
|
54
|
-
description: |
|
|
55
|
-
A complete feedback collection REST API — no code, pure YAML.
|
|
56
|
-
Persists entries to SQLite and serves them over HTTP.
|
|
57
|
-
imports:
|
|
58
|
-
Http: std/http-server@0.12.0
|
|
59
|
-
Sql: std/sql@0.9.2
|
|
60
|
-
targets:
|
|
61
|
-
- !ref Migrations
|
|
62
|
-
- !ref Server
|
|
63
|
-
---
|
|
64
|
-
# SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
|
|
65
|
-
kind: Sql.Connection
|
|
66
|
-
metadata:
|
|
67
|
-
name: Db
|
|
68
|
-
driver: sqlite
|
|
69
|
-
file: ./tmp/feedback.db
|
|
70
|
-
---
|
|
71
|
-
# Migrations: applied automatically before the server starts
|
|
72
|
-
kind: Sql.Migrations
|
|
73
|
-
metadata:
|
|
74
|
-
name: Migrations
|
|
75
|
-
connection: !ref Db
|
|
76
|
-
---
|
|
77
|
-
kind: Sql.Migration
|
|
78
|
-
metadata:
|
|
79
|
-
name: Migration_20260413_182154_CreateFeedback
|
|
80
|
-
version: 20260413_182154_CreateFeedback
|
|
81
|
-
sql: |
|
|
82
|
-
CREATE TABLE IF NOT EXISTS feedback (
|
|
83
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
-
text TEXT NOT NULL,
|
|
85
|
-
source TEXT,
|
|
86
|
-
score INTEGER NOT NULL DEFAULT 0,
|
|
87
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
88
|
-
)
|
|
89
|
-
---
|
|
90
|
-
kind: Http.Server
|
|
91
|
-
metadata:
|
|
92
|
-
name: Server
|
|
93
|
-
baseUrl: http://localhost:8844
|
|
94
|
-
port: 8844
|
|
95
|
-
logger: true
|
|
96
|
-
openapi:
|
|
97
|
-
info:
|
|
98
|
-
title: Feedback API
|
|
99
|
-
version: 1.0.0
|
|
100
|
-
mounts:
|
|
101
|
-
- path: /v1
|
|
102
|
-
mount: !ref FeedbackRoutes
|
|
103
|
-
---
|
|
104
|
-
kind: Http.Api
|
|
105
|
-
metadata:
|
|
106
|
-
name: FeedbackRoutes
|
|
107
|
-
routes:
|
|
108
|
-
# POST /v1/feedback — insert a new entry, score derived from body length heuristic
|
|
109
|
-
- request:
|
|
110
|
-
path: /feedback
|
|
111
|
-
method: POST
|
|
112
|
-
schema:
|
|
113
|
-
body:
|
|
114
|
-
type: object
|
|
115
|
-
properties:
|
|
116
|
-
text:
|
|
117
|
-
type: string
|
|
118
|
-
minLength: 1
|
|
119
|
-
source:
|
|
120
|
-
type: string
|
|
121
|
-
required: [ text ]
|
|
122
|
-
handler:
|
|
123
|
-
kind: Sql.Exec
|
|
124
|
-
connection: !ref Db
|
|
125
|
-
inputs:
|
|
126
|
-
sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
|
|
127
|
-
bindings:
|
|
128
|
-
- "${{ request.body.text }}"
|
|
129
|
-
- "${{ request.body.source }}"
|
|
130
|
-
- "${{ size(request.body.text) }}"
|
|
131
|
-
response:
|
|
132
|
-
- status: 201
|
|
133
|
-
headers:
|
|
134
|
-
Content-Type: application/json
|
|
135
|
-
body:
|
|
136
|
-
ok: true
|
|
137
|
-
message: Feedback received
|
|
138
|
-
|
|
139
|
-
# GET /v1/feedback — list all entries, newest first
|
|
140
|
-
- request:
|
|
141
|
-
path: /feedback
|
|
142
|
-
method: GET
|
|
143
|
-
handler:
|
|
144
|
-
kind: Sql.Select
|
|
145
|
-
connection: !ref Db
|
|
146
|
-
from: feedback
|
|
147
|
-
columns: [ id, text, source, score, created_at ]
|
|
148
|
-
orderBy:
|
|
149
|
-
- { column: created_at, direction: desc }
|
|
150
|
-
response:
|
|
151
|
-
- status: 200
|
|
152
|
-
headers:
|
|
153
|
-
Content-Type: application/json
|
|
154
|
-
body: "${{ result.rows }}"
|
|
155
|
-
|
|
156
|
-
# GET /v1/feedback/{id} — fetch a single entry
|
|
157
|
-
- request:
|
|
158
|
-
path: /feedback/{id}
|
|
159
|
-
method: GET
|
|
160
|
-
schema:
|
|
161
|
-
params:
|
|
162
|
-
type: object
|
|
163
|
-
properties:
|
|
164
|
-
id:
|
|
165
|
-
type: integer
|
|
166
|
-
required: [ id ]
|
|
167
|
-
handler:
|
|
168
|
-
kind: Sql.Select
|
|
169
|
-
connection: !ref Db
|
|
170
|
-
from: feedback
|
|
171
|
-
columns: [ id, text, source, score, created_at ]
|
|
172
|
-
where:
|
|
173
|
-
- { column: id, op: "=", value: "${{ request.params.id }}" }
|
|
174
|
-
response:
|
|
175
|
-
- status: 200
|
|
176
|
-
when: "size(result.rows) > 0"
|
|
177
|
-
headers:
|
|
178
|
-
Content-Type: application/json
|
|
179
|
-
body: "${{ result.rows[0] }}"
|
|
180
|
-
- status: 404
|
|
181
|
-
headers:
|
|
182
|
-
Content-Type: application/json
|
|
183
|
-
body:
|
|
184
|
-
ok: false
|
|
185
|
-
message: Not found
|
|
186
|
-
```
|
|
47
|
+
See [examples/](./examples/) for a list of working applications.
|
|
187
48
|
|
|
188
49
|
## Status
|
|
189
50
|
|
|
@@ -19,6 +19,7 @@ declare class MemoryStore implements ResourceInstance, CacheStore {
|
|
|
19
19
|
get(key: string): Promise<CacheLookupResult>;
|
|
20
20
|
set(key: string, value: unknown, ttlMs: number, staleTtlMs: number): Promise<void>;
|
|
21
21
|
delete(key: string): Promise<void>;
|
|
22
|
+
increment(key: string, delta: number, ttlMs: number): Promise<number>;
|
|
22
23
|
provide(): Promise<MemoryStore>;
|
|
23
24
|
teardown(): Promise<void>;
|
|
24
25
|
snapshot(): Record<string, unknown>;
|
package/dist/store-controller.js
CHANGED
|
@@ -40,6 +40,27 @@ class MemoryStore {
|
|
|
40
40
|
async delete(key) {
|
|
41
41
|
this.entries.delete(key);
|
|
42
42
|
}
|
|
43
|
+
// Atomic within the single-threaded event loop: no `await` between the read
|
|
44
|
+
// and the write, so concurrent increments can't interleave. The expiry is set
|
|
45
|
+
// only when the counter is first created this window (fixed window).
|
|
46
|
+
async increment(key, delta, ttlMs) {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const entry = this.entries.get(key);
|
|
49
|
+
if (!entry || now >= entry.expireAt) {
|
|
50
|
+
this.entries.delete(key);
|
|
51
|
+
this.entries.set(key, { value: delta, storedAt: now, freshUntil: now + ttlMs, expireAt: now + ttlMs });
|
|
52
|
+
while (this.entries.size > this.maxEntries) {
|
|
53
|
+
const oldest = this.entries.keys().next().value;
|
|
54
|
+
if (oldest === undefined)
|
|
55
|
+
break;
|
|
56
|
+
this.entries.delete(oldest);
|
|
57
|
+
}
|
|
58
|
+
return delta;
|
|
59
|
+
}
|
|
60
|
+
const next = (typeof entry.value === "number" ? entry.value : 0) + delta;
|
|
61
|
+
entry.value = next;
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
43
64
|
async provide() {
|
|
44
65
|
return this;
|
|
45
66
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/cache-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Telo cache-memory module — in-process, freshness-aware CacheMemory.Store.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"src/**"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@telorun/cache": "0.
|
|
43
|
+
"@telorun/cache": "0.3.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^20.0.0",
|
|
47
47
|
"typescript": "^5.0.0",
|
|
48
|
-
"@telorun/sdk": "0.
|
|
48
|
+
"@telorun/sdk": "0.38.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@telorun/sdk": "*"
|
package/src/store-controller.ts
CHANGED
|
@@ -58,6 +58,27 @@ class MemoryStore implements ResourceInstance, CacheStore {
|
|
|
58
58
|
this.entries.delete(key);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Atomic within the single-threaded event loop: no `await` between the read
|
|
62
|
+
// and the write, so concurrent increments can't interleave. The expiry is set
|
|
63
|
+
// only when the counter is first created this window (fixed window).
|
|
64
|
+
async increment(key: string, delta: number, ttlMs: number): Promise<number> {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const entry = this.entries.get(key);
|
|
67
|
+
if (!entry || now >= entry.expireAt) {
|
|
68
|
+
this.entries.delete(key);
|
|
69
|
+
this.entries.set(key, { value: delta, storedAt: now, freshUntil: now + ttlMs, expireAt: now + ttlMs });
|
|
70
|
+
while (this.entries.size > this.maxEntries) {
|
|
71
|
+
const oldest = this.entries.keys().next().value;
|
|
72
|
+
if (oldest === undefined) break;
|
|
73
|
+
this.entries.delete(oldest);
|
|
74
|
+
}
|
|
75
|
+
return delta;
|
|
76
|
+
}
|
|
77
|
+
const next = (typeof entry.value === "number" ? entry.value : 0) + delta;
|
|
78
|
+
entry.value = next;
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
async provide(): Promise<MemoryStore> {
|
|
62
83
|
return this;
|
|
63
84
|
}
|