lazypock 0.1.1 → 0.1.2

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/src/collection.ts CHANGED
@@ -3,7 +3,51 @@
3
3
 
4
4
  import type { HttpClient } from "./http";
5
5
  import type { AuthStore, AuthModel } from "./auth";
6
- import type { ApiRecord, ListResult, RequestOptions, CreateData, UpdateData } from "./types";
6
+ import type {
7
+ ApiRecord,
8
+ ListResult,
9
+ RequestOptions,
10
+ CreateData,
11
+ UpdateData,
12
+ } from "./types";
13
+ import type { RealtimeService } from "./realtime";
14
+
15
+ /**
16
+ * A realtime record-change event delivered to subscription callbacks.
17
+ * Mirrors PocketBase's RealtimeService result shape (`action` + `record`).
18
+ */
19
+ export interface RealtimeMessage {
20
+ action: "create" | "update" | "delete";
21
+ record: Record<string, unknown>;
22
+ topic?: string;
23
+ }
24
+
25
+ /** Subscription callback for a collection's realtime events. */
26
+ export type RealtimeCallback = (e: RealtimeMessage) => void;
27
+
28
+ /** Raw event passed by the low-level realtime service. */
29
+ interface RealtimeEventLike {
30
+ event: string;
31
+ topic: string;
32
+ payload?: Record<string, unknown>;
33
+ }
34
+
35
+ /** Map a raw event/action to a normalised create/update/delete action. */
36
+ function normalizeAction(
37
+ event: string,
38
+ rawAction?: unknown,
39
+ ): RealtimeMessage["action"] {
40
+ if (typeof rawAction === "string") {
41
+ const a = rawAction.toLowerCase();
42
+ if (a === "create" || a === "update" || a === "delete") {
43
+ return a;
44
+ }
45
+ }
46
+ if (event === "record_change") return "update";
47
+ if (event === "create" || event === "record_create") return "create";
48
+ if (event === "delete" || event === "record_delete") return "delete";
49
+ return "update";
50
+ }
7
51
 
8
52
  /**
9
53
  * Typed CRUD service for a single dynamic collection.
@@ -15,12 +59,19 @@ export class CollectionService<T = ApiRecord> {
15
59
  private http: HttpClient;
16
60
  private collectionName: string;
17
61
  private authStore?: AuthStore;
62
+ private realtime?: RealtimeService;
18
63
 
19
64
  /** @internal */
