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 CHANGED
@@ -329,10 +329,13 @@ PocketBase-style service for the collections themselves (admin):
329
329
 
330
330
  - `realtime.connect(opts)` — Connect to WebSocket
331
331
  - `realtime.disconnect()` — Disconnect
332
- - `realtime.subscribe(topic, callback)` — Low-level subscribe (topic like `collection:posts`)
332
+ - `realtime.refresh()` — Reconnect with the current auth token (auto-called on auth change)
333
+ - `realtime.setTokenProvider(fn)` — Register a token provider consulted at every connect
334
+ - `realtime.subscribe(topic, callback, joinPayload?)` — Low-level subscribe (any topic, e.g. `collection:posts` or custom `chat:room1`)
333
335
  - `realtime.unsubscribe(topic, callback?)` — Low-level unsubscribe
334
- - `collection(name).subscribe(callback, recordId?)` — Subscribe to record changes; callback receives `{ action, record }`; returns unsubscribe fn
335
- - `collection(name).unsubscribe(recordId?)` — Unsubscribe from record changes
336
+ - `realtime.unsubscribeByPrefix(prefix)` — Remove all subscriptions under a topic prefix
337
+ - `collection(name).subscribe(topicOrCallback?, callback?, options?)` — PocketBase-style record subscription; callback receives `{ action, record }` (full record); returns unsubscribe fn
338
+ - `collection(name).unsubscribe(topic?)` — Unsubscribe `'*'`, a record id, or all subscriptions
336
339
 
337
340
  ### CollectionService
338
341
 
@@ -346,8 +349,8 @@ Returned by `client.collection(name)`.
346
349
  - `create(data, options?)` — Create record
347
350
  - `update(id, data, options?)` — Update record
348
351
  - `delete(id, options?)` — Delete record
349
- - `subscribe(callback, recordId?)` — Subscribe to record changes (PocketBase-style)
350
- - `unsubscribe(recordId?)` — Unsubscribe
352
+ - `subscribe(topicOrCallback?, callback?, options?)` — PocketBase-style: `subscribe(cb)`, `subscribe('*', cb)`, `subscribe('id', cb)`, or with `{ expand }` options (legacy `subscribe(cb, recordId)` still works)
353
+ - `unsubscribe(topic?)` — `unsubscribe('*')` / `unsubscribe('id')` / `unsubscribe()` (all)
351
354
  - `authWithPassword(identity, password, options?)` — Login to this auth collection
352
355
  - `authRefresh(options?)` — Refresh token for this auth collection
353
356
  - `authMethods(options?)` — Get available auth methods
@@ -533,23 +536,56 @@ The SDK automatically refreshes expired auth tokens. When a token expires, the n
533
536
 
534
537
  ## Real-time Subscriptions
535
538
 
539
+ ### PocketBase-style `subscribe` / `unsubscribe`
540
+
541
+ Collection subscriptions use the same argument order as PocketBase. The
542
+ callback always receives the **full record** (all fields) — `select()`
543
+ projections only affect `getList`/`getOne`, never subscriptions:
544
+
536
545
  ```typescript
537
- // Subscribe to all changes in a collection (PocketBase-style: callback-first)
546
+ // Subscribe to all records (three equivalent forms)
538
547
  const off = client.collection('posts').subscribe((event) => {
539
548
  console.log(event.action); // 'create' | 'update' | 'delete'
540
- console.log(event.record);
549
+ console.log(event.record); // full record — all fields
541
550
  });
551
+ client.collection('posts').subscribe('*', (event) => { ... });
542
552
 
543
- // Subscribe to a specific record only
544
- client.collection('posts').subscribe((event) => { ... }, 'abc123');
553
+ // Subscribe to a single record
554
+ client.collection('posts').subscribe('RECORD_ID', (event) => { ... });
555
+
556
+ // With options — forwarded to the server channel join payload
557
+ // (available to onRealtimeSubscribeRequest hooks)
558
+ client.collection('posts').subscribe('*', (event) => { ... }, {
559
+ expand: 'author',
560
+ customKey: 'any extra key is forwarded',
561
+ });
545
562
 
546
563
  // Unsubscribe
547
- client.collection('posts').unsubscribe();
564
+ client.collection('posts').unsubscribe('*'); // wildcard only
565
+ client.collection('posts').unsubscribe('RECORD_ID'); // one record
566
+ client.collection('posts').unsubscribe(); // everything in this collection
548
567
 
549
568
  // ...or call the returned unsubscribe function for one-shot listeners:
550
569
  off();
551
570
  ```
552
571
 
572
+ The legacy callback-first form (`subscribe(cb, recordId)`) still works.
573
+ `headers` in the options object is accepted for PocketBase signature
574
+ compatibility but is not sent over the WebSocket.
575
+
576
+ ### Auth tokens are attached automatically
577
+
578
+ The WebSocket automatically uses the current auth token (`authStore.token`)
579
+ and **reconnects when auth changes** (login / logout / token refresh) — so
580
+ subscriptions to rule-protected collections and admin channels work without
581
+ any manual socket management:
582
+
583
+ ```typescript
584
+ await client.login('admin@example.com', 'secret');
585
+ // The socket reconnects with the new token; existing subscriptions re-join.
586
+ client.collection('private_feed').subscribe('*', (e) => { ... });
587
+ ```
588
+
553
589
  ### Anonymous / rule-based realtime
