live-cache 0.1.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/LICENSE +21 -0
- package/README.md +490 -0
- package/dist/core/Collection.d.ts +92 -0
- package/dist/core/Controller.d.ts +141 -0
- package/dist/core/Document.d.ts +40 -0
- package/dist/core/ObjectStore.d.ts +47 -0
- package/dist/core/StorageManager.d.ts +35 -0
- package/dist/core/join.d.ts +106 -0
- package/dist/index.cjs +1270 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.mjs +1251 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.js +1274 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/react/Context.d.ts +33 -0
- package/dist/react/useController.d.ts +39 -0
- package/dist/react/useJoinController.d.ts +27 -0
- package/dist/storage-manager/IndexDbStorageManager.d.ts +37 -0
- package/dist/storage-manager/LocalStorageManager.d.ts +15 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# LiveCache
|
|
2
|
+
|
|
3
|
+
A lightweight, type-safe client-side database library for JavaScript written in TypeScript. Store and query data collections directly in the browser with MongoDB-like syntax.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📦 Written in TypeScript with full type definitions
|
|
8
|
+
- 🎯 Small bundle size with minimal dependencies
|
|
9
|
+
- 🔧 Works in both browser and module environments
|
|
10
|
+
- ⚡ Fast indexed queries using hash-based lookups
|
|
11
|
+
- 💾 Built-in serialization/deserialization for persistence
|
|
12
|
+
- 🔍 MongoDB-like query interface
|
|
13
|
+
- 🎨 Beautiful examples included
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install live-cache
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or use it directly in the browser via UMD build:
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<script src="path/to/dist/index.umd.js"></script>
|
|
25
|
+
<script>
|
|
26
|
+
const { Collection } = LiveCache;
|
|
27
|
+
const users = new Collection("users");
|
|
28
|
+
</script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### ES Modules (Recommended)
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
import { Collection } from "live-cache";
|
|
37
|
+
|
|
38
|
+
// Create a collection
|
|
39
|
+
const users = new Collection("users");
|
|
40
|
+
|
|
41
|
+
// Insert documents
|
|
42
|
+
const user = users.insertOne({
|
|
43
|
+
name: "John Doe",
|
|
44
|
+
email: "john@example.com",
|
|
45
|
+
age: 30,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(user._id); // Auto-generated MongoDB-style ID
|
|
49
|
+
|
|
50
|
+
// Insert multiple documents
|
|
51
|
+
const newUsers = users.insertMany([
|
|
52
|
+
{ name: "Jane Smith", email: "jane@example.com", age: 25 },
|
|
53
|
+
{ name: "Bob Johnson", email: "bob@example.com", age: 35 },
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// Find documents
|
|
57
|
+
const allUsers = users.find(); // Get all documents
|
|
58
|
+
const jane = users.findOne({ name: "Jane Smith" }); // Find by condition
|
|
59
|
+
const userById = users.findOne("507f1f77bcf86cd799439011"); // Find by _id
|
|
60
|
+
|
|
61
|
+
// Update documents
|
|
62
|
+
const updated = users.findOneAndUpdate({ name: "John Doe" }, { age: 31 });
|
|
63
|
+
|
|
64
|
+
// Delete documents
|
|
65
|
+
users.deleteOne({ name: "Bob Johnson" });
|
|
66
|
+
users.deleteOne("507f1f77bcf86cd799439011"); // Delete by _id
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Persistence with Serialization
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
import { Collection } from "live-cache";
|
|
73
|
+
|
|
74
|
+
const todos = new Collection("todos");
|
|
75
|
+
|
|
76
|
+
// Add some data
|
|
77
|
+
todos.insertMany([
|
|
78
|
+
{ task: "Buy groceries", completed: false },
|
|
79
|
+
{ task: "Write code", completed: true },
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// Serialize to string for storage
|
|
83
|
+
const serialized = todos.serialize();
|
|
84
|
+
localStorage.setItem("todos", serialized);
|
|
85
|
+
|
|
86
|
+
// Later... deserialize and restore
|
|
87
|
+
const savedData = localStorage.getItem("todos");
|
|
88
|
+
const restoredTodos = Collection.deserialize("todos", savedData);
|
|
89
|
+
|
|
90
|
+
// Or hydrate an existing collection
|
|
91
|
+
todos.hydrate(savedData);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Browser (UMD)
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
<script src="node_modules/live-cache/dist/index.umd.js"></script>
|
|
98
|
+
<script>
|
|
99
|
+
const { Collection } = LiveCache;
|
|
100
|
+
|
|
101
|
+
const products = new Collection("products");
|
|
102
|
+
products.insertOne({
|
|
103
|
+
name: "Laptop",
|
|
104
|
+
price: 999,
|
|
105
|
+
inStock: true,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const laptop = products.findOne({ name: "Laptop" });
|
|
109
|
+
console.log(laptop.toModel());
|
|
110
|
+
</script>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Using ObjectStore (recommended with Controllers)
|
|
114
|
+
|
|
115
|
+
`ObjectStore` is a simple registry for controllers. It’s used by the React helpers, but you can use it in any framework.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { createObjectStore } from "live-cache";
|
|
119
|
+
|
|
120
|
+
const store = createObjectStore();
|
|
121
|
+
// store.register(new UsersController(...))
|
|
122
|
+
// store.get("users")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Controllers (recommended integration layer)
|
|
126
|
+
|
|
127
|
+
Use `Controller<T, Name>` for **server-backed** resources: it wraps a `Collection` and adds hydration, persistence, subscriptions, and invalidation hooks.
|
|
128
|
+
|
|
129
|
+
### Extending `Controller` + using `commit()`
|
|
130
|
+
|
|
131
|
+
`commit()` is the important part: it **publishes** the latest snapshot to subscribers and **persists** the snapshot using the configured `StorageManager`.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { Controller } from "live-cache";
|
|
135
|
+
|
|
136
|
+
type User = { id: number; name: string };
|
|
137
|
+
|
|
138
|
+
class UsersController extends Controller<User, "users"> {
|
|
139
|
+
async fetchAll(): Promise<[User[], number]> {
|
|
140
|
+
const res = await fetch("/api/users");
|
|
141
|
+
if (!res.ok) throw new Error("Failed to fetch users");
|
|
142
|
+
const data = (await res.json()) as User[];
|
|
143
|
+
return [data, data.length];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Example invalidation hook (you decide what invalidation means).
|
|
148
|
+
* Common behavior is: abort in-flight fetch, clear/patch local cache, refetch, then commit.
|
|
149
|
+
*/
|
|
150
|
+
invalidate(): () => void {
|
|
151
|
+
this.abort();
|
|
152
|
+
void this.refetch();
|
|
153
|
+
return () => {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async renameUser(id: number, name: string) {
|
|
157
|
+
// Mutate the collection…
|
|
158
|
+
this.collection.findOneAndUpdate({ id }, { name });
|
|
159
|
+
// …then commit so subscribers + persistence stay in sync.
|
|
160
|
+
await this.commit();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Persistence (`StorageManager`)
|
|
166
|
+
|
|
167
|
+
Controllers persist snapshots through a `StorageManager` (array-of-models, not a JSON string).
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { Controller, LocalStorageStorageManager } from "live-cache";
|
|
171
|
+
|
|
172
|
+
const users = new UsersController(
|
|
173
|
+
"users",
|
|
174
|
+
true,
|
|
175
|
+
new LocalStorageStorageManager("my-app:")
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## React integration
|
|
180
|
+
|
|
181
|
+
Use `ContextProvider` to provide an `ObjectStore`, `useRegister()` to register controllers, and `useController()` to subscribe to a controller.
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import React from "react";
|
|
185
|
+
import { ContextProvider, useRegister, useController } from "live-cache";
|
|
186
|
+
|
|
187
|
+
const usersController = new UsersController("users");
|
|
188
|
+
|
|
189
|
+
function App() {
|
|
190
|
+
useRegister([usersController]);
|
|
191
|
+
const { data, loading, error, controller } = useController(
|
|
192
|
+
"users",
|
|
193
|
+
undefined,
|
|
194
|
+
{
|
|
195
|
+
withInvalidation: true,
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (loading) return <div>Loading…</div>;
|
|
200
|
+
if (error) return <div>Something went wrong</div>;
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div>
|
|
204
|
+
<button onClick={() => void controller.invalidate()}>Refresh</button>
|
|
205
|
+
{data.map((u) => (
|
|
206
|
+
<div key={u._id}>{u.name}</div>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default function Root() {
|
|
213
|
+
return (
|
|
214
|
+
<ContextProvider>
|
|
215
|
+
<App />
|
|
216
|
+
</ContextProvider>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Cache invalidation recipes
|
|
222
|
+
|
|
223
|
+
These show **framework-agnostic** controller patterns and a **React** wiring example for each.
|
|
224
|
+
|
|
225
|
+
### 1) Timeout-based cache invalidation (TTL)
|
|
226
|
+
|
|
227
|
+
#### Framework-agnostic
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { Controller } from "live-cache";
|
|
231
|
+
|
|
232
|
+
type Post = { id: number; title: string };
|
|
233
|
+
|
|
234
|
+
class PostsController extends Controller<Post, "posts"> {
|
|
235
|
+
private ttlMs: number;
|
|
236
|
+
private lastFetchedAt = 0;
|
|
237
|
+
private cleanupInvalidation: (() => void) | null = null;
|
|
238
|
+
|
|
239
|
+
constructor(name: "posts", ttlMs = 30_000) {
|
|
240
|
+
super(name);
|
|
241
|
+
this.ttlMs = ttlMs;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async fetchAll(): Promise<[Post[], number]> {
|
|
245
|
+
const res = await fetch("/api/posts");
|
|
246
|
+
const data = (await res.json()) as Post[];
|
|
247
|
+
this.lastFetchedAt = Date.now();
|
|
248
|
+
return [data, data.length];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* TTL invalidation lives here:
|
|
253
|
+
* - first call wires up the interval
|
|
254
|
+
* - subsequent calls perform the TTL check and revalidate if expired
|
|
255
|
+
*/
|
|
256
|
+
invalidate(): () => void {
|
|
257
|
+
if (!this.cleanupInvalidation) {
|
|
258
|
+
const id = window.setInterval(() => void this.invalidate(), this.ttlMs);
|
|
259
|
+
this.cleanupInvalidation = () => {
|
|
260
|
+
window.clearInterval(id);
|
|
261
|
+
this.cleanupInvalidation = null;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
const fresh = this.lastFetchedAt && now - this.lastFetchedAt < this.ttlMs;
|
|
267
|
+
if (fresh) return this.cleanupInvalidation!;
|
|
268
|
+
|
|
269
|
+
this.abort();
|
|
270
|
+
void this.refetch();
|
|
271
|
+
return this.cleanupInvalidation!;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const posts = new PostsController("posts", 10_000);
|
|
276
|
+
posts.invalidate(); // starts the interval + performs initial TTL check
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### React
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
function PostsPage() {
|
|
283
|
+
useRegister([posts]);
|
|
284
|
+
const { data } = useController("posts", undefined, {
|
|
285
|
+
withInvalidation: true,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return data.map((p) => <div key={p._id}>{p.title}</div>);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 2) SWR-style invalidation (stale-while-revalidate)
|
|
293
|
+
|
|
294
|
+
#### Framework-agnostic
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { Controller } from "live-cache";
|
|
298
|
+
|
|
299
|
+
type Todo = { id: number; title: string };
|
|
300
|
+
|
|
301
|
+
class TodosController extends Controller<Todo, "todos"> {
|
|
302
|
+
private revalidateAfterMs = 30_000;
|
|
303
|
+
private lastFetchedAt = 0;
|
|
304
|
+
private cleanupInvalidation: (() => void) | null = null;
|
|
305
|
+
|
|
306
|
+
async fetchAll(): Promise<[Todo[], number]> {
|
|
307
|
+
const res = await fetch("/api/todos");
|
|
308
|
+
const data = (await res.json()) as Todo[];
|
|
309
|
+
this.lastFetchedAt = Date.now();
|
|
310
|
+
return [data, data.length];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async initialise(): Promise<void> {
|
|
314
|
+
// hydrate/publish cached snapshot first (super.initialise does this)
|
|
315
|
+
await super.initialise();
|
|
316
|
+
|
|
317
|
+
// then revalidate in background if stale
|
|
318
|
+
const stale =
|
|
319
|
+
!this.lastFetchedAt ||
|
|
320
|
+
Date.now() - this.lastFetchedAt > this.revalidateAfterMs;
|
|
321
|
+
if (stale) void this.refetch();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
invalidate(): () => void {
|
|
325
|
+
// SWR-style invalidation wiring lives here:
|
|
326
|
+
// - first call wires up triggers (focus/online)
|
|
327
|
+
// - every call can also trigger a revalidation
|
|
328
|
+
if (!this.cleanupInvalidation) {
|
|
329
|
+
const revalidate = () => {
|
|
330
|
+
this.abort();
|
|
331
|
+
void this.refetch();
|
|
332
|
+
};
|
|
333
|
+
window.addEventListener("focus", revalidate);
|
|
334
|
+
window.addEventListener("online", revalidate);
|
|
335
|
+
this.cleanupInvalidation = () => {
|
|
336
|
+
window.removeEventListener("focus", revalidate);
|
|
337
|
+
window.removeEventListener("online", revalidate);
|
|
338
|
+
this.cleanupInvalidation = null;
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this.abort();
|
|
343
|
+
void this.refetch();
|
|
344
|
+
return this.cleanupInvalidation!;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### React
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
function TodosPage() {
|
|
353
|
+
useRegister([todos]);
|
|
354
|
+
const { data, loading, controller } = useController("todos", undefined, {
|
|
355
|
+
withInvalidation: true,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<div>
|
|
360
|
+
{loading ? <div>Revalidating…</div> : null}
|
|
361
|
+
<button onClick={() => void controller.invalidate()}>Revalidate</button>
|
|
362
|
+
{data.map((t) => (
|
|
363
|
+
<div key={t._id}>{t.title}</div>
|
|
364
|
+
))}
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 3) Websocket-based invalidation (push)
|
|
371
|
+
|
|
372
|
+
#### Framework-agnostic
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
type InvalidationMsg =
|
|
376
|
+
| { type: "invalidate"; controller: "users" }
|
|
377
|
+
| { type: "patch-user"; id: number; name: string };
|
|
378
|
+
|
|
379
|
+
class UsersController extends Controller<
|
|
380
|
+
{ id: number; name: string },
|
|
381
|
+
"users"
|
|
382
|
+
> {
|
|
383
|
+
private ws: WebSocket | null = null;
|
|
384
|
+
private cleanupInvalidation: (() => void) | null = null;
|
|
385
|
+
|
|
386
|
+
async fetchAll() {
|
|
387
|
+
const res = await fetch("/api/users");
|
|
388
|
+
const data = (await res.json()) as { id: number; name: string }[];
|
|
389
|
+
return [data, data.length] as const;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Websocket subscription lives here:
|
|
394
|
+
* - first call attaches the socket + listeners
|
|
395
|
+
* - incoming messages either trigger a refetch or apply a patch + commit
|
|
396
|
+
*/
|
|
397
|
+
invalidate(): () => void {
|
|
398
|
+
if (this.cleanupInvalidation) return this.cleanupInvalidation;
|
|
399
|
+
|
|
400
|
+
const ws = new WebSocket("wss://example.com/ws");
|
|
401
|
+
this.ws = ws;
|
|
402
|
+
this.cleanupInvalidation = () => {
|
|
403
|
+
this.ws?.close();
|
|
404
|
+
this.ws = null;
|
|
405
|
+
this.cleanupInvalidation = null;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
ws.addEventListener("message", (evt) => {
|
|
409
|
+
const msg = JSON.parse(String(evt.data)) as InvalidationMsg;
|
|
410
|
+
|
|
411
|
+
if (msg.type === "invalidate" && msg.controller === "users") {
|
|
412
|
+
this.abort();
|
|
413
|
+
void this.refetch();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (msg.type === "patch-user") {
|
|
418
|
+
this.collection.findOneAndUpdate({ id: msg.id }, { name: msg.name });
|
|
419
|
+
void this.commit();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
return this.cleanupInvalidation;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### React
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
function UsersPage() {
|
|
431
|
+
useRegister([usersController]);
|
|
432
|
+
const { data, controller } = useController("users", undefined, {
|
|
433
|
+
withInvalidation: true,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
return data.map((u) => <div key={u._id}>{u.name}</div>);
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Joins (advanced)
|
|
441
|
+
|
|
442
|
+
Join data across controllers with `join(from, where, select)` or subscribe in React via `useJoinController`.
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
import { join } from "live-cache";
|
|
446
|
+
|
|
447
|
+
const result = join(
|
|
448
|
+
[usersController, postsController] as const,
|
|
449
|
+
{
|
|
450
|
+
$and: {
|
|
451
|
+
posts: { userId: { $ref: { controller: "users", field: "id" } } },
|
|
452
|
+
},
|
|
453
|
+
} as const,
|
|
454
|
+
["users.name", "posts.title"] as const
|
|
455
|
+
);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
import { useJoinController } from "live-cache";
|
|
460
|
+
|
|
461
|
+
const rows = useJoinController({
|
|
462
|
+
from: [usersController, postsController] as const,
|
|
463
|
+
where: {
|
|
464
|
+
$and: {
|
|
465
|
+
posts: { userId: { $ref: { controller: "users", field: "id" } } },
|
|
466
|
+
},
|
|
467
|
+
} as const,
|
|
468
|
+
select: ["users.name", "posts.title"] as const,
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## API Reference (high-level)
|
|
473
|
+
|
|
474
|
+
For full details, see the TSDoc on the exported APIs.
|
|
475
|
+
|
|
476
|
+
- **Core**: `Collection`, `Document`, `Controller`, `ObjectStore`, `StorageManager`, `DefaultStorageManager`, `join`
|
|
477
|
+
- **Storage managers**: `LocalStorageStorageManager`, `IndexDbStorageManager`
|
|
478
|
+
- **React**: `ContextProvider`, `useRegister`, `useController`, `useJoinController`
|
|
479
|
+
|
|
480
|
+
## Development
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
npm install
|
|
484
|
+
npm run build
|
|
485
|
+
npm run dev
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## License
|
|
489
|
+
|
|
490
|
+
MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import Document from "./Document";
|
|
2
|
+
/**
|
|
3
|
+
* An in-memory collection of documents with simple hash-based indexes.
|
|
4
|
+
*
|
|
5
|
+
* - Insert/update/delete operations keep indexes consistent.
|
|
6
|
+
* - `find()` / `findOne()` attempt indexed lookups first and fall back to linear scan.
|
|
7
|
+
*
|
|
8
|
+
* This is commonly used via `Controller.collection`.
|
|
9
|
+
*
|
|
10
|
+
* @typeParam TVariable - the data shape stored in the collection (without `_id`)
|
|
11
|
+
* @typeParam TName - the collection name (string-literal type)
|
|
12
|
+
*/
|
|
13
|
+
export default class Collection<TVariable, TName extends string> {
|
|
14
|
+
name: TName;
|
|
15
|
+
private dataMap;
|
|
16
|
+
private indexes;
|
|
17
|
+
private counter;
|
|
18
|
+
constructor(name: TName);
|
|
19
|
+
/**
|
|
20
|
+
* Clear all in-memory documents and indexes.
|
|
21
|
+
*/
|
|
22
|
+
clear(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Add a document to all relevant indexes
|
|
25
|
+
*/
|
|
26
|
+
private addToIndexes;
|
|
27
|
+
/**
|
|
28
|
+
* Remove a document from all indexes
|
|
29
|
+
*/
|
|
30
|
+
private removeFromIndexes;
|
|
31
|
+
/**
|
|
32
|
+
* Find a single document by _id or by matching conditions (optimized with indexes)
|
|
33
|
+
*/
|
|
34
|
+
findOne(where: string | Partial<TVariable>): Document<TVariable> | null;
|
|
35
|
+
/**
|
|
36
|
+
* Find all documents matching the conditions (optimized with indexes)
|
|
37
|
+
*/
|
|
38
|
+
find(where?: string | Partial<TVariable>): Document<TVariable>[];
|
|
39
|
+
/**
|
|
40
|
+
* Helper method to check if a document matches the conditions
|
|
41
|
+
*/
|
|
42
|
+
private matchesConditions;
|
|
43
|
+
/**
|
|
44
|
+
* Insert a new document into the collection
|
|
45
|
+
*/
|
|
46
|
+
insertOne(data: TVariable): Document<TVariable>;
|
|
47
|
+
/**
|
|
48
|
+
* Delete a document by _id or by matching conditions
|
|
49
|
+
*/
|
|
50
|
+
deleteOne(where: string | Partial<TVariable>): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Find a document and update it with new data
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* users.findOneAndUpdate({ id: 1 }, { name: "Updated" });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
findOneAndUpdate(where: string | Partial<TVariable>, update: Partial<TVariable>): Document<TVariable> | null;
|
|
60
|
+
/**
|
|
61
|
+
* Insert multiple documents into the collection at once
|
|
62
|
+
* @param dataArray Array of data objects to insert
|
|
63
|
+
* @returns Array of inserted documents
|
|
64
|
+
*/
|
|
65
|
+
insertMany(dataArray: TVariable[]): Document<TVariable>[];
|
|
66
|
+
/**
|
|
67
|
+
* Serialize the collection to a plain object for storage
|
|
68
|
+
* @returns A plain object representation of the collection
|
|
69
|
+
*/
|
|
70
|
+
serialize(): string;
|
|
71
|
+
/**
|
|
72
|
+
* Deserialize and restore collection data from storage
|
|
73
|
+
* @param serializedData The serialized collection data
|
|
74
|
+
* @returns A new Collection instance with the restored data
|
|
75
|
+
*/
|
|
76
|
+
static deserialize<T, N extends string>(name: N, serializedData: string): Collection<T, N>;
|
|
77
|
+
/**
|
|
78
|
+
* Hydrate the current collection instance with data from storage
|
|
79
|
+
* This clears existing data and replaces it with the deserialized data
|
|
80
|
+
* @param serializedData The serialized collection data
|
|
81
|
+
*/
|
|
82
|
+
hydrate(serializedData: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* Dehydrate the collection to a format suitable for storage
|
|
85
|
+
* This is an alias for serialize() for semantic clarity
|
|
86
|
+
* @returns A serialized string representation of the collection
|
|
87
|
+
*/
|
|
88
|
+
dehydrate(): string;
|
|
89
|
+
static hash(data: any): string;
|
|
90
|
+
private static stableStringify;
|
|
91
|
+
private static fnv1a;
|
|
92
|
+
}
|