20
- constructor(http: HttpClient, collectionName: string, authStore?: AuthStore) {
65
+ constructor(
66
+ http: HttpClient,
67
+ collectionName: string,
68
+ authStore?: AuthStore,
69
+ realtime?: RealtimeService,
70
+ ) {
21
71
  this.http = http;
22
72
  this.collectionName = collectionName;
23
73
  this.authStore = authStore;
74
+ this.realtime = realtime;
24
75
  }
25
76
 
26
77
  private encodeId(id: string): string {
@@ -32,7 +83,10 @@ export class CollectionService<T = ApiRecord> {
32
83
  * @param params Query parameters including `filter`, `sort`, `page`, `perPage`, `expand`.
33
84
  * @param options Optional request options.
34
85
  */
35
- list(params?: Record<string, string>, options?: RequestOptions): Promise<ListResult<T> | null> {
86
+ list(
87
+ params?: Record<string, string>,
88
+ options?: RequestOptions,
89
+ ): Promise<ListResult<T> | null> {
36
90
  const qs = params ? "?" + new URLSearchParams(params).toString() : "";
37
91
  return this.http.get<ListResult<T>>(
38
92
  "/" + this.encodeId(this.collectionName) + qs,
@@ -58,7 +112,10 @@ export class CollectionService<T = ApiRecord> {
58
112
  * record type), excess/unknown fields are rejected at compile time.
59
113
  * @param options Optional request options.
60
114
  */
61
- create(data: T extends ApiRecord ? Record<string, unknown> : CreateData<T>, options?: RequestOptions): Promise<T | null> {
115
+ create(
116
+ data: T extends ApiRecord ? Record<string, unknown> : CreateData<T>,
117
+ options?: RequestOptions,
118
+ ): Promise<T | null> {
62
119
  return this.http.post<T>(
63
120
  "/" + this.encodeId(this.collectionName),
64
121
  data,
@@ -73,7 +130,11 @@ export class CollectionService<T = ApiRecord> {
73
130
  * must be a partial of `T` — unknown fields are rejected.
74
131
  * @param options Optional request options.
75
132
  */
76
- update(id: string, data: T extends ApiRecord ? Record<string, unknown> : UpdateData<T>, options?: RequestOptions): Promise<T | null> {
133
+ update(
134
+ id: string,
135
+ data: T extends ApiRecord ? Record<string, unknown> : UpdateData<T>,
136
+ options?: RequestOptions,
137
+ ): Promise<T | null> {
77
138
  return this.http.patch<T>(
78
139
  "/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
79
140
  data,
@@ -102,11 +163,12 @@ export class CollectionService<T = ApiRecord> {
102
163
  options?: RequestOptions,
103
164
  ): Promise<{ field: string; targetCollection: string }[] | null> {
104
165
  const data = await this.http.get<{
105
- fields?: { name: string; type: string; options?: Record<string, string> }[];
106
- }>(
107
- "/collections/" + this.encodeId(this.collectionName),
108
- options,
109
- );
166
+ fields?: {
167
+ name: string;
168
+ type: string;
169
+ options?: Record<string, string>;
170
+ }[];
171
+ }>("/collections/" + this.encodeId(this.collectionName), options);
110
172
  if (!data?.fields) return null;
111
173
  return data.fields
112
174
  .filter((f) => f.type === "relation" && f.options?.collection)
@@ -137,6 +199,50 @@ export class CollectionService<T = ApiRecord> {
137
199
  return this as unknown as CollectionService<TRecord>;
138
200
  }
139
201
 
202
+ // ── Realtime Subscriptions (PocketBase-style) ──
203
+
204
+ /**
205
+ * Subscribe to realtime changes for this collection.
206
+ * The event's `action` is one of `"create" | "update" | "delete"`.
207
+ *
208
+ * Access is governed by the collection's `listRule` (PocketBase semantics):
209
+ * public collections allow anonymous subscriptions; other collections
210
+ * require a matching logged-in user or superuser.
211
+ *
212
+ * @param callback Received on every record change.
213
+ * @param recordId Optional — subscribe to a single record instead of `*`.
214
+ * @returns A function that unsubscribes this callback.
215
+ */
216
+ subscribe(callback: RealtimeCallback, recordId?: string): () => void {
217
+ if (!this.realtime) {
218
+ console.warn("[lazypock] No realtime service configured.");
219
+ return () => {};
220
+ }
221
+ const topic =
222
+ "collection:" + this.collectionName + (recordId ? ":" + recordId : "");
223
+ const handler = (raw: RealtimeEventLike) => {
224
+ const record = (raw.payload?.["record"] ?? {}) as Record<string, unknown>;
225
+ callback({
226
+ action: normalizeAction(raw.event, raw.payload?.["action"]),
227
+ record,
228
+ topic: raw.topic,
229
+ });
230
+ };
231
+ this.realtime.ensureConnected();
232
+ this.realtime.subscribe(topic, handler as never);
233
+ return () => this.realtime?.unsubscribe(topic, handler as never);
234
+ }
235
+
236
+ /**
237
+ * Unsubscribe all callbacks from this collection (or a specific record).
238
+ * @param recordId Optional record id; omitting it unsubs everything.
239
+ */
240
+ unsubscribe(recordId?: string): void {
241
+ const topic =
242
+ "collection:" + this.collectionName + (recordId ? ":" + recordId : "");
243
+ this.realtime?.unsubscribe(topic);
244
+ }
245
+
140
246
  // ── Auth Collection Methods ──
141
247
 
142
248
  /**
@@ -147,7 +253,9 @@ export class CollectionService<T = ApiRecord> {
147
253
  identity: string,
148
254
  password: string,
149
255
  options?: RequestOptions,
150
- ): Promise<({ token: string; record: ApiRecord } & Record<string, unknown>) | null> {
256
+ ): Promise<
257
+ ({ token: string; record: ApiRecord } & Record<string, unknown>) | null
258
+ > {
151
259
  const data = await this.http.post<
152
260
  { token: string; record: ApiRecord } & Record<string, unknown>
153
261
  >(
@@ -168,7 +276,9 @@ export class CollectionService<T = ApiRecord> {
168
276
  */
169
277
  async authRefresh(
170
278
  options?: RequestOptions,
171
- ): Promise<({ token: string; record: ApiRecord } & Record<string, unknown>) | null> {
279
+ ): Promise<
280
+ ({ token: string; record: ApiRecord } & Record<string, unknown>) | null
281
+ > {
172
282
  const data = await this.http.post<
173
283
  { token: string; record: ApiRecord } & Record<string, unknown>
174
284
  >(
@@ -186,6 +296,8 @@ export class CollectionService<T = ApiRecord> {
186
296
  /**
187
297
  * Get available auth methods for this collection.
188
298
  */
299
+ // ── end Realtime ──
300
+
189
301
  async authMethods(
190
302
  options?: RequestOptions,
191
303
  ): Promise<Record<string, unknown> | null> {
package/src/index.ts CHANGED
@@ -41,3 +41,4 @@ export type {
41
41
  LazypockClientOptions,
42
42
  } from "./lazypock";
43
43
  export type { LazypockCollections } from "./client";
44
+ export type { RealtimeMessage, RealtimeCallback } from "./collection";
package/src/lazypock.ts CHANGED
@@ -109,6 +109,10 @@ export class LazypockClient {
109
109
  options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
110
110
  this.http = new HttpClient(baseUrl, this.authStore);
111
111
  this.realtime = options.realtime ?? new RealtimeService();
112
+ // Cache the socket URL so collection-level subscribe() can auto-connect.
113
+ if (!options.realtime) {
114
+ this.realtime.setUrl(wsUrlFromBaseUrl(baseUrl));
115
+ }
112
116
  this.files = new FilesService(this.http);
113
117
  if (options.types?.schemas) {
114
118
  this.schemaByName = new Map(
@@ -131,7 +135,12 @@ export class LazypockClient {
131
135
  collection(name: string): CollectionService<unknown> {
132
136
  let svc = this.collectionCache.get(name);
133
137
  if (!svc) {
134
- svc = new CollectionService(this.http, name, this.authStore);
138
+ svc = new CollectionService(
139
+ this.http,
140
+ name,
141
+ this.authStore,
142
+ this.realtime,
143
+ );
135
144
  this.collectionCache.set(name, svc);
136
145
  }
137
146
  return svc;
package/src/realtime.ts CHANGED
@@ -68,6 +68,44 @@ export class RealtimeService {
68
68
  private url: string = "";
69
69
  private token: string | undefined;
70
70
 
71
+ /** Whether the WebSocket is currently open. */
72
+ get isOpen(): boolean {
73
+ return this.ws?.readyState === WebSocket.OPEN;
74
+ }
75
+
76
+ /**
77
+ * The most recently used WebSocket URL (set on {@link connect}).
78
+ * Useful for SDK convenience methods that auto-connect before subscribing.
79
+ */
80
+ get lastUrl(): string {
81
+ return this.url;
82
+ }
83
+
84
+ /** The auth token configured for this connection (set on connect). */
85
+ get lastToken(): string | undefined {
86
+ return this.token;
87
+ }
88
+
89
+ /**
90
+ * Set the socket URL. Useful before subscribing so the SDK can
91
+ * auto-connect on the first {@link subscribe}.
92
+ */
93
+ setUrl(url: string): void {
94
+ this.url = url;
95
+ }
96
+
97
+ /*
98
+ * Ensure the socket is connected, then subscribe.
99
+ * Used by collection-level convenience wrappers so a connection is opened
100
+ * automatically on the first subscribe (matching PocketBase behaviour —
101
+ * works for anonymous/public collections too).
102
+ */
103
+ ensureConnected(): void {
104
+ if (this.isOpen || !this.url) return;
105
+ if (typeof WebSocket === "undefined") return;
106
+ this.doConnect();
107
+ }
108
+
71
109
  connect(opts: RealtimeConnectOpts): void {
72
110
  this.url = opts.url;
73
111
  this.token = opts.token;