@liveblocks/client 0.14.0-beta.1 → 0.14.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/lib/esm/room.js CHANGED
@@ -8,7 +8,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { getTreesDiffOperations, isSameNodeOrChildOf, remove } from "./utils";
11
- import auth, { parseToken } from "./authentication";
12
11
  import { ClientMessageType, ServerMessageType, OpType, } from "./live";
13
12
  import { LiveMap } from "./LiveMap";
14
13
  import { LiveObject } from "./LiveObject";
@@ -36,6 +35,9 @@ function makeOthers(presenceMap) {
36
35
  get count() {
37
36
  return array.length;
38
37
  },
38
+ [Symbol.iterator]() {
39
+ return array[Symbol.iterator]();
40
+ },
39
41
  map(callback) {
40
42
  return array.map(callback);
41
43
  },
@@ -50,16 +52,12 @@ function log(...params) {
50
52
  }
51
53
  export function makeStateMachine(state, context, mockedEffects) {
52
54
  const effects = mockedEffects || {
53
- authenticate() {
55
+ authenticate(auth, createWebSocket) {
54
56
  return __awaiter(this, void 0, void 0, function* () {
55
57
  try {
56
- const token = yield auth(context.authEndpoint, context.room, context.publicApiKey);
58
+ const { token } = yield auth(context.room);
57
59
  const parsedToken = parseToken(token);
58
- const socket = new WebSocket(`${context.liveblocksServer}/?token=${token}`);
59
- socket.addEventListener("message", onMessage);
60
- socket.addEventListener("open", onOpen);
61
- socket.addEventListener("close", onClose);
62
- socket.addEventListener("error", onError);
60
+ const socket = createWebSocket(token);
63
61
  authenticationSuccess(parsedToken, socket);
64
62
  }
65
63
  catch (er) {
@@ -162,6 +160,7 @@ export function makeStateMachine(state, context, mockedEffects) {
162
160
  function load(items) {
163
161
  const [root, parentToChildren] = buildRootAndParentToChildren(items);
164
162
  return LiveObject._deserialize(root, parentToChildren, {
163
+ getItem,
165
164
  addItem,
166
165
  deleteItem,
167
166
  generateId,
@@ -337,31 +336,31 @@ export function makeStateMachine(state, context, mockedEffects) {
337
336
  }
338
337
  case OpType.CreateObject: {
339
338
  const parent = state.items.get(op.parentId);
340
- if (parent == null || getItem(op.id) != null) {
339
+ if (parent == null) {
341
340
  return { modified: false };
342
341
  }
343
- return parent._attachChild(op.id, op.parentKey, new LiveObject(op.data), isLocal);
342
+ return parent._attachChild(op.id, op.parentKey, new LiveObject(op.data), op.opId, isLocal);
344
343
  }
345
344
  case OpType.CreateList: {
346
345
  const parent = state.items.get(op.parentId);
347
- if (parent == null || getItem(op.id) != null) {
346
+ if (parent == null) {
348
347
  return { modified: false };
349
348
  }
350
- return parent._attachChild(op.id, op.parentKey, new LiveList(), isLocal);
349
+ return parent._attachChild(op.id, op.parentKey, new LiveList(), op.opId, isLocal);
351
350
  }
352
351
  case OpType.CreateRegister: {
353
352
  const parent = state.items.get(op.parentId);
354
- if (parent == null || getItem(op.id) != null) {
353
+ if (parent == null) {
355
354
  return { modified: false };
356
355
  }
357
- return parent._attachChild(op.id, op.parentKey, new LiveRegister(op.data), isLocal);
356
+ return parent._attachChild(op.id, op.parentKey, new LiveRegister(op.data), op.opId, isLocal);
358
357
  }
359
358
  case OpType.CreateMap: {
360
359
  const parent = state.items.get(op.parentId);
361
- if (parent == null || getItem(op.id) != null) {
360
+ if (parent == null) {
362
361
  return { modified: false };
363
362
  }
364
- return parent._attachChild(op.id, op.parentKey, new LiveMap(), isLocal);
363
+ return parent._attachChild(op.id, op.parentKey, new LiveMap(), op.opId, isLocal);
365
364
  }
366
365
  }
367
366
  return { modified: false };
@@ -408,15 +407,14 @@ See v0.13 release notes for more information.
408
407
  : null;
409
408
  }
410
409
  function connect() {
411
- if (typeof window === "undefined") {
412
- return;
413
- }
414
410
  if (state.connection.state !== "closed" &&
415
411
  state.connection.state !== "unavailable") {
416
412
  return null;
417
413
  }
414
+ const auth = prepareAuthEndpoint(context.authentication, context.fetchPolyfill);
415
+ const createWebSocket = prepareCreateWebSocket(context.liveblocksServer, context.WebSocketPolyfill);
418
416
  updateConnection({ state: "authenticating" });
419
- effects.authenticate();
417
+ effects.authenticate(auth, createWebSocket);
420
418
  }
421
419
  function updatePresence(overrides, options) {
422
420
  const oldValues = {};
@@ -443,6 +441,10 @@ See v0.13 release notes for more information.
443
441
  }
444
442
  }
445
443
  function authenticationSuccess(token, socket) {
444
+ socket.addEventListener("message", onMessage);
445
+ socket.addEventListener("open", onOpen);
446
+ socket.addEventListener("close", onClose);
447
+ socket.addEventListener("error", onError);
446
448
  updateConnection({
447
449
  state: "connecting",
448
450
  id: token.actor,
@@ -453,7 +455,9 @@ See v0.13 release notes for more information.
453
455
  state.socket = socket;
454
456
  }
455
457
  function authenticationFailure(error) {
456
- console.error(error);
458
+ if (process.env.NODE_ENV !== "production") {
459
+ console.error("Call to authentication endpoint failed", error);
460
+ }
457
461
  updateConnection({ state: "unavailable" });
458
462
  state.numberOfRetry++;
459
463
  state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
@@ -620,14 +624,20 @@ See v0.13 release notes for more information.
620
624
  updateConnection({ state: "failed" });
621
625
  const error = new LiveblocksError(event.reason, event.code);
622
626
  for (const listener of state.listeners.error) {
623
- console.error(`Liveblocks WebSocket connection closed. Reason: ${error.message} (code: ${error.code})`);
627
+ if (process.env.NODE_ENV !== "production") {
628
+ console.error(`Connection to Liveblocks websocket server closed. Reason: ${error.message} (code: ${error.code})`);
629
+ }
624
630
  listener(error);
625
631
  }
626
632
  }
627
633
  else if (event.wasClean === false) {
628
- updateConnection({ state: "unavailable" });
629
634
  state.numberOfRetry++;
630
- state.timeoutHandles.reconnect = effects.scheduleReconnect(getRetryDelay());
635
+ const delay = getRetryDelay();
636
+ if (process.env.NODE_ENV !== "production") {
637
+ console.warn(`Connection to Liveblocks websocket server closed (code: ${event.code}). Retrying in ${delay}ms.`);
638
+ }
639
+ updateConnection({ state: "unavailable" });
640
+ state.timeoutHandles.reconnect = effects.scheduleReconnect(delay);
631
641
  }
632
642
  else {
633
643
  updateConnection({ state: "closed" });
@@ -668,7 +678,7 @@ See v0.13 release notes for more information.
668
678
  }
669
679
  clearTimeout(state.timeoutHandles.pongTimeout);
670
680
  state.timeoutHandles.pongTimeout = effects.schedulePongTimeout();
671
- if (state.socket.readyState === WebSocket.OPEN) {
681
+ if (state.socket.readyState === state.socket.OPEN) {
672
682
  state.socket.send("ping");
673
683
  }
674
684
  }
@@ -715,7 +725,7 @@ See v0.13 release notes for more information.
715
725
  state.offlineOperations.set(op.opId, op);
716
726
  });
717
727
  }
718
- if (state.socket == null || state.socket.readyState !== WebSocket.OPEN) {
728
+ if (state.socket == null || state.socket.readyState !== state.socket.OPEN) {
719
729
  state.buffer.storageOperations = [];
720
730
  return;
721
731
  }
@@ -791,8 +801,10 @@ See v0.13 release notes for more information.
791
801
  function getOthers() {
792
802
  return state.others;
793
803
  }
794
- function broadcastEvent(event) {
795
- if (state.socket == null) {
804
+ function broadcastEvent(event, options = {
805
+ shouldQueueEventIfNotReady: false,
806
+ }) {
807
+ if (state.socket == null && options.shouldQueueEventIfNotReady == false) {
796
808
  return;
797
809
  }
798
810
  state.buffer.messages.push({
@@ -917,7 +929,6 @@ See v0.13 release notes for more information.
917
929
  }
918
930
  return {
919
931
  // Internal
920
- onOpen,
921
932
  onClose,
922
933
  onMessage,
923
934
  authenticationSuccess,
@@ -1005,26 +1016,9 @@ export function defaultState(me, defaultStorageRoot) {
1005
1016
  offlineOperations: new Map(),
1006
1017
  };
1007
1018
  }
1008
- export function createRoom(name, options) {
1009
- const throttleDelay = options.throttle || 100;
1010
- const liveblocksServer = options.liveblocksServer || "wss://liveblocks.net/v5";
1011
- let authEndpoint;
1012
- if (options.authEndpoint) {
1013
- authEndpoint = options.authEndpoint;
1014
- }
1015
- else {
1016
- const publicAuthorizeEndpoint = options.publicAuthorizeEndpoint ||
1017
- "https://liveblocks.io/api/public/authorize";
1018
- authEndpoint = publicAuthorizeEndpoint;
1019
- }
1019
+ export function createRoom(options, context) {
1020
1020
  const state = defaultState(options.defaultPresence, options.defaultStorageRoot);
1021
- const machine = makeStateMachine(state, {
1022
- throttleDelay,
1023
- liveblocksServer,
1024
- authEndpoint,
1025
- room: name,
1026
- publicApiKey: options.publicApiKey,
1027
- });
1021
+ const machine = makeStateMachine(state, context);
1028
1022
  const room = {
1029
1023
  /////////////
1030
1024
  // Core //
@@ -1068,3 +1062,76 @@ class LiveblocksError extends Error {
1068
1062
  this.code = code;
1069
1063
  }
1070
1064
  }
1065
+ function parseToken(token) {
1066
+ const tokenParts = token.split(".");
1067
+ if (tokenParts.length !== 3) {
1068
+ throw new Error(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`);
1069
+ }
1070
+ const data = JSON.parse(atob(tokenParts[1]));
1071
+ if (typeof data.actor !== "number") {
1072
+ throw new Error(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`);
1073
+ }
1074
+ return data;
1075
+ }
1076
+ function prepareCreateWebSocket(liveblocksServer, WebSocketPolyfill) {
1077
+ if (typeof window === "undefined" && WebSocketPolyfill == null) {
1078
+ throw new Error("To use Liveblocks client in a non-dom environment, you need to provide a WebSocket polyfill.");
1079
+ }
1080
+ const ws = WebSocketPolyfill || WebSocket;
1081
+ return (token) => {
1082
+ return new ws(`${liveblocksServer}/?token=${token}`);
1083
+ };
1084
+ }
1085
+ function prepareAuthEndpoint(authentication, fetchPolyfill) {
1086
+ if (authentication.type === "public") {
1087
+ if (typeof window === "undefined" && fetchPolyfill == null) {
1088
+ throw new Error("To use Liveblocks client in a non-dom environment with a publicApiKey, you need to provide a fetch polyfill.");
1089
+ }
1090
+ return (room) => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
1091
+ room,
1092
+ publicApiKey: authentication.publicApiKey,
1093
+ });
1094
+ }
1095
+ if (authentication.type === "private") {
1096
+ if (typeof window === "undefined" && fetchPolyfill == null) {
1097
+ throw new Error("To use Liveblocks client in a non-dom environment with a url as auth endpoint, you need to provide a fetch polyfill.");
1098
+ }
1099
+ return (room) => fetchAuthEndpoint(fetchPolyfill || fetch, authentication.url, {
1100
+ room,
1101
+ });
1102
+ }
1103
+ if (authentication.type === "custom") {
1104
+ return authentication.callback;
1105
+ }
1106
+ throw new Error("Internal error. Unexpected authentication type");
1107
+ }
1108
+ function fetchAuthEndpoint(fetch, endpoint, body) {
1109
+ return __awaiter(this, void 0, void 0, function* () {
1110
+ const res = yield fetch(endpoint, {
1111
+ method: "POST",
1112
+ headers: {
1113
+ "Content-Type": "application/json",
1114
+ },
1115
+ body: JSON.stringify(body),
1116
+ });
1117
+ if (!res.ok) {
1118
+ throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
1119
+ }
1120
+ let authResponse = null;
1121
+ try {
1122
+ authResponse = yield res.json();
1123
+ }
1124
+ catch (er) {
1125
+ throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
1126
+ }
1127
+ if (typeof authResponse.token !== "string") {
1128
+ throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
1129
+ }
1130
+ return authResponse;
1131
+ });
1132
+ }
1133
+ class AuthenticationError extends Error {
1134
+ constructor(message) {
1135
+ super(message);
1136
+ }
1137
+ }
@@ -28,6 +28,14 @@ export declare type LiveListUpdates<TItem = any> = {
28
28
  type: "LiveList";
29
29
  node: LiveList<TItem>;
30
30
  };
31
+ export declare type BroadcastOptions = {
32
+ /**
33
+ * Whether or not event is queued if the connection is currently closed.
34
+ *
35
+ * ❗ We are not sure if we want to support this option in the future so it might be deprecated to be replaced by something else
36
+ */
37
+ shouldQueueEventIfNotReady: boolean;
38
+ };
31
39
  export declare type StorageUpdate = LiveMapUpdates | LiveObjectUpdates | LiveListUpdates;
32
40
  export declare type StorageCallback = (updates: StorageUpdate[]) => void;
33
41
  export declare type Client = {
@@ -65,6 +73,10 @@ export interface Others<TPresence extends Presence = Presence> {
65
73
  * Number of other users in the room.
66
74
  */
67
75
  readonly count: number;
76
+ /**
77
+ * Returns a new Iterator object that contains the users.
78
+ */
79
+ [Symbol.iterator](): IterableIterator<User<TPresence>>;
68
80
  /**
69
81
  * Returns the array of connected users in room.
70
82
  */
@@ -109,6 +121,8 @@ export declare type AuthEndpoint = string | AuthEndpointCallback;
109
121
  */
110
122
  export declare type ClientOptions = {
111
123
  throttle?: number;
124
+ fetchPolyfill?: any;
125
+ WebSocketPolyfill?: any;
112
126
  } & ({
113
127
  publicApiKey: string;
114
128
  authEndpoint?: never;
@@ -119,6 +133,17 @@ export declare type ClientOptions = {
119
133
  export declare type AuthorizeResponse = {
120
134
  token: string;
121
135
  };
136
+ export declare type Authentication = {
137
+ type: "public";
138
+ publicApiKey: string;
139
+ url: string;
140
+ } | {
141
+ type: "private";
142
+ url: string;
143
+ } | {
144
+ type: "custom";
145
+ callback: (room: string) => Promise<AuthorizeResponse>;
146
+ };
122
147
  declare type ConnectionState = "closed" | "authenticating" | "unavailable" | "failed" | "open" | "connecting";
123
148
  export declare type Connection = {
124
149
  state: "closed" | "authenticating" | "unavailable" | "failed";
@@ -423,7 +448,7 @@ export declare type Room = {
423
448
  * }
424
449
  * });
425
450
  */
426
- broadcastEvent: (event: any) => void;
451
+ broadcastEvent: (event: any, options?: BroadcastOptions) => void;
427
452
  /**
428
453
  * Get the room's storage asynchronously.
429
454
  * The storage's root is a {@link LiveObject}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/client",
3
- "version": "0.14.0-beta.1",
3
+ "version": "0.14.2",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -29,9 +29,13 @@
29
29
  "@babel/preset-env": "^7.12.16",
30
30
  "@babel/preset-typescript": "^7.12.16",
31
31
  "@types/jest": "^26.0.21",
32
+ "@types/node-fetch": "^2.6.1",
33
+ "@types/ws": "^8.2.2",
32
34
  "babel-jest": "^26.6.3",
33
35
  "jest": "^26.6.3",
34
- "typescript": "^4.4.0"
36
+ "node-fetch": "2.6.7",
37
+ "typescript": "^4.4.0",
38
+ "ws": "^8.5.0"
35
39
  },
36
40
  "repository": {
37
41
  "type": "git",
@@ -1,3 +0,0 @@
1
- import { AuthEndpoint, AuthenticationToken } from "./types";
2
- export default function auth(endpoint: AuthEndpoint, room: string, publicApiKey?: string): Promise<string>;
3
- export declare function parseToken(token: string): AuthenticationToken;
@@ -1,71 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.parseToken = void 0;
13
- function fetchAuthorize(endpoint, room, publicApiKey) {
14
- return __awaiter(this, void 0, void 0, function* () {
15
- const res = yield fetch(endpoint, {
16
- method: "POST",
17
- headers: {
18
- "Content-Type": "application/json",
19
- },
20
- body: JSON.stringify({
21
- room,
22
- publicApiKey,
23
- }),
24
- });
25
- if (!res.ok) {
26
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
27
- }
28
- let authResponse = null;
29
- try {
30
- authResponse = yield res.json();
31
- }
32
- catch (er) {
33
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
34
- }
35
- if (typeof authResponse.token !== "string") {
36
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
37
- }
38
- return authResponse.token;
39
- });
40
- }
41
- function auth(endpoint, room, publicApiKey) {
42
- return __awaiter(this, void 0, void 0, function* () {
43
- if (typeof endpoint === "string") {
44
- return fetchAuthorize(endpoint, room, publicApiKey);
45
- }
46
- if (typeof endpoint === "function") {
47
- const { token } = yield endpoint(room);
48
- // TODO: Validation
49
- return token;
50
- }
51
- throw new Error("Authentication error. Liveblocks could not parse the response of your authentication endpoint");
52
- });
53
- }
54
- exports.default = auth;
55
- class AuthenticationError extends Error {
56
- constructor(message) {
57
- super(message);
58
- }
59
- }
60
- function parseToken(token) {
61
- const tokenParts = token.split(".");
62
- if (tokenParts.length !== 3) {
63
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`);
64
- }
65
- const data = JSON.parse(atob(tokenParts[1]));
66
- if (typeof data.actor !== "number") {
67
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`);
68
- }
69
- return data;
70
- }
71
- exports.parseToken = parseToken;
@@ -1,3 +0,0 @@
1
- import { AuthEndpoint, AuthenticationToken } from "./types";
2
- export default function auth(endpoint: AuthEndpoint, room: string, publicApiKey?: string): Promise<string>;
3
- export declare function parseToken(token: string): AuthenticationToken;
@@ -1,66 +0,0 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- function fetchAuthorize(endpoint, room, publicApiKey) {
11
- return __awaiter(this, void 0, void 0, function* () {
12
- const res = yield fetch(endpoint, {
13
- method: "POST",
14
- headers: {
15
- "Content-Type": "application/json",
16
- },
17
- body: JSON.stringify({
18
- room,
19
- publicApiKey,
20
- }),
21
- });
22
- if (!res.ok) {
23
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
24
- }
25
- let authResponse = null;
26
- try {
27
- authResponse = yield res.json();
28
- }
29
- catch (er) {
30
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
31
- }
32
- if (typeof authResponse.token !== "string") {
33
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication "${endpoint}"`);
34
- }
35
- return authResponse.token;
36
- });
37
- }
38
- export default function auth(endpoint, room, publicApiKey) {
39
- return __awaiter(this, void 0, void 0, function* () {
40
- if (typeof endpoint === "string") {
41
- return fetchAuthorize(endpoint, room, publicApiKey);
42
- }
43
- if (typeof endpoint === "function") {
44
- const { token } = yield endpoint(room);
45
- // TODO: Validation
46
- return token;
47
- }
48
- throw new Error("Authentication error. Liveblocks could not parse the response of your authentication endpoint");
49
- });
50
- }
51
- class AuthenticationError extends Error {
52
- constructor(message) {
53
- super(message);
54
- }
55
- }
56
- export function parseToken(token) {
57
- const tokenParts = token.split(".");
58
- if (tokenParts.length !== 3) {
59
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`);
60
- }
61
- const data = JSON.parse(atob(tokenParts[1]));
62
- if (typeof data.actor !== "number") {
63
- throw new AuthenticationError(`Authentication error. Liveblocks could not parse the response of your authentication endpoint`);
64
- }
65
- return data;
66
- }