@awesomate/sdk 0.17.0 → 0.18.1

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/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  * Docs: https://hub.awesomate.ai/docs/sdk/
25
25
  */
26
26
  /** This package's version, sent to the hub with every server call. */
27
- export declare const VERSION = "0.17.0";
27
+ export declare const VERSION = "0.18.1";
28
28
  /** Augmented by the generated awesomate.d.ts, so each kind's rows are typed. */
29
29
  export interface Kinds {
30
30
  }
@@ -290,7 +290,7 @@ export declare class AwesomateClient {
290
290
  move: (from: string, to: string) => Promise<FileEntry>;
291
291
  /** Move a file from private/ or temp/ to the same place under public/, and answer its public_url. Anyone with the address can open it. */
292
292
  makePublic: (path: string) => Promise<FileEntry>;
293
- /** Move a file out of public/ into private/. Its public address stops serving it, though Cloudflare can keep showing the last copy for up to 4 hours. */
293
+ /** Move a file out of public/ into private/. Its public address stops serving it within seconds (a browser that already opened it may keep its own copy for a while). */
294
294
  makePrivate: (path: string) => Promise<FileEntry>;
295
295
  /** Move to the trash. Not erased, and no longer counted toward the Files space. */
296
296
  remove: (path: string) => Promise<{
@@ -830,7 +830,7 @@ export interface HereHandle {
830
830
  * stopped (on send or blur). Without another call, typing ends on its own after a few seconds.
831
831
  */
832
832
  typing(on: boolean): void;
833
- /** Close the record: the others stop seeing this person on it. */
833
+ /** Close the record: the others stop seeing this person on it once every part of the page that opened it has left. */
834
834
  leave(): void;
835
835
  }
836
836
  /** What live() calls. */
@@ -1007,7 +1007,9 @@ export declare class AwesomateAppClient {
1007
1007
  * Open a record (a job's conversation) for who-is-here: onPeople gets everyone else who has it
1008
1008
  * open with their tab in front, and who is typing, at first and on every change; the app's
1009
1009
  * assistant appears while it writes a reply. The hub checks this person may read the record, and
1010
- * keeps checking; onError hears a refusal. Shares the one live connection. Returns typing() and leave().
1010
+ * keeps checking; onError hears a refusal. Shares the one live connection. Two parts of a page (a
1011
+ * thread and a sidebar) can open the same record: each hears onPeople, and it stays open until both
1012
+ * have left. Returns typing() and leave().
1011
1013
  */
1012
1014
  here(recordId: string, onPeople: (people: HerePerson[]) => void, onError?: (err: AwesomateError) => void): HereHandle;
1013
1015
  }
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * Docs: https://hub.awesomate.ai/docs/sdk/
25
25
  */
26
26
  /** This package's version, sent to the hub with every server call. */
27
- export const VERSION = '0.17.0';
27
+ export const VERSION = '0.18.1';
28
28
  const DEFAULT_BASE = 'https://hub.awesomate.ai';
29
29
  const ERROR_CODES = ['unauthenticated', 'forbidden', 'not_found', 'validation', 'consent_blocked', 'rate_limited', 'conflict', 'unavailable'];
30
30
  /**
@@ -220,7 +220,7 @@ export class AwesomateClient {
220
220
  throw new AwesomateError('validation', 'Name a file inside the folder.', 0, 'path');
221
221
  return this.files.move(path, `public/${rest.join('/')}`);
222
222
  },
223
- /** Move a file out of public/ into private/. Its public address stops serving it, though Cloudflare can keep showing the last copy for up to 4 hours. */
223
+ /** Move a file out of public/ into private/. Its public address stops serving it within seconds (a browser that already opened it may keep its own copy for a while). */
224
224
  makePrivate: async (path) => {
225
225
  const [top, ...rest] = path.replace(/^\/+/, '').split('/');
226
226
  if (top !== 'public' || !rest.length)
@@ -799,7 +799,9 @@ export class AwesomateAppClient {
799
799
  * Open a record (a job's conversation) for who-is-here: onPeople gets everyone else who has it
800
800
  * open with their tab in front, and who is typing, at first and on every change; the app's
801
801
  * assistant appears while it writes a reply. The hub checks this person may read the record, and
802
- * keeps checking; onError hears a refusal. Shares the one live connection. Returns typing() and leave().
802
+ * keeps checking; onError hears a refusal. Shares the one live connection. Two parts of a page (a
803
+ * thread and a sidebar) can open the same record: each hears onPeople, and it stays open until both
804
+ * have left. Returns typing() and leave().
803
805
  */
804
806
  here(recordId, onPeople, onError) {
805
807
  return this.liveSocket().join(recordId, onPeople, onError);
@@ -842,32 +844,57 @@ class LiveSocket {
842
844
  if (this.ready && typeof d?.visibilityState === 'string')
843
845
  this.send({ type: 'presence', visible: d.visibilityState === 'visible' });
844
846
  }
845
- /** Records open with here(): who else is on them goes to onPeople. */
847
+ /**
848
+ * Records open with here(), one per record id however many parts of the page opened it: the hub
849
+ * hears `here on` once and `here off` when the last of them leaves, and every one of them gets
850
+ * onPeople. Typing is the person's, so one resend clock per record, shared by them all.
851
+ */
846
852
  rooms = new Map();
847
853
  get wanted() {
848
854
  return this.entries.size > 0 || this.rooms.size > 0;
849
855
  }
850
856
  join(id, onPeople, onError) {
851
- const room = { onPeople, onError, typingSentAt: 0 };
852
- this.rooms.set(id, room);
853
- if (this.ready)
854
- this.send({ type: 'here', id, on: true });
855
- else if (!this.ws && !this.connecting)
856
- void this.connect();
857
+ const listener = { onPeople, onError };
858
+ let room = this.rooms.get(id);
859
+ if (room) {
860
+ room.listeners.add(listener);
861
+ // Already open: the hub sends nothing new for a second join, so hand over who it last said was there.
862
+ const people = room.people;
863
+ if (people)
864
+ queueMicrotask(() => { if (this.rooms.get(id)?.listeners.has(listener))
865
+ onPeople(people); });
866
+ }
867
+ else {
868
+ room = { listeners: new Set([listener]), typingSentAt: 0, people: null };
869
+ this.rooms.set(id, room);
870
+ if (this.ready)
871
+ this.send({ type: 'here', id, on: true });
872
+ else if (!this.ws && !this.connecting)
873
+ void this.connect();
874
+ }
875
+ const open = () => {
876
+ const r = this.rooms.get(id);
877
+ return r?.listeners.has(listener) ? r : undefined;
878
+ };
857
879
  return {
858
880
  typing: (on) => {
859
- if (this.rooms.get(id) !== room || !this.ready)
881
+ const r = open();
882
+ if (!r || !this.ready)
860
883
  return;
861
884
  const now = Date.now();
862
- if (on && now - room.typingSentAt < TYPING_RESEND_MS)
885
+ if (on && now - r.typingSentAt < TYPING_RESEND_MS)
863
886
  return;
864
- if (!on && !room.typingSentAt)
887
+ if (!on && !r.typingSentAt)
865
888
  return;
866
- room.typingSentAt = on ? now : 0;
889
+ r.typingSentAt = on ? now : 0;
867
890
  this.send({ type: 'typing', id, on });
868
891
  },
869
892
  leave: () => {
870
- if (this.rooms.get(id) !== room)
893
+ const r = open();
894
+ if (!r)
895
+ return;
896
+ r.listeners.delete(listener);
897
+ if (r.listeners.size)
871
898
  return;
872
899
  this.rooms.delete(id);
873
900
  if (this.ready)
@@ -948,7 +975,13 @@ class LiveSocket {
948
975
  return;
949
976
  }
950
977
  if (f.type === 'people' && typeof f.id === 'string') {
951
- this.rooms.get(f.id)?.onPeople(Array.isArray(f.people) ? f.people : []);
978
+ const room = this.rooms.get(f.id);
979
+ if (!room)
980
+ return;
981
+ const people = Array.isArray(f.people) ? f.people : [];
982
+ room.people = people;
983
+ for (const l of [...room.listeners])
984
+ l.onPeople(people);
952
985
  return;
953
986
  }
954
987
  if (f.type === 'error' && typeof f.id === 'string' && f.id.startsWith('here:')) {
@@ -958,7 +991,9 @@ class LiveSocket {
958
991
  return;
959
992
  this.rooms.delete(id);
960
993
  const code = ERROR_CODES.includes(String(f.code)) ? f.code : 'unavailable';
961
- room.onError?.(new AwesomateError(code, String(f.message ?? 'Refused.'), 0, undefined, String(f.code)));
994
+ const err = new AwesomateError(code, String(f.message ?? 'Refused.'), 0, undefined, String(f.code));
995
+ for (const l of room.listeners)
996
+ l.onError?.(err);
962
997
  if (!this.wanted)
963
998
  this.shut();
964
999
  return;
@@ -1013,6 +1048,8 @@ class LiveSocket {
1013
1048
  if (this.retry)
1014
1049
  clearTimeout(this.retry);
1015
1050
  this.pinger = this.retry = null;
1051
+ for (const room of this.rooms.values())
1052
+ room.people = null;
1016
1053
  if (ws && ws.readyState <= 1) {
1017
1054
  try {
1018
1055
  ws.send(JSON.stringify({ type: 'bye' }));
@@ -1039,6 +1076,9 @@ class LiveSocket {
1039
1076
  if (this.pinger)
1040
1077
  clearInterval(this.pinger);
1041
1078
  this.pinger = null;
1079
+ // Who was there is the old connection's word: a part opening a record now waits for the new one.
1080
+ for (const room of this.rooms.values())
1081
+ room.people = null;
1042
1082
  if (this.stopped || !this.wanted)
1043
1083
  return;
1044
1084
  if (code === 4401) {
@@ -1060,7 +1100,8 @@ class LiveSocket {
1060
1100
  for (const e of all)
1061
1101
  e.handlers.onError?.(err);
1062
1102
  for (const r of rooms)
1063
- r.onError?.(err);
1103
+ for (const l of r.listeners)
1104
+ l.onError?.(err);
1064
1105
  this.shut();
1065
1106
  }
1066
1107
  shut() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "description": "Your own Awesomate data from Node and the browser: query contacts and app data with generated types, and sign your app's own users in",
5
5
  "license": "MIT",
6
6
  "type": "module",