lazypock 0.8.4 → 0.9.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 +60 -10
- package/dist/index.cjs +87 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -8
- package/dist/index.d.ts +65 -8
- package/dist/index.global.js +87 -29
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +87 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/collection.ts +77 -9
- package/src/index.ts +1 -1
- package/src/lazypock.ts +10 -0
- package/src/realtime.ts +79 -11
package/src/collection.ts
CHANGED
|
@@ -60,6 +60,23 @@ export interface RealtimeMessage {
|
|
|
60
60
|
/** Subscription callback for a collection's realtime events. */
|
|
61
61
|
export type RealtimeCallback = (e: RealtimeMessage) => void;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Options for {@link CollectionService.subscribe} (PocketBase-compatible).
|
|
65
|
+
*
|
|
66
|
+
* `expand` and any extra keys are forwarded to the server channel join
|
|
67
|
+
* payload (available to `onRealtimeSubscribeRequest` hooks). `headers` is
|
|
68
|
+
* accepted for PocketBase signature compatibility but has no effect over
|
|
69
|
+
* WebSocket — it is not forwarded.
|
|
70
|
+
*/
|
|
71
|
+
export interface RealtimeSubscribeOptions {
|
|
72
|
+
/** Fields to expand on the subscribed records (server join payload). */
|
|
73
|
+
expand?: string;
|
|
74
|
+
/** Accepted for PocketBase parity; not sent over the WebSocket. */
|
|
75
|
+
headers?: Record<string, string>;
|
|
76
|
+
/** Any extra keys are forwarded verbatim to the server join payload. */
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
/** Raw event passed by the low-level realtime service. */
|
|
64
81
|
interface RealtimeEventLike {
|
|
65
82
|
event: string;
|
|
@@ -505,23 +522,64 @@ export class CollectionService<T = ApiRecord, TData = never> {
|
|
|
505
522
|
|
|
506
523
|
/**
|
|
507
524
|
* Subscribe to realtime changes for this collection.
|
|
508
|
-
* The event's `action` is one of `"create" | "update" | "delete"
|
|
525
|
+
* The event's `action` is one of `"create" | "update" | "delete"` and the
|
|
526
|
+
* callback always receives the **full record** (all fields, regardless of
|
|
527
|
+
* any `select()` projection on the service).
|
|
509
528
|
*
|
|
510
529
|
* Access is governed by the collection's `listRule` (PocketBase semantics):
|
|
511
530
|
* public collections allow anonymous subscriptions; other collections
|
|
512
531
|
* require a matching logged-in user or superuser.
|
|
513
532
|
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
533
|
+
* PocketBase-compatible argument forms:
|
|
534
|
+
* - `subscribe(cb)` — all records of the collection
|
|
535
|
+
* - `subscribe('*', cb)` — all records (explicit wildcard)
|
|
536
|
+
* - `subscribe('RECORD_ID', cb)` — a single record
|
|
537
|
+
* - `subscribe('*' | 'RECORD_ID', cb, options)` — with join options
|
|
538
|
+
*
|
|
539
|
+
* Legacy form (callback first) is still accepted: `subscribe(cb, recordId)`.
|
|
540
|
+
*
|
|
516
541
|
* @returns A function that unsubscribes this callback.
|
|
517
542
|
*/
|
|
518
|
-
subscribe(callback: RealtimeCallback
|
|
543
|
+
subscribe(callback: RealtimeCallback): () => void;
|
|
544
|
+
subscribe(
|
|
545
|
+
recordId: string,
|
|
546
|
+
callback: RealtimeCallback,
|
|
547
|
+
options?: RealtimeSubscribeOptions,
|
|
548
|
+
): () => void;
|
|
549
|
+
/** @deprecated Use `subscribe(recordId, callback)` — PocketBase order. */
|
|
550
|
+
subscribe(callback: RealtimeCallback, recordId?: string): () => void;
|
|
551
|
+
subscribe(
|
|
552
|
+
topicOrCallback: string | RealtimeCallback,
|
|
553
|
+
maybeCallback?: RealtimeCallback | string,
|
|
554
|
+
options?: RealtimeSubscribeOptions,
|
|
555
|
+
): () => void {
|
|
519
556
|
if (!this.realtime) {
|
|
520
557
|
console.warn("[lazypock] No realtime service configured.");
|
|
521
558
|
return () => {};
|
|
522
559
|
}
|
|
560
|
+
|
|
561
|
+
// Normalise PocketBase-style args; also accept the legacy callback-first
|
|
562
|
+
// form (subscribe(cb, recordId)) for backward compatibility.
|
|
563
|
+
let recordId: string | undefined;
|
|
564
|
+
let callback: RealtimeCallback;
|
|
565
|
+
let joinPayload: Record<string, unknown> | undefined;
|
|
566
|
+
|
|
567
|
+
if (typeof topicOrCallback === "function") {
|
|
568
|
+
callback = topicOrCallback;
|
|
569
|
+
if (typeof maybeCallback === "string") recordId = maybeCallback;
|
|
570
|
+
} else {
|
|
571
|
+
callback = maybeCallback as RealtimeCallback;
|
|
572
|
+
recordId = topicOrCallback === "*" ? undefined : topicOrCallback;
|
|
573
|
+
// Forward subscribe options (minus HTTP-only headers) to the server
|
|
574
|
+
// channel join payload.
|
|
575
|
+
const { headers: _ignored, ...rest } = options ?? {};
|
|
576
|
+
if (Object.keys(rest).length > 0) joinPayload = rest;
|
|
577
|
+
}
|
|
578
|
+
|
|
523
579
|
const topic =
|
|
524
|
-
"collection:" +
|
|
580
|
+
"collection:" +
|
|
581
|
+
this.collectionName +
|
|
582
|
+
(recordId ? ":" + recordId : "");
|
|
525
583
|
const handler = (raw: RealtimeEventLike) => {
|
|
526
584
|
const record = (raw.payload?.["record"] ?? {}) as Record<string, unknown>;
|
|
527
585
|
callback({
|
|
@@ -531,17 +589,27 @@ export class CollectionService<T = ApiRecord, TData = never> {
|
|
|
531
589
|
});
|
|
532
590
|
};
|
|
533
591
|
this.realtime.ensureConnected();
|
|
534
|
-
this.realtime.subscribe(topic, handler as never);
|
|
592
|
+
this.realtime.subscribe(topic, handler as never, joinPayload);
|
|
535
593
|
return () => this.realtime?.unsubscribe(topic, handler as never);
|
|
536
594
|
}
|
|
537
595
|
|
|
538
596
|
/**
|
|
539
|
-
* Unsubscribe
|
|
540
|
-
*
|
|
597
|
+
* Unsubscribe from realtime changes (PocketBase-compatible):
|
|
598
|
+
* - `unsubscribe()` — remove **all** subscriptions of this collection
|
|
599
|
+
* - `unsubscribe('*')` — remove wildcard subscriptions
|
|
600
|
+
* - `unsubscribe('RECORD_ID')` — remove that record's subscriptions
|
|
541
601
|
*/
|
|
542
602
|
unsubscribe(recordId?: string): void {
|
|
603
|
+
if (recordId === undefined) {
|
|
604
|
+
this.realtime?.unsubscribeByPrefix(
|
|
605
|
+
"collection:" + this.collectionName,
|
|
606
|
+
);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
543
609
|
const topic =
|
|
544
|
-
"collection:" +
|
|
610
|
+
"collection:" +
|
|
611
|
+
this.collectionName +
|
|
612
|
+
(recordId === "*" ? "" : ":" + recordId);
|
|
545
613
|
this.realtime?.unsubscribe(topic);
|
|
546
614
|
}
|
|
547
615
|
|
package/src/index.ts
CHANGED
|
@@ -56,5 +56,5 @@ export type {
|
|
|
56
56
|
LazypockClientOptions,
|
|
57
57
|
} from "./lazypock";
|
|
58
58
|
export type { LazypockCollections } from "./client";
|
|
59
|
-
export type { RealtimeMessage, RealtimeCallback } from "./collection";
|
|
59
|
+
export type { RealtimeMessage, RealtimeCallback, RealtimeSubscribeOptions } from "./collection";
|
|
60
60
|
export type { CollectionsMessage } from "./collections";
|
package/src/lazypock.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type FieldKey,
|
|
29
29
|
} from "./types";
|
|
30
30
|
import { RealtimeService, wsUrlFromBaseUrl } from "./realtime";
|
|
31
|
+
import type { RealtimeConnectOpts, RealtimeTokenProvider } from "./realtime";
|
|
31
32
|
import {
|
|
32
33
|
FilesService,
|
|
33
34
|
getFileUrl,
|
|
@@ -69,6 +70,9 @@ export type {
|
|
|
69
70
|
SystemFields,
|
|
70
71
|
RequestOptions,
|
|
71
72
|
FileRecord,
|
|
73
|
+
// realtime
|
|
74
|
+
RealtimeConnectOpts,
|
|
75
|
+
RealtimeTokenProvider,
|
|
72
76
|
CollectionSchema,
|
|
73
77
|
SchemaField,
|
|
74
78
|
// schema-driven query typing
|
|
@@ -154,6 +158,12 @@ export class LazypockClient {
|
|
|
154
158
|
if (!options.realtime) {
|
|
155
159
|
this.realtime.setUrl(wsUrlFromBaseUrl(baseUrl));
|
|
156
160
|
}
|
|
161
|
+
// Keep the realtime socket authenticated with the current auth token
|
|
162
|
+
// and reconnect whenever auth changes (login/logout/token refresh) —
|
|
163
|
+
// PocketBase parity. Reconnect is a no-op until something subscribes.
|
|
164
|
+
this.realtime.setTokenProvider(() => this.authStore.token);
|
|
165
|
+
this.authStore.onChange(() => this.realtime.refresh());
|
|
166
|
+
this.authReady.then(() => this.realtime.refresh());
|
|
157
167
|
this.files = new FilesService(this.http);
|
|
158
168
|
this.collections = new CollectionsService(this.http, this.realtime);
|
|
159
169
|
if (options.types?.schemas) {
|
package/src/realtime.ts
CHANGED
|
@@ -16,6 +16,8 @@ interface RealtimeEvent {
|
|
|
16
16
|
interface SubEntry {
|
|
17
17
|
topic: string;
|
|
18
18
|
callback: (e: RealtimeEvent) => void;
|
|
19
|
+
/** Optional payload forwarded with the channel join (e.g. expand). */
|
|
20
|
+
joinPayload?: Record<string, unknown>;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export type RealtimeConnectOpts = {
|
|
@@ -25,6 +27,13 @@ export type RealtimeConnectOpts = {
|
|
|
25
27
|
token?: string;
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Provides the current auth token at connect/reconnect time.
|
|
32
|
+
* Wired by {@link LazypockClient} from its `authStore` so the socket is
|
|
33
|
+
* always authenticated with the latest token (PocketBase parity).
|
|
34
|
+
*/
|
|
35
|
+
export type RealtimeTokenProvider = () => string | undefined;
|
|
36
|
+
|
|
28
37
|
/**
|
|
29
38
|
* Derive a WebSocket URL from an HTTP base URL.
|
|
30
39
|
* http://localhost:4000/api → ws://localhost:4000/socket/websocket
|
|
@@ -67,6 +76,7 @@ export class RealtimeService {
|
|
|
67
76
|
|
|
68
77
|
private url: string = "";
|
|
69
78
|
private token: string | undefined;
|
|
79
|
+
private tokenProvider: RealtimeTokenProvider | null = null;
|
|
70
80
|
|
|
71
81
|
/** Whether the WebSocket is currently open. */
|
|
72
82
|
get isOpen(): boolean {
|
|
@@ -94,6 +104,35 @@ export class RealtimeService {
|
|
|
94
104
|
this.url = url;
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Register a token provider consulted at every (re)connect.
|
|
109
|
+
* When set, it takes precedence over the token passed to {@link connect}.
|
|
110
|
+
*/
|
|
111
|
+
setTokenProvider(provider: RealtimeTokenProvider): void {
|
|
112
|
+
this.tokenProvider = provider;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reconnect the socket immediately with the current token.
|
|
117
|
+
* Called by the SDK when auth changes (login/logout/token refresh) so
|
|
118
|
+
* private-channel joins are authorized with the new credentials. No-op
|
|
119
|
+
* when the socket has never been opened and nothing is subscribed.
|
|
120
|
+
*/
|
|
121
|
+
refresh(): void {
|
|
122
|
+
if (typeof WebSocket === "undefined") return;
|
|
123
|
+
if (!this.ws && this.subscriptions.size === 0) return;
|
|
124
|
+
this.clearReconnectTimer();
|
|
125
|
+
const ws = this.ws;
|
|
126
|
+
this.ws = null;
|
|
127
|
+
if (ws) {
|
|
128
|
+
// Suppress the auto-reconnect path for this intentional close.
|
|
129
|
+
ws.onclose = null;
|
|
130
|
+
ws.close();
|
|
131
|
+
}
|
|
132
|
+
this.reconnectAttempt = 0;
|
|
133
|
+
this.doConnect();
|
|
134
|
+
}
|
|
135
|
+
|
|
97
136
|
/*
|
|
98
137
|
* Ensure the socket is connected, then subscribe.
|
|
99
138
|
* Used by collection-level convenience wrappers so a connection is opened
|
|
@@ -120,16 +159,36 @@ export class RealtimeService {
|
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
/**
|
|
123
|
-
* Subscribe to a topic (e.g. "collection:posts" or "
|
|
162
|
+
* Subscribe to a topic (e.g. "collection:posts" or "custom:chat-room").
|
|
124
163
|
* The backend Channel authorizes via listRule on join.
|
|
164
|
+
*
|
|
165
|
+
* @param joinPayload Optional payload forwarded with the channel join
|
|
166
|
+
* (available to the server's join callback / hooks, e.g. `expand`).
|
|
125
167
|
*/
|
|
126
|
-
subscribe(
|
|
168
|
+
subscribe(
|
|
169
|
+
topic: string,
|
|
170
|
+
callback: (e: RealtimeEvent) => void,
|
|
171
|
+
joinPayload?: Record<string, unknown>,
|
|
172
|
+
): void {
|
|
127
173
|
const subs = this.subscriptions.get(topic) || [];
|
|
128
|
-
subs.push({ topic, callback });
|
|
174
|
+
subs.push({ topic, callback, joinPayload });
|
|
129
175
|
this.subscriptions.set(topic, subs);
|
|
130
176
|
|
|
131
177
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
132
|
-
this.joinTopic(topic);
|
|
178
|
+
this.joinTopic(topic, joinPayload);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Remove all subscriptions for topics under `prefix` (the topic itself or
|
|
184
|
+
* any topic starting with `prefix + ":"`). Used by collection-level
|
|
185
|
+
* `unsubscribe()` to drop every subscription of a collection.
|
|
186
|
+
*/
|
|
187
|
+
unsubscribeByPrefix(prefix: string): void {
|
|
188
|
+
for (const topic of [...this.subscriptions.keys()]) {
|
|
189
|
+
if (topic === prefix || topic.startsWith(prefix + ":")) {
|
|
190
|
+
this.subscriptions.delete(topic);
|
|
191
|
+
}
|
|
133
192
|
}
|
|
134
193
|
}
|
|
135
194
|
|
|
@@ -152,9 +211,14 @@ export class RealtimeService {
|
|
|
152
211
|
}
|
|
153
212
|
|
|
154
213
|
private resubscribeAll(): void {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
214
|
+
const seen = new Set<string>();
|
|
215
|
+
for (const entries of this.subscriptions.values()) {
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (seen.has(entry.topic)) continue;
|
|
218
|
+
seen.add(entry.topic);
|
|
219
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
220
|
+
this.joinTopic(entry.topic, entry.joinPayload);
|
|
221
|
+
}
|
|
158
222
|
}
|
|
159
223
|
}
|
|
160
224
|
}
|
|
@@ -168,11 +232,12 @@ export class RealtimeService {
|
|
|
168
232
|
}
|
|
169
233
|
|
|
170
234
|
let url = this.url;
|
|
171
|
-
|
|
235
|
+
const token = this.tokenProvider ? this.tokenProvider() : this.token;
|
|
236
|
+
if (token) {
|
|
172
237
|
url +=
|
|
173
238
|
(url.includes("?") ? "&" : "?") +
|
|
174
239
|
"token=" +
|
|
175
|
-
encodeURIComponent(
|
|
240
|
+
encodeURIComponent(token);
|
|
176
241
|
}
|
|
177
242
|
|
|
178
243
|
this.ws = new WebSocket(url);
|
|
@@ -232,13 +297,16 @@ export class RealtimeService {
|
|
|
232
297
|
}
|
|
233
298
|
}
|
|
234
299
|
|
|
235
|
-
private joinTopic(
|
|
300
|
+
private joinTopic(
|
|
301
|
+
topic: string,
|
|
302
|
+
joinPayload?: Record<string, unknown>,
|
|
303
|
+
): void {
|
|
236
304
|
const ref = this.nextRef();
|
|
237
305
|
// Phoenix V1 JSON Serializer expects a JSON object, not an array
|
|
238
306
|
const msg = JSON.stringify({
|
|
239
307
|
topic: topic,
|
|
240
308
|
event: "phx_join",
|
|
241
|
-
payload: {},
|
|
309
|
+
payload: joinPayload ?? {},
|
|
242
310
|
ref: ref,
|
|
243
311
|
});
|
|
244
312
|
this.ws?.send(msg);
|