@cioky/ripple-query-remult 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/README.md +75 -0
- package/package.json +35 -0
- package/src/index.ts +169 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @cioky/ripple-query-remult
|
|
2
|
+
|
|
3
|
+
Remult adapter for `@cioky/ripple-query` — automatic key derivation from Remult queries, LiveQuery invalidation.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
bun add @cioky/ripple-query-remult
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createRemultQuery } from '@cioky/ripple-query-remult'
|
|
13
|
+
import { remult } from 'remult'
|
|
14
|
+
|
|
15
|
+
export function TaskList() @{
|
|
16
|
+
let &[tasks] = createRemultQuery(
|
|
17
|
+
remult.repo(Task),
|
|
18
|
+
'find',
|
|
19
|
+
{ where: { completed: true } },
|
|
20
|
+
{ liveQuery: true }
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@if (tasks === undefined) {
|
|
24
|
+
<p>Loading...</p>
|
|
25
|
+
} @else {
|
|
26
|
+
<ul>
|
|
27
|
+
@for (const t of tasks) {
|
|
28
|
+
<li>{t.title}</li>
|
|
29
|
+
}
|
|
30
|
+
</ul>
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `createRemultQuery(repo, method, params?, options?)`
|
|
38
|
+
|
|
39
|
+
Returns `[data, info]` where both are `Tracked` signals. Generates a stable query key from `[entityName, method, params]`.
|
|
40
|
+
|
|
41
|
+
| Param | Type | Description |
|
|
42
|
+
|-------|------|-------------|
|
|
43
|
+
| `repo` | `Repo<T>` | A Remult repo, e.g. `remult.repo(Task)` |
|
|
44
|
+
| `method` | `'find' \| 'findFirst' \| 'count'` | Query method |
|
|
45
|
+
| `params` | `Record<string, unknown>` | Query params (where, orderBy, limit, etc.) |
|
|
46
|
+
| `options.liveQuery` | `boolean` | Subscribe to entity SSE channel for realtime invalidation |
|
|
47
|
+
|
|
48
|
+
### Manual key building
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { buildKey } from '@cioky/ripple-query-remult'
|
|
52
|
+
import { query, invalidateKeys } from '@cioky/ripple-query'
|
|
53
|
+
|
|
54
|
+
const key = buildKey(remult.repo(Task), 'find', { where: { done: true } })
|
|
55
|
+
const [tasks] = query(key, () => remult.repo(Task).find({ where: { done: true } }))
|
|
56
|
+
|
|
57
|
+
// Later, after a Task mutation:
|
|
58
|
+
invalidateKeys(['Task']) // invalidates ALL Task queries
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `subscribeEntity(entityName)`
|
|
62
|
+
|
|
63
|
+
Manually subscribe to LiveQuery changes for an entity. Safe to call multiple times — deduplicates.
|
|
64
|
+
|
|
65
|
+
### Re-exports from `@cioky/ripple-query`
|
|
66
|
+
|
|
67
|
+
- `invalidateKeys`
|
|
68
|
+
- `invalidateAll`
|
|
69
|
+
- `QueryKey`, `QueryInfo`
|
|
70
|
+
|
|
71
|
+
## Peer Dependencies
|
|
72
|
+
|
|
73
|
+
- `@cioky/ripple-query` >= 0.1.x
|
|
74
|
+
- `remult` >= 0.26.0
|
|
75
|
+
- `remult-partykit` >= 0.0.1
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cioky/ripple-query-remult",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Remult adapter for @cioky/ripple-query — auto-key derivation, LiveQuery invalidation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"default": "./src/index.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Opaius/vike-ripple.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/Opaius/vike-ripple",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/Opaius/vike-ripple/issues"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ripple",
|
|
25
|
+
"remult",
|
|
26
|
+
"query",
|
|
27
|
+
"livequery"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@cioky/ripple-query": "0.1.x",
|
|
32
|
+
"remult": ">=0.26.0",
|
|
33
|
+
"remult-partykit": ">=0.0.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cioky/ripple-query-remult — Remult adapter for @cioky/ripple-query.
|
|
3
|
+
*
|
|
4
|
+
* Provides auto-key derivation from Remult repo queries, LiveQuery
|
|
5
|
+
* invalidation, and a factory wrapper around `query()`.
|
|
6
|
+
*/
|
|
7
|
+
import type { Tracked } from 'ripple';
|
|
8
|
+
import {
|
|
9
|
+
query,
|
|
10
|
+
invalidateKeys,
|
|
11
|
+
type QueryKey,
|
|
12
|
+
type QueryInfo,
|
|
13
|
+
type QueryOptions,
|
|
14
|
+
} from '@cioky/ripple-query';
|
|
15
|
+
|
|
16
|
+
// ── Types ─────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/** Minimal Remult entity metadata shape we depend on. */
|
|
19
|
+
export interface EntityInfo {
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Remult repository interface — matches the subset `find`/`findFirst`/`count` use. */
|
|
24
|
+
export interface Repo<T> {
|
|
25
|
+
metadata: EntityInfo;
|
|
26
|
+
find(options?: Record<string, unknown>): Promise<T[]>;
|
|
27
|
+
findFirst(options?: Record<string, unknown>): Promise<T | undefined>;
|
|
28
|
+
count(options?: Record<string, unknown>): Promise<number>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extended options for the Remult query wrapper. */
|
|
32
|
+
export interface RemultQueryOptions extends QueryOptions {
|
|
33
|
+
/** Enable LiveQuery subscription — auto-invalidates on entity changes. */
|
|
34
|
+
liveQuery?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Key Derivation ────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a stable query key from a Remult repo entity, method, and
|
|
41
|
+
* filter params. Keys are sorted for deterministic serialization.
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* buildKey(Task, 'find', { where: { completed: true } })
|
|
45
|
+
* // → ['Task', 'find', { where: { completed: true } }]
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function buildKey<T>(
|
|
49
|
+
repo: Repo<T>,
|
|
50
|
+
method: string,
|
|
51
|
+
params?: Record<string, unknown>,
|
|
52
|
+
): QueryKey {
|
|
53
|
+
const key: QueryKey = [repo.metadata.name, method];
|
|
54
|
+
if (params !== undefined && Object.keys(params).length > 0) {
|
|
55
|
+
key.push(params);
|
|
56
|
+
}
|
|
57
|
+
return key;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Query Factory ─────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a cached Remult query with auto key derivation and optional
|
|
64
|
+
* LiveQuery subscription.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* const [tasks, info] = createRemultQuery(
|
|
68
|
+
* remult.repo(Task),
|
|
69
|
+
* 'find',
|
|
70
|
+
* { where: { completed: true } },
|
|
71
|
+
* { liveQuery: true }
|
|
72
|
+
* )
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function createRemultQuery<T>(
|
|
76
|
+
repo: Repo<T>,
|
|
77
|
+
method: 'find',
|
|
78
|
+
params?: Record<string, unknown>,
|
|
79
|
+
options?: RemultQueryOptions,
|
|
80
|
+
): [Tracked<T[] | undefined>, QueryInfo];
|
|
81
|
+
|
|
82
|
+
export function createRemultQuery<T>(
|
|
83
|
+
repo: Repo<T>,
|
|
84
|
+
method: 'findFirst',
|
|
85
|
+
params?: Record<string, unknown>,
|
|
86
|
+
options?: RemultQueryOptions,
|
|
87
|
+
): [Tracked<T | undefined>, QueryInfo];
|
|
88
|
+
|
|
89
|
+
export function createRemultQuery<T>(
|
|
90
|
+
repo: Repo<T>,
|
|
91
|
+
method: 'count',
|
|
92
|
+
params?: Record<string, unknown>,
|
|
93
|
+
options?: RemultQueryOptions,
|
|
94
|
+
): [Tracked<number | undefined>, QueryInfo];
|
|
95
|
+
|
|
96
|
+
export function createRemultQuery<T>(
|
|
97
|
+
repo: Repo<T>,
|
|
98
|
+
method: string,
|
|
99
|
+
params?: Record<string, unknown>,
|
|
100
|
+
options?: RemultQueryOptions,
|
|
101
|
+
): [Tracked<unknown>, QueryInfo] {
|
|
102
|
+
const key = buildKey(repo, method, params);
|
|
103
|
+
|
|
104
|
+
const fetcher = (): Promise<unknown> => {
|
|
105
|
+
switch (method) {
|
|
106
|
+
case 'find':
|
|
107
|
+
return repo.find(params);
|
|
108
|
+
case 'findFirst':
|
|
109
|
+
return repo.findFirst(params);
|
|
110
|
+
case 'count':
|
|
111
|
+
return repo.count(params);
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(`Unknown Remult method: ${method}`);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const { liveQuery, ...queryOpts } = options ?? {};
|
|
118
|
+
|
|
119
|
+
const result = query(key, fetcher, queryOpts);
|
|
120
|
+
|
|
121
|
+
if (liveQuery) {
|
|
122
|
+
subscribeEntity(repo.metadata.name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── LiveQuery Subscription ────────────────────────────────
|
|
129
|
+
|
|
130
|
+
const subscribedEntities = new Set<string>();
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Open a LiveQuery subscription for the given entity name.
|
|
134
|
+
* On any delta event (insert/update/delete), invalidate all
|
|
135
|
+
* cache entries prefixed by that entity name.
|
|
136
|
+
*
|
|
137
|
+
* Safe to call multiple times for the same entity — only one
|
|
138
|
+
* subscription is created per entity.
|
|
139
|
+
*/
|
|
140
|
+
export function subscribeEntity(entityName: string): void {
|
|
141
|
+
if (subscribedEntities.has(entityName)) return;
|
|
142
|
+
subscribedEntities.add(entityName);
|
|
143
|
+
|
|
144
|
+
// Remult LiveQuery via EventSource (SSE).
|
|
145
|
+
// The endpoint pattern: /api/live-query/<entityName>
|
|
146
|
+
// When a delta arrives, invalidate the entity cache prefix.
|
|
147
|
+
const url = `/api/live-query/${entityName}`;
|
|
148
|
+
const source = new EventSource(url);
|
|
149
|
+
|
|
150
|
+
source.addEventListener('insert', () => {
|
|
151
|
+
invalidateKeys([entityName]);
|
|
152
|
+
});
|
|
153
|
+
source.addEventListener('update', () => {
|
|
154
|
+
invalidateKeys([entityName]);
|
|
155
|
+
});
|
|
156
|
+
source.addEventListener('delete', () => {
|
|
157
|
+
invalidateKeys([entityName]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
source.addEventListener('error', () => {
|
|
161
|
+
// On disconnect, keep stale data (stale-while-revalidate).
|
|
162
|
+
// The next query() call will refetch when observed.
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Re-exports ────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export { invalidateKeys, invalidateAll } from '@cioky/ripple-query';
|
|
169
|
+
export type { QueryKey, QueryInfo } from '@cioky/ripple-query';
|