554
590
 
555
591
  Realtime subscriptions honor your API **and list rules** — matching PocketBase
@@ -565,6 +601,20 @@ const off = client.collection('public_feed').subscribe((e) => {
565
601
  });
566
602
  ```
567
603
 
604
+ ### Custom channels
605
+
606
+ Any topic string can be subscribed to via the low-level realtime service
607
+ (PocketBase behavior — anonymous joins allowed, broadcasts come from the
608
+ server side):
609
+
610
+ ```typescript
611
+ // Subscribe to an arbitrary topic
612
+ const off = client.realtime.subscribe('chat:room1', (event) => {
613
+ console.log(event.event, event.payload);
614
+ });
615
+ off(); // or client.realtime.unsubscribe('chat:room1');
616
+ ```
617
+
568
618
 
569
619
  ## Releasing (automatic)
570
620
 
package/dist/index.cjs CHANGED
@@ -817,25 +817,24 @@ var CollectionService = class _CollectionService {
817
817
  derived.fieldsPreset = this.fieldsPreset;
818
818
  return derived;
819
819
  }
820
- // ── Realtime Subscriptions (PocketBase-style) ──
821
- /**
822
- * Subscribe to realtime changes for this collection.
823
- * The event's `action` is one of `"create" | "update" | "delete"`.
824
- *
825
- * Access is governed by the collection's `listRule` (PocketBase semantics):
826
- * public collections allow anonymous subscriptions; other collections
827
- * require a matching logged-in user or superuser.
828
- *
829
- * @param callback Received on every record change.
830
- * @param recordId Optional — subscribe to a single record instead of `*`.
831
- * @returns A function that unsubscribes this callback.
832
- */
833
- subscribe(callback, recordId) {
820
+ subscribe(topicOrCallback, maybeCallback, options) {
834
821
  if (!this.realtime) {
835
822
  console.warn("[lazypock] No realtime service configured.");
836
823
  return () => {
837
824
  };
838
825
  }
826
+ let recordId;
827
+ let callback;
828
+ let joinPayload;
829
+ if (typeof topicOrCallback === "function") {
830
+ callback = topicOrCallback;
831
+ if (typeof maybeCallback === "string") recordId = maybeCallback;
832
+ } else {
833
+ callback = maybeCallback;
834
+ recordId = topicOrCallback === "*" ? void 0 : topicOrCallback;
835
+ const { headers: _ignored, ...rest } = options ?? {};
836
+ if (Object.keys(rest).length > 0) joinPayload = rest;
837
+ }
839
838
  const topic = "collection:" + this.collectionName + (recordId ? ":" + recordId : "");
840
839
  const handler = (raw) => {
841
840
  const record = raw.payload?.["record"] ?? {};
@@ -846,15 +845,23 @@ var CollectionService = class _CollectionService {
846
845
  });
847
846
  };
848
847
  this.realtime.ensureConnected();
849
- this.realtime.subscribe(topic, handler);
848
+ this.realtime.subscribe(topic, handler, joinPayload);
850
849
  return () => this.realtime?.unsubscribe(topic, handler);
851
850
  }
852
851
  /**
853
- * Unsubscribe all callbacks from this collection (or a specific record).
854
- * @param recordId Optional record id; omitting it unsubs everything.
852
+ * Unsubscribe from realtime changes (PocketBase-compatible):
853
+ * - `unsubscribe()` remove **all** subscriptions of this collection
854
+ * - `unsubscribe('*')` — remove wildcard subscriptions
855
+ * - `unsubscribe('RECORD_ID')` — remove that record's subscriptions
855
856
  */
856
857
  unsubscribe(recordId) {
857
- const topic = "collection:" + this.collectionName + (recordId ? ":" + recordId : "");
858
+ if (recordId === void 0) {
859
+ this.realtime?.unsubscribeByPrefix(
860
+ "collection:" + this.collectionName
861
+ );
862
+ return;
863
+ }
864
+ const topic = "collection:" + this.collectionName + (recordId === "*" ? "" : ":" + recordId);
858
865
  this.realtime?.unsubscribe(topic);
859
866
  }
860
867
  // ── Auth Collection Methods ──
@@ -921,6 +928,7 @@ var RealtimeService = class {
921
928
  this.reconnectAttempt = 0;
922
929
  this.maxReconnectDelay = 5e3;
923
930
  this.url = "";
931
+ this.tokenProvider = null;
924
932
  // ── Heartbeat ──
925
933
  this.heartbeatInterval = null;
926
934
  }
@@ -946,6 +954,32 @@ var RealtimeService = class {
946
954
  setUrl(url) {
947
955
  this.url = url;
948
956
  }
957
+ /**
958
+ * Register a token provider consulted at every (re)connect.
959
+ * When set, it takes precedence over the token passed to {@link connect}.
960
+ */
961
+ setTokenProvider(provider) {
962
+ this.tokenProvider = provider;
963
+ }
964
+ /**
965
+ * Reconnect the socket immediately with the current token.
966
+ * Called by the SDK when auth changes (login/logout/token refresh) so
967
+ * private-channel joins are authorized with the new credentials. No-op
968
+ * when the socket has never been opened and nothing is subscribed.
969
+ */
970
+ refresh() {
971
+ if (typeof WebSocket === "undefined") return;
972
+ if (!this.ws && this.subscriptions.size === 0) return;
973
+ this.clearReconnectTimer();
974
+ const ws = this.ws;
975
+ this.ws = null;
976
+ if (ws) {
977
+ ws.onclose = null;
978
+ ws.close();
979
+ }
980
+ this.reconnectAttempt = 0;
981
+ this.doConnect();
982
+ }
949
983
  /*
950
984
  * Ensure the socket is connected, then subscribe.
951
985
  * Used by collection-level convenience wrappers so a connection is opened
@@ -969,15 +1003,30 @@ var RealtimeService = class {
969
1003
  this.ws = null;
970
1004
  }
971
1005
  /**
972
- * Subscribe to a topic (e.g. "collection:posts" or "collection:posts:*").
1006
+ * Subscribe to a topic (e.g. "collection:posts" or "custom:chat-room").
973
1007
  * The backend Channel authorizes via listRule on join.
1008
+ *
1009
+ * @param joinPayload Optional payload forwarded with the channel join
1010
+ * (available to the server's join callback / hooks, e.g. `expand`).
974
1011
  */
975
- subscribe(topic, callback) {
1012
+ subscribe(topic, callback, joinPayload) {
976
1013
  const subs = this.subscriptions.get(topic) || [];
977
- subs.push({ topic, callback });
1014
+ subs.push({ topic, callback, joinPayload });
978
1015
  this.subscriptions.set(topic, subs);
979
1016
  if (this.ws?.readyState === WebSocket.OPEN) {
980
- this.joinTopic(topic);
1017
+ this.joinTopic(topic, joinPayload);
1018
+ }
1019
+ }
1020
+ /**
1021
+ * Remove all subscriptions for topics under `prefix` (the topic itself or
1022
+ * any topic starting with `prefix + ":"`). Used by collection-level
1023
+ * `unsubscribe()` to drop every subscription of a collection.
1024
+ */
1025
+ unsubscribeByPrefix(prefix) {
1026
+ for (const topic of [...this.subscriptions.keys()]) {
1027
+ if (topic === prefix || topic.startsWith(prefix + ":")) {
1028
+ this.subscriptions.delete(topic);
1029
+ }
981
1030
  }
982
1031
  }
983
1032
  /**
@@ -996,9 +1045,14 @@ var RealtimeService = class {
996
1045
  }
997
1046
  }
998
1047
  resubscribeAll() {
999
- for (const topic of this.subscriptions.keys()) {
1000
- if (this.ws?.readyState === WebSocket.OPEN) {
1001
- this.joinTopic(topic);
1048
+ const seen = /* @__PURE__ */ new Set();
1049
+ for (const entries of this.subscriptions.values()) {
1050
+ for (const entry of entries) {
1051
+ if (seen.has(entry.topic)) continue;
1052
+ seen.add(entry.topic);
1053
+ if (this.ws?.readyState === WebSocket.OPEN) {
1054
+ this.joinTopic(entry.topic, entry.joinPayload);
1055
+ }
1002
1056
  }
1003
1057
  }
1004
1058
  }
@@ -1010,8 +1064,9 @@ var RealtimeService = class {
1010
1064
  return;
1011
1065
  }
1012
1066
  let url = this.url;
1013
- if (this.token) {
1014
- url += (url.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(this.token);
1067
+ const token = this.tokenProvider ? this.tokenProvider() : this.token;
1068
+ if (token) {
1069
+ url += (url.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(token);
1015
1070
  }
1016
1071
  this.ws = new WebSocket(url);
1017
1072
  this.ws.onopen = () => {
@@ -1058,12 +1113,12 @@ var RealtimeService = class {
1058
1113
  }
1059
1114
  }
1060
1115
  }
1061
- joinTopic(topic) {
1116
+ joinTopic(topic, joinPayload) {
1062
1117
  const ref = this.nextRef();
1063
1118
  const msg = JSON.stringify({
1064
1119
  topic,
1065
1120
  event: "phx_join",
1066
- payload: {},
1121
+ payload: joinPayload ?? {},
1067
1122
  ref
1068
1123
  });
1069
1124
  this.ws?.send(msg);
@@ -1635,6 +1690,9 @@ var LazypockClient = class {
1635
1690
  if (!options.realtime) {
1636
1691
  this.realtime.setUrl(wsUrlFromBaseUrl(baseUrl));
1637
1692
  }
1693
+ this.realtime.setTokenProvider(() => this.authStore.token);
1694
+ this.authStore.onChange(() => this.realtime.refresh());
1695
+ this.authReady.then(() => this.realtime.refresh());
1638
1696
  this.files = new FilesService(this.http);
1639
1697
  this.collections = new CollectionsService(this.http, this.realtime);
1640
1698
  if (options.types?.schemas) {