From 47f00021ec9f383c913dd31d83426c15620b8152 Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Thu, 24 Aug 2023 15:00:59 +0100 Subject: [PATCH 001/191] Add legacy CDN upload I mistakenly thought cdn.ably.com was using the new bucket, but it's actually using the old bucket. Until we are migrated we will need to also upload files to the old bucket. --- .github/workflows/cdn.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cdn.yml b/.github/workflows/cdn.yml index 1b4b08c7..7caa2e20 100644 --- a/.github/workflows/cdn.yml +++ b/.github/workflows/cdn.yml @@ -47,4 +47,30 @@ jobs: npm run build - run: | aws s3 cp ./dist/iife/index.bundle.js s3://${{ github.event.inputs.bucket }}/spaces/${{ github.event.inputs.version }}/iife/index.bundle.js - + publish_legacy: + if: ${{ github.event.inputs.bucket == 'prod-cdn.ably.com' }} # Only run for the prod build + runs-on: ubuntu-latest + # These permissions are necessary to run the configure-aws-credentials action + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.inputs.version }} + - uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_PRODUCTION }}:role/github-actions-sdk + aws-region: us-east-1 + - name: Use Node.js 14.x + uses: actions/setup-node@v3 + with: + node-version: 14.x + - name: Install dependencies and build + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm ci + npm run build + - run: | + aws s3 cp ./dist/iife/index.bundle.js s3://cdn.ably.io/spaces/${{ github.event.inputs.version }}/iife/index.bundle.js From 88886441eb0737fedd2b14862cfd68d679cf92b8 Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Wed, 16 Aug 2023 12:22:40 +0100 Subject: [PATCH 002/191] Delete whitespace --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94f2e622..5a4b0785 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@

+<<<<<<< HEAD _[Ably](https://ably.com) is the platform that powers synchronized digital experiences in realtime. Whether attending an event in a virtual venue, receiving realtime financial information, or monitoring live car performance data – consumers simply expect realtime digital experiences as standard. Ably provides a suite of APIs to build, extend, and deliver powerful digital experiences in realtime for more than 250 million devices across 80 countries each month. Organizations like Bloomberg, HubSpot, Verizon, and Hopin depend on Ably’s platform to offload the growing complexity of business-critical realtime data synchronization at global scale. For more information, see the [Ably documentation](https://ably.com/docs)._ --- @@ -45,7 +46,7 @@ The Collaborative Spaces SDK is currently under development. If you are interest Get started quickly using this section, or take a look at: * more detailed [usage instructions](/docs/usage.md) -* [class definitions](/docs/class-definitions.md) +* [class definitions](/docs/class-definitions.md) * how the Spaces SDK uses [Ably internally](/docs/channel-behaviors.md) ### Prerequisites From e91b7958175d9abdc76ca6b6a4634ce8f3326ba0 Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Wed, 16 Aug 2023 12:43:28 +0100 Subject: [PATCH 003/191] Update README to latest API --- README.md | 93 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5a4b0785..e3880539 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,10 @@ Get started quickly using this section, or take a look at: To begin, you will need the following: -* An Ably account. You can [sign up](https://ably.com/signup) for free. -* An Ably API key. You can create API keys in an app within your [Ably account](https://ably.com/dashboard). +* An Ably account + * You can [sign up](https://ably.com/signup) for free +* An Ably API key + * Create API keys in an app within your [Ably account](https://ably.com/dashboard) * The API key needs the following [capabilities](https://ably.com/docs/auth/capabilities): `publish`, `subscribe`, `presence` and `history`. You can use [basic authentication](https://ably.com/docs/auth/basic) for testing purposes, however it is strongly recommended that you use [token authentication](https://ably.com/docs/auth/token) in production environments. @@ -88,29 +90,27 @@ You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): ``` -### Space membership +### Space A space is the virtual, collaborative area of an application you want to monitor. A space can be anything from a web page, a sheet within a spreadsheet, an individual slide in a slideshow, or the slideshow itself. Create a space and listen for events to see when clients enter and leave. -Space membership is used to build avatar stacks and find out which members are online within a space. - ```ts // Create a new space const space = await spaces.get('demoSlideshow'); -// Register a listener to subscribe to events of when users enter or leave the space -space.subscribe('membersUpdate', (members) => { - console.log(members); +// Subscribe to space state events +space.subscribe('update', (spaceState) => { + console.log(spaceState.members); }); -// Enter a space, publishing a memberUpdate event, including optional profile data +// Enter a space, publishing an update event, including optional profile data space.enter({ username: 'Claire Lemons', avatar: 'https://slides-internal.com/users/clemons.png', }); ``` -The following is an example `membersUpdate` event received by listeners when a user enters a space: +The following is an example `update` event received by listeners when a user enters a space: ```json [ @@ -131,13 +131,71 @@ The following is an example `membersUpdate` event received by listeners when a u ] ``` +### Members + +Members is used to build avatar stacks and find out which members are online within a space. + +```ts +// Subscribe to all member events in a space by passing the 'update' event +space.members.subscribe('update', (memberUpdate) => { + console.log(memberUpdate); +}); + +// Subscribe to when a member enters the space +space.members.subscribe('enter', (memberJoined) => { + console.log(memberJoined); +}); + +// Subscribe to when a member leaves the space +space.members.subscribe('leave', (memberLeft) => { + console.log(memberLeft); +}); + +// Subscribe to when a member is removed from the space +space.members.subscribe('remove', (memberRemoved) => { + console.log(memberRemoved); +}); +``` + +The following is an example `memberUpdate` event received by listeners: + +```json +{ + "clientId": "clemons#142", + "connectionId": "hd9743gjDc", + "isConnected": true, + "lastEvent": { + "name": "enter", + "timestamp": 1677595689759 + }, + "location": null, + "profileData": { + "username": "Claire Lemons", + "avatar": "https://slides-internal.com/users/clemons.png" + } +} +``` + +Members has methods to get the current snapshot of member state: + +```ts +// Get all members in a space +const allMembers = await space.members.getAll(); + +// Get your own member object +const myMemberInfo = await space.members.getSelf(); + +// Get everyone else's member object but yourself +const othersMemberInfo = await space.members.getOthers(); +``` + ### Location -Member locations enable you to track where users are within an application. A location could be a form field, multiple cells in a spreadsheet or a slide in a slide deck editor. Subscribe to all location updates, specific location, or locations changes for a given member. +Member locations enable you to track where users are within an application. A location could be a form field, multiple cells in a spreadsheet or a slide in a slide deck editor. ```ts // Register a listener to subscribe to events of when users change location -space.locations.on('locationUpdate', (locationUpdate) => { +space.locations.subscribe('update', (locationUpdate) => { console.log(locationUpdate); }); @@ -194,16 +252,15 @@ space.enter({ avatar: 'https://slides-internal.com/users/clemons.png', }); +// Listen to events published on "mousemove" by all members +space.cursors.subscribe('update', (cursorUpdate) => { + console.log(cursorUpdate); +}); + // Publish a CursorUpdate with the location of a mouse, including optional data for the current member window.addEventListener('mousemove', ({ clientX, clientY }) => { space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); }); - - -// Listen to events published on "mousemove" by all members -space.cursors.on('cursorsUpdate', (cursorUpdate) => { - console.log(cursorUpdate); -}); ``` The above listener will receive a `CursorUpdate` event: From b09855a995aa891f3123d83a0d0f2700e283ebed Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Wed, 16 Aug 2023 13:11:43 +0100 Subject: [PATCH 004/191] Update usage docs --- docs/usage.md | 169 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 58 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 13e51eec..ae199061 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,9 +6,11 @@ To use Spaces, you will need the following: -- An Ably account. You can [sign up](https://ably.com/signup) for free. -- An Ably API key. You can create API keys in an app within your [Ably account](https://ably.com/dashboard). - - The API key needs the following [capabilities](https://ably.com/docs/realtime/authentication#capabilities-explained): `publish`, `subscribe`, `presence` and `history`. +* An Ably account + * You can [sign up](https://ably.com/signup) for free +* An Ably API key + * Create API keys in an app within your [Ably account](https://ably.com/dashboard) + * The API key needs the following [capabilities](https://ably.com/docs/realtime/authentication#capabilities-explained): `publish`, `subscribe`, `presence` and `history` ### Environment @@ -85,53 +87,24 @@ The following is an example of setting `offlineTimeout` to 3 minutes and a `pagi const space = await spaces.get('demoSlideshow', { offlineTimeout: 180_000, cursors: { paginationLimit: 10 } }); ``` -## Members +### Subscribe to a space -Members is a core concept of the library. When you enter a space, you become a `member`. On the client, your own membership is to referred to as `self`. You can get your `self` by calling `space.getSelf`. To get all the members (including self), call `space.getMembers`. These method will return (respectively an object and array of): +You can subscribe to events in a space: -```js -{ - "clientId": "clemons#142", - "connectionId": "hd9743gjDc", - "isConnected": true, - "lastEvent": { - "name": "enter", - "timestamp": 1677595689759 - }, - "location": null, - "profileData": { - "username": "Claire Lemons", - "avatar": "https://slides-internal.com/users/clemons.png" - } -} +```ts +space.subscribe('update', (spaceState) => { + console.log(spaceState); +}); ``` -See [SpaceMember](/docs/class-definitions.md#spacemember) for details on properties. - -### Listen to members updates - -The `space` instance is an `EventEmitter`. Events will be emitted for updates to members (including self). You can listen to the following events: - -#### enter +This gets triggered on [member](#members) and [location](#location) events. -Emitted when a member enters a space. Called with the member entering the space. - -#### leave - -Emitted when a member leaves a space. Called with the member leaving the space. - -#### membersUpdate - -Emitted when members enter, leave and their location is updated. Called with an array of all the members in the space. +Similarly you can unsubscribe: ```ts -space.subscribe('membersUpdate', (members) => { - console.log(members); -}); +space.unsubscribe(); ``` -To stop listening to member events, users can call the `space.unsubscribe()` method. See [Event emitters](#event-emitters) for options and usage. - ### Enter a space To become a member of a space (and use the other APIs, like location or cursors) a client needs to enter a space. @@ -153,8 +126,8 @@ space.enter({ A leave event is sent when a user leaves a space. This can occur for one of the following reasons: -- `space.leave()` is called explicitly. -- The user closes the tab. +- `space.leave()` is called explicitly +- The user closes the tab - The user is abruptly disconnected from the internet for longer than 2 minutes A leave event does not remove the member immediately from members. Instead, they are removed after a timeout which is configurable by the [`offlineTimeout` option](#options). This allows the UI to display an intermediate state before disconnection/reconnection. @@ -181,6 +154,85 @@ await space.updateProfileData((oldProfileData) => { }); ``` +## Members + +When you enter a space, you become a `member`. On the client, your own membership is to referred to as `self`. You can get your `self` by calling `space.members.getSelf()`. To get all the members (including self), call `space.members.getAll()`. These methods will return (respectively an object and array of): + +```json +{ + "clientId": "clemons#142", + "connectionId": "hd9743gjDc", + "isConnected": true, + "lastEvent": { + "name": "enter", + "timestamp": 1677595689759 + }, + "location": null, + "profileData": { + "username": "Claire Lemons", + "avatar": "https://slides-internal.com/users/clemons.png" + } +} +``` + +See [SpaceMember](/docs/class-definitions.md#spacemember) for details on properties. + +### Member events + +Subscribe to either `enter`, `leave`, `remove` or `update` events related to members in a space. + +#### enter + +Emitted when a member enters a space. Called with the member entering the space. + +```ts +space.members.subscribe('enter', (memberJoins) => { + console.log(memberJoins); +}); +``` + +#### leave + +Emitted when a member leaves a space. Called with the member leaving the space. + +```ts +space.members.subscribe('leave', (memberLeft) => { + console.log(memberLeft); +}); +``` + +#### remove + +Emitted when a member is removed from a space. Called with the member removed from the space. + +```ts +space.members.subscribe('remove', (memberRemoved) => { + console.log(memberRemoved); +}); +``` + +#### update + +Emitted when for `enter`, `leave` and `remove` events in a space: + + +```ts +space.members.subscribe('update', (memberUpdate) => { + console.log(memberUpdate); +}); +``` + +This is the same as not specifying the event: + +```ts +space.members.subscribe((memberUpdate) => { + console.log(memberUpdate); +}); +``` + +To stop listening to member events, users can call the `space.members.unsubscribe()` method. See [Event emitters](#event-emitters) for options and usage. + + ## Location Each member can set a location for themselves: @@ -193,18 +245,19 @@ A location does not have a prescribed shape. In your UI it can represent a singl The location property will be set on the [member](#members). -Because locations are part of members, a `memberUpdate` event will be emitted when a member updates their location. When a member leaves, their location is set to `null`. +A location event will be emitted when a member updates their location: ```ts -space.subscribe('membersUpdate', (members) => { - console.log(members); +space.subscribe('update', (member) => { + console.log(member.location); }); ``` +When a member leaves, their location is set to `null`. However, it's possible to listen to just location updates. `locations` is an [event emitter](#event-emitters) and will emit the `locationUpdate` event: ```ts -space.locations.subscribe('locationUpdate', (locationUpdate) => { +space.locations.subscribe('update', (locationUpdate) => { console.log(locationUpdate); }); ``` @@ -250,7 +303,7 @@ The most common use case is to show the current mouse pointer position. To start listing to cursor events, use the `.subscribe` method: ```ts -space.cursors.subscribe('cursorsUpdate', (cursorUpdate) => { +space.cursors.subscribe('update', (cursorUpdate) => { console.log(cursorUpdate); }); ``` @@ -272,7 +325,7 @@ To set the position of a cursor and emit a `CursorUpdate`, first enter the space space.enter(); ``` -Then call `.set`: +Then call `set`: ```ts window.addEventListener('mousemove', ({ clientX, clientY }) => { @@ -316,51 +369,51 @@ const lastPositions = await space.cursors.getAll(); ## Event Emitters -`space`, `cursors` & `locations` are event emitters. Event emitters provide `subscribe` and `unsubscribe` methods to attach/detach event listeners. Both methods support overloaded versions, described below. +`space`, `members`, `cursors` and `locations` are event emitters. Event emitters provide `subscribe` and `unsubscribe` methods to attach/detach event listeners. Both methods support overloaded versions, described below. Calling `subscribe` with a single function argument will subscribe to all events on that emitter. ```ts -space.subscribe(() => {}); +space.members.subscribe(); ``` Calling `subscribe` with a named event and a function argument will subscribe to that event only. ```ts -space.subscribe(`membersUpdate`, () => {}); +space.members.subscribe(`enter`, () => {}); ``` Calling `subscribe` with an array of named events and a function argument will subscribe to those events. ```ts -space.subscribe([`membersUpdate`], () => {}); +space.members.subscribe([`enter`, `leave`], () => {}); ``` Calling `unsubscribe` with no arguments will remove all registered listeners. ```ts -space.unsubscribe(); +space.members.unsubscribe(); ``` Calling `unsubscribe` with a single named event will remove all listeners registered for that event. ```ts -space.unsubscribe(`membersUpdate`); +space.members.unsubscribe(`enter`); ``` Calling `unsubscribe` with an array of named events will remove all listeners registered for those events. ```ts -space.unsubscribe([`membersUpdate`]); +space.members.unsubscribe([`enter`, `leave`]); ``` Calling `unsubscribe` and adding a listener function as the second argument to both of the above will remove only that listener. ```ts const listener = () => {}; -space.unsubscribe(`membersUpdate`, listener); -space.unsubscribe([`membersUpdate`], listener); +space.members.unsubscribe(`update`, listener); +space.members.unsubscribe([`update`], listener); ``` As with the native DOM API, this only works if the listener is the same reference as the one passed to `subscribe`. From 54af7c7a871b786d6a9fdc9ce777583dee7458ab Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Wed, 16 Aug 2023 16:10:55 +0100 Subject: [PATCH 005/191] Update class definitions I've tried to organise this to include the members namespace, and tried to keep the examples consistent. --- docs/class-definitions.md | 219 ++++++++++++++++++++++++++------------ 1 file changed, 148 insertions(+), 71 deletions(-) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 54b74427..0ef33e7f 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -102,6 +102,14 @@ An instance of a Space created using [spaces.get](#get). Inherits from [EventEmi ## Properties +### members + +An instance of [Members](#members). + +```ts +type members = instanceof Members; +``` + ### cursors An instance of [Cursors](#cursors). @@ -154,66 +162,112 @@ await space.updateProfileData((oldProfileData) => { }) ``` +# Members + +Handles members within a space. + +## Methods + ### subscribe -Listen to events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. +Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. Available events: -- #### **membersUpdate** +- #### **update** - Listen to updates to members. + Listen to profile data updates of members. ```ts - space.subscribe('membersUpdate', (members: SpaceMember[]) => {}); + space.members.subscribe('update', (member: SpaceMember) => {}); ``` - Triggers on: - - presence updates ([`enter`, `leave`, `update` and `present` events](https://ably.com/docs/presence-occupancy/presence?lang=javascript)) - - [location updates](#locationupdate) - - The argument supplied to the callback is an array of [SpaceMember](#spacemember) (members) objects within the space. + The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event. - #### **enter** + Listen to enter events of members. ```ts - space.subscribe('enter', (member: SpaceMember) => {}) + space.members.subscribe('enter', (member: SpaceMember) => {}) ``` The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. - #### **leave** - - Listen to leave events of members. Note that the leave event will only fire once the [offlineTimeout](#spaceoptions) has passed. + + Listen to leave events of members. The leave event will be issued when a member calls `space.leave()` or is disconnected. ```ts - space.subscribe('leave', (member: SpaceMember) => {}) + space.members.subscribe('leave', (member: SpaceMember) => {}) ``` The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member leaving the space. -### off +- #### **remove** + + Listen to remove events of members. The remove event will be issued when the [offlineTimeout](#spaceoptions) has passed. + + ```ts + space.members.subscribe('remove', (member: SpaceMember) => {}) + ``` + + The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member removed from the space. + +### unsubscribe Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. ```ts -space.off('enter'); +// Unsubscribe from all events +space.members.unsubscribe(); + +// Unsubscribe from enter events +space.members.unsubscribe('enter'); + +// Unsubscribe from leave events +space.members.unsubscribe('leave'); ``` -### getMembers +### getSelf -Returns an array of all [SpaceMember](#spacemember) objects (members) currently in the space, including any who have left and not yet timed out. (_see: [offlineTimeout](#spaceoptions)_) +Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `undefined` if the client hasn't entered the space yet. ```ts -type getMembers = () => SpaceMember[]; +type getSelf = () => Promise; ``` -### getSelf +Example: + +```ts +const myMember = await space.members.getSelf(); +``` + +### getAll + +Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, including any who have left and not yet timed out. (_see: [offlineTimeout](#spaceoptions)_) -Gets the [SpaceMember](#spacemember) object which relates to the local connection. Will return `undefined` if the client hasn't entered the space yet. +```ts +type getAll = () => Promise; +``` + +Example: ```ts -type getSelf = () => SpaceMember | undefined; +const allMembers = await space.members.getAll(); +``` + +### getOthers + +Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, excluding your own member object. + +```ts +type getSelf = () => Promise; +``` + +Example: + +```ts +const otherMembers = await space.members.getOthers(); ``` ## Related Types @@ -272,53 +326,76 @@ Handles the tracking of member locations within a space. Inherits from [EventEmi ## Methods +### subscribe + +Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. + +Available events: + +- #### **update** + + Fires when a member updates their location. The argument supplied to the event listener is a [LocationUpdate](#locationupdate-1). + + ```ts + space.locations.subscribe('update', (locationUpdate: LocationUpdate) => {}); + ``` + ### set Set your current location. [Location](#location-1) can be any JSON-serializable object. Emits a [locationUpdate](#locationupdate) event to all connected clients in this space. ```ts -type set = (update: Location) => void; +type set = (update: Location) => Promise; ``` -### getSelf -Get location for self + +### unsubscribe + +Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + ```ts -await space.locations.getSelf() +space.locations.unsubscribe('update'); ``` -### getAll -Get location for all members + +### getSelf + +Get location for self. ```ts -await space.locations.getAll() +type getSelf = () => Promise; ``` -### getOthers -Get location for other members + +Example: ```ts -await space.locations.getOthers() +const myLocation = await space.locations.getSelf(); ``` +### getAll +Get location for all members. -### subscribe - -Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. +```ts +type getAll = () => Promise>; +``` -Available events: +Example: -- #### **locationUpdate** +```ts +const allLocations = await space.locations.getAll(); +``` - Fires when a member updates their location. The argument supplied to the event listener is an [LocationUpdate](#locationupdate-1). +### getOthers - ```ts - space.locations.subscribe('locationUpdate', (locationUpdate: LocationUpdate) => {}); - ``` +Get location for other members -### off +```ts +type getOthers = () => Promise>; +``` -Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. +Example: ```ts -space.locations.off('locationUpdate'); +const otherLocations = await space.locations.getOthers() ``` ## Related types @@ -349,6 +426,20 @@ Handles tracking of member cursors within a space. Inherits from [EventEmitter]( ## Methods +### subscribe + +Listen to `CursorUpdate` events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + +Available events: + +- #### **update** + + Emits an event when a new cursor position is set. The argument supplied to the event listener is a [CursorUpdate](#cursorupdate). + + ```ts + space.cursors.subscribe('update', (cursorUpdate: CursorUpdate) => {}); + ``` + ### set Set the position of a cursor. This will emit a `CursorUpdate` event. If a member has not yet entered the space, this method will error. @@ -367,18 +458,12 @@ window.addEventListener('mousemove', ({ clientX, clientY }) => { }); ``` -### getAll - -Get the last CursorUpdate for each connection. - -```ts -type getAll = () => Record; -``` +### unsubscribe -Example: +Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. ```ts -const lastPositions = space.cursors.getAll(); +space.cursors.unsubscribe('update'); ``` ### getSelf @@ -395,40 +480,32 @@ Example: const selfPosition = space.cursors.getSelf(); ``` -### getOthers +### getAll Get the last CursorUpdate for each connection. ```ts -type getOthers = () => Record; +type getAll = () => Record; ``` Example: ```ts -const otherPositions = space.cursors.getOthers(); +const allLatestPositions = space.cursors.getAll(); ``` -### subscribe - -Listen to `CursorUpdate` events. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. - -Available events: - -- #### **cursorsUpdate** - - Emits an event when a new cursor position is set. The argument supplied to the event listener is a [CursorUpdate](#cursorupdate). +### getOthers - ```ts - space.cursors.subscribe('cursorsUpdate', (cursorUpdate: CursorUpdate) => {}); - ``` +Get the last CursorUpdate for each connection. -### off +```ts +type getOthers = () => Record; +``` -Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. +Example: ```ts -space.cursors.off('cursorsUpdate'); +const otherPositions = space.cursors.getOthers(); ``` ## Related types @@ -464,4 +541,4 @@ Represent data that can be associated with a cursor update. ```ts type CursorData = Record; -``` \ No newline at end of file +``` From 743d8608ed91367b9ca0a2337df1a69458b53440 Mon Sep 17 00:00:00 2001 From: Srushtika Neelakantam Date: Wed, 23 Aug 2023 21:40:26 +0100 Subject: [PATCH 006/191] Update README.md --- README.md | 157 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 99 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index e3880539..b0a8da9f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Ably Collaborative Spaces SDK +# Ably Spaces SDK

@@ -12,58 +12,57 @@

-<<<<<<< HEAD -_[Ably](https://ably.com) is the platform that powers synchronized digital experiences in realtime. Whether attending an event in a virtual venue, receiving realtime financial information, or monitoring live car performance data – consumers simply expect realtime digital experiences as standard. Ably provides a suite of APIs to build, extend, and deliver powerful digital experiences in realtime for more than 250 million devices across 80 countries each month. Organizations like Bloomberg, HubSpot, Verizon, and Hopin depend on Ably’s platform to offload the growing complexity of business-critical realtime data synchronization at global scale. For more information, see the [Ably documentation](https://ably.com/docs)._ +_[Ably](https://ably.com) is the scalable and five nines reliable middleware, used by developers at over 500 companies worldwide, to quickly build realtime applications - from collaborative experiences and live chat, to data broadcast and notifications. Supported by its globally distributed infrastructure, Ably offers 25 SDKs for various programming languages and platforms, as well as partner integrations with technologies including Datadog, Kafka, and Vercel, that accelerate the delivery of realtime experiences._ --- -The [Ably](https://ably.com) Collaborative Spaces SDK enables you to implement realtime collaborative features in your applications. +The **Spaces SDK** contains a purpose built set of APIs that help you build collaborative environments for your apps to quickly enable remote team collaboration. Try out a [live demo](https://space.ably.dev) of a slideshow application for an example of realtime collaboration powered by the Spaces SDK. ![Example collaboration GIF](/docs/images/collab.gif) -Rather than having to coordinate resources on calls, or send documents and spreadsheets back and forth using a combination of tools, having in-app realtime collaboration features has proven to boost productivity in remote workplaces. Try out a [live demo](https://space.ably.dev) of a slideshow application for an example of realtime collaboration in action. -Realtime collaboration enables users to have contextual awareness of other users within an application. This means knowing: +## Realtime collaboration -**Who is in the application?** +Rather than having to coordinate resources on calls, or send documents and spreadsheets back and forth using a combination of tools, having in-app realtime collaboration features has proven to boost productivity in remote workplaces. Such features enable end users to have contextual awareness of other users within an application. This means knowing: -One of the most important aspects of collaboration is knowing who else you're working with. The most common way to display this is using an "Avatar Stack" to show who else is currently online, and those that have recently gone offline. - -**Where is each user within the application?** +### Who is in the application? -Knowing where each user is within an application helps you understand their intentions without having to explicitly ask them. For example, seeing that a colleague is currently viewing slide 2 of a slideshow means that you can carry out your own edits to slide 3 without interfering with their work. Displaying the locations of your users can be achieved by highlighting the UI element they have selected, displaying a miniature avatar stack on the slide they are viewing, or showing the live location of their cursors. +One of the most important aspects of collaboration is knowing who else you're working with. The most common way to display this is using an "Avatar Stack" to show who else is currently online, and those that have recently gone offline. -**What is everyone doing in the application?** +### Where is each user within the application? -Changes to the app state made by users not only need to be synced with your backend for validation and long term storage, but also be immediately reflected in the UI so that users are always viewing the latest information. For example, in a spreadsheet application, if one user has entered a value in a cell, all other users need to see that change instantly. Live updates help accomplish this in a collaborative space. +Knowing where each user is within an application helps you understand their intentions without having to explicitly ask them. For example, seeing that a colleague is currently viewing slide 2 of a presentation deck means that you can carry out your own edits to slide 3 without interfering with their work. Displaying the locations of your users can be achieved by highlighting the UI element they have selected, displaying a miniature avatar stack on the slide they are viewing, or showing the live location of their cursors. In Spaces, we call this "Member Location". -## Status +### What is everyone doing in the application? -The Collaborative Spaces SDK is currently under development. If you are interested in being an early adopter and providing feedback then you can [sign up](https://go.ably.com/spaces-early-access) for early access and are welcome to [provide us with feedback](https://go.ably.com/spaces-feedback). +Changes to the app state made by users not only need to be synced with your backend for validation and long term storage, but also be immediately reflected in the UI so that users are always viewing the latest information. For example, in a spreadsheet application, if one user has entered a value in a cell, all other users need to see that change instantly. Pub/Sub Channels help flexibly broadcast live updates in a collaborative space. -## Quickstart +## SDK Development Status -Get started quickly using this section, or take a look at: +The Spaces SDK is currently under development. If you are interested in being an early adopter and providing feedback then you can [sign up](https://go.ably.com/spaces-early-access) for early access and are welcome to [provide us with feedback](https://go.ably.com/spaces-feedback). -* more detailed [usage instructions](/docs/usage.md) -* [class definitions](/docs/class-definitions.md) -* how the Spaces SDK uses [Ably internally](/docs/channel-behaviors.md) +The next section gives you an overview of how to use the SDK. Alternatively, you can jump to: +* [Class definitions](/docs/class-definitions.md) +* [Usage instructions](/docs/usage.md) +* [Channel behaviors](/docs/channel-behaviors.md) -### Prerequisites +## Prerequisites -To begin, you will need the following: +To start using this SDK, you will need the following: * An Ably account - * You can [sign up](https://ably.com/signup) for free + * You can [sign up](https://ably.com/signup) to the generous free tier. * An Ably API key - * Create API keys in an app within your [Ably account](https://ably.com/dashboard) - * The API key needs the following [capabilities](https://ably.com/docs/auth/capabilities): `publish`, `subscribe`, `presence` and `history`. + * Use the default or create a new API key in an app within your [Ably account dashboard](https://ably.com/dashboard). + * Make sure your API key has the following [capabilities](https://ably.com/docs/auth/capabilities): `publish`, `subscribe`, `presence` and `history`. + -You can use [basic authentication](https://ably.com/docs/auth/basic) for testing purposes, however it is strongly recommended that you use [token authentication](https://ably.com/docs/auth/token) in production environments. -### Authenticate and instantiate +## Installation and authentication -Install the Collaborative Spaces SDK and the Ably JavaScript SDK: +#### Option 1: Using NPM + +Install the Ably JavaScript SDK and the Spaces SDK: ```sh npm install ably @ably-labs/spaces @@ -78,10 +77,11 @@ import { Realtime } from 'ably'; const client = new Realtime.Promise({key: "", clientId: ""}); const spaces = new Spaces(client); ``` +You can use [basic authentication](https://ably.com/docs/auth/basic) i.e. the API Key directly for testing purposes, however it is strongly recommended that you use [token authentication](https://ably.com/docs/auth/token) in production environments. -You can create an Ably client with just an API key, however to use Spaces you must also set a [`clientId`](https://ably.com/docs/auth/identified-clients) so that clients are identifiable. If you are prototyping, you can use a package like [nanoid](https://www.npmjs.com/package/nanoid) to generate an ID. +To use Spaces you must also set a [`clientId`](https://ably.com/docs/auth/identified-clients) so that clients are identifiable. If you are prototyping, you can use a package like [nanoid](https://www.npmjs.com/package/nanoid) to generate an ID. -#### CDN +#### Option 2: Using a CDN You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): @@ -89,10 +89,13 @@ You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): ``` +After this, instantiate the SDK in the same way as in the NPM option above. + +## Creating a new Space -### Space +A space is the virtual area of your application where you want to enable synchronous collaboration. A space can be anything from a web page, a sheet within a spreadsheet, an individual slide in a slideshow, or the slideshow itself. A space has a participant state containing online and recently left members, their profile details, their locations and any locks they have acquired for the UI components. -A space is the virtual, collaborative area of an application you want to monitor. A space can be anything from a web page, a sheet within a spreadsheet, an individual slide in a slideshow, or the slideshow itself. Create a space and listen for events to see when clients enter and leave. +Create a space and subscribe to any updates to the participant state. ```ts // Create a new space @@ -110,7 +113,7 @@ space.enter({ }); ``` -The following is an example `update` event received by listeners when a user enters a space: +The following is an example event payload received by subscribers when a user enters a space: ```json [ @@ -131,33 +134,38 @@ The following is an example `update` event received by listeners when a user ent ] ``` -### Members +## Space members -Members is used to build avatar stacks and find out which members are online within a space. +The `members` namespace within a Space is a client-side filtered listener optimized for building avatar stacks. Subscribe to members entering, leaving, being removed from the Space (after a timeout) or updating their profile information. ```ts -// Subscribe to all member events in a space by passing the 'update' event -space.members.subscribe('update', (memberUpdate) => { +// Subscribe to all member events in a space +space.members.subscribe((memberUpdate) => { console.log(memberUpdate); }); -// Subscribe to when a member enters the space +// Subscribe to member enter events only space.members.subscribe('enter', (memberJoined) => { console.log(memberJoined); }); -// Subscribe to when a member leaves the space +// Subscribe to member leave events only space.members.subscribe('leave', (memberLeft) => { console.log(memberLeft); }); -// Subscribe to when a member is removed from the space +// Subscribe to member remove events only space.members.subscribe('remove', (memberRemoved) => { console.log(memberRemoved); }); + +// Subscribe to member profile update events only +space.members.subscribe('update', (memberProfileUpdated) => { + console.log(memberProfileUpdated); +}); ``` -The following is an example `memberUpdate` event received by listeners: +The following is an example event payload received by subscribers when member updates occur in a space: ```json { @@ -176,7 +184,9 @@ The following is an example `memberUpdate` event received by listeners: } ``` -Members has methods to get the current snapshot of member state: +### Getting a snapshot of space members + +Space members has methods to get the current snapshot of member state: ```ts // Get all members in a space @@ -189,27 +199,28 @@ const myMemberInfo = await space.members.getSelf(); const othersMemberInfo = await space.members.getOthers(); ``` -### Location +## Member locations -Member locations enable you to track where users are within an application. A location could be a form field, multiple cells in a spreadsheet or a slide in a slide deck editor. +The `locations` namespace within a Space is a client-side filtered listener optimized for building member locations which enable you to track where users are within an application. A location could be a form field, multiple cells in a spreadsheet or a slide in a slide deck editor. ```ts -// Register a listener to subscribe to events of when users change location -space.locations.subscribe('update', (locationUpdate) => { - console.log(locationUpdate); -}); - -// You need to enter a space before setting your location +// You need to enter a space before publishing your location space.enter({ username: 'Claire Lemons', avatar: 'https://slides-internal.com/users/clemons.png', }); -// Publish locationUpdate event with a client's location when they select a UI element +// Publish your location based on the UI element selected space.locations.set({ slide: '3', component: 'slide-title' }); + +// Subscribe to location events from all members in a space +space.locations.subscribe('update', (locationUpdate) => { + console.log(locationUpdate); +}); + ``` -The following is an example `locationUpdate` event received by subscribers when a user changes location: +The following is an example event payload received by subscribers when a member changes location: ```json { @@ -241,29 +252,44 @@ The following is an example `locationUpdate` event received by subscribers when } ``` -### Cursors +### Getting a snapshot of member locations + +Member locations has methods to get the current snapshot of member state: + +```ts +// Get a snapshot of all the member locations +const allLocations = space.locations.getAll(); + +// Get a snapshot of my location +const myLocation = space.locations.getSelf(); + +// Get a snapshot of everyone else's locations +const othersLocations = space.locations.getOthers(); +``` + +## Live cursors -Use the Cursors API to track client pointer events across an application. Events can also include associated data, such as pointer attributes and the IDs of associated UI elements: +The `cursors` namespace within a Space is a client-side filtered listener optimized for building live cursors which allows you to track a member's pointer position updates across an application. Events can also include associated data, such as pointer attributes and the IDs of associated UI elements: ```ts -// You need to enter a space before setting your cursor updates +// You need to enter a space before publishing your cursor updates space.enter({ username: 'Claire Lemons', avatar: 'https://slides-internal.com/users/clemons.png', }); -// Listen to events published on "mousemove" by all members +// Subscribe to events published on "mousemove" by all members space.cursors.subscribe('update', (cursorUpdate) => { console.log(cursorUpdate); }); -// Publish a CursorUpdate with the location of a mouse, including optional data for the current member +// Publish a your cursor position on "mousemove" including optional data window.addEventListener('mousemove', ({ clientX, clientY }) => { space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); }); ``` -The above listener will receive a `CursorUpdate` event: +The following is an example event payload received by subscribers when a member moves their cursor: ```js { @@ -273,3 +299,18 @@ The above listener will receive a `CursorUpdate` event: "data": { "color": "red" } } ``` + +### Getting a snapshot of member cursors + +Member cursors has methods to get the current snapshot of member state: + +```ts +// Get a snapshot of all the cursors +const allCursors = space.cursors.getAll(); + +// Get a snapshot of my cursor +const myCursor = space.cursors.getSelf(); + +// Get a snapshot of everyone else's cursors +const othersCursors = space.cursors.getOthers(); +``` From 982d515ad80dcc08b7f354cb0175453ffa204b38 Mon Sep 17 00:00:00 2001 From: Srushtika Neelakantam Date: Wed, 23 Aug 2023 22:02:42 +0100 Subject: [PATCH 007/191] Update class-definitions --- docs/class-definitions.md | 164 ++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 85 deletions(-) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 0ef33e7f..4ed4cb7b 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -2,19 +2,7 @@ ## Constructor -Create a new instance of the Spaces library. - -The Spaces library constructor is overloaded allowing it to be instantiated using a [ClientOptions](https://ably.com/docs/api/realtime-sdk?lang=javascript#client-options) object: - -_**Deprecated: the ClientOptions option will be removed in the next release. Use the Ably client instance method described underneath.**_ - -```ts -import Spaces from '@ably-labs/spaces'; - -const spaces = new Spaces({ key: "", clientId: "" }); -``` - -Or an instance of the realtime, promise-based [Ably client](https://github.com/ably/ably-js): +Create a new instance of the Space SDK by passing an instance of the realtime, promise-based [Ably client](https://github.com/ably/ably-js): ```ts import { Realtime } from 'ably/promise'; @@ -24,15 +12,15 @@ const client = new Realtime.Promise({ key: "", clientId: "" const spaces = new Spaces(client); ``` -In both cases, a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascript) is required. +Please note that a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascript) is required. An API key will required for [basic authentication](https://ably.com/docs/auth/basic?lang=javascript). We strongly recommended that you use [token authentication](https://ably.com/docs/realtime/authentication#token-authentication) in any production environments. Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/setup?lang=javascript) for information on setting up a realtime promise client. -## Properties +### Properties -### ably +#### ably Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) client that was passed to the [constructor](#constructor). @@ -40,7 +28,7 @@ Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) client t type ably = Ably.RealtimePromise; ``` -### version +#### version Version of the Spaces library. @@ -48,9 +36,9 @@ Version of the Spaces library. type version = string; ``` -## Methods +### Methods -### get +#### get() Get or create a Space instance. Returns a [Space](#space) instance. Configure the space by passing [SpaceOptions](#spaceoptions) as the second argument. @@ -58,9 +46,9 @@ Get or create a Space instance. Returns a [Space](#space) instance. Configure th type get = (name: string, options?: SpaceOptions) => Promise; ``` -## Related Types +### Related Types -### SpaceOptions +#### SpaceOptions Used to configure a Space instance on creation. @@ -88,21 +76,21 @@ type CursorsOptions = { }; ``` -##### outboundBatchInterval +#### outboundBatchInterval The interval in milliseconds at which a batch of cursor positions are published. This is multiplied by the number of members in the space minus 1. The default value is 100ms. -##### paginationLimit +#### paginationLimit The number of pages searched from [history](https://ably.com/docs/storage-history/history) for the last published cursor position. The default is 5. -# Space +## Space An instance of a Space created using [spaces.get](#get). Inherits from [EventEmitter](/docs/usage.md#event-emitters). -## Properties +### Properties -### members +#### members An instance of [Members](#members). @@ -110,7 +98,7 @@ An instance of [Members](#members). type members = instanceof Members; ``` -### cursors +#### cursors An instance of [Cursors](#cursors). @@ -118,7 +106,7 @@ An instance of [Cursors](#cursors). type cursors = instanceof Cursors; ``` -### locations +#### locations An instance of [Locations](#locations). @@ -126,9 +114,9 @@ An instance of [Locations](#locations). type locations = instanceof Locations; ``` -## Methods +### Methods -### enter +#### enter() Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the [member object](#spacemember). Returns all current space members. @@ -136,7 +124,7 @@ Enter the space. Can optionally take `profileData`. This data can be an arbitrar type enter = (profileData?: Record) => Promise; ``` -### leave +#### leave() Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See [offlineTimeout](#spaceoptions). @@ -144,7 +132,7 @@ Leave the space. Can optionally take `profileData`. This triggers the `leave` ev type leave = (profileData?: Record) => Promise; ``` -### updateProfileData +#### updateProfileData() Update `profileData`. This data can be an arbitrary JSON-serializable object which is attached to the [member object](#spacemember). If the connection has not entered the space, calling `updateProfileData` will call `enter` instead. @@ -162,29 +150,25 @@ await space.updateProfileData((oldProfileData) => { }) ``` -# Members +## Space Members Handles members within a space. -## Methods +### Methods -### subscribe +#### subscribe() Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. -Available events: - -- #### **update** - - Listen to profile data updates of members. - ```ts - space.members.subscribe('update', (member: SpaceMember) => {}); + space.members.subscribe((member: SpaceMember) => {}); ``` The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event. -- #### **enter** + Available events: + +- ##### **enter** Listen to enter events of members. @@ -193,7 +177,7 @@ Available events: ``` The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. -- #### **leave** +- ##### **leave** Listen to leave events of members. The leave event will be issued when a member calls `space.leave()` or is disconnected. @@ -203,9 +187,9 @@ Available events: The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member leaving the space. -- #### **remove** +- ##### **remove** - Listen to remove events of members. The remove event will be issued when the [offlineTimeout](#spaceoptions) has passed. + Listen to remove events of members. The remove event will be triggered when the [offlineTimeout](#spaceoptions) has passed. ```ts space.members.subscribe('remove', (member: SpaceMember) => {}) @@ -213,9 +197,19 @@ Available events: The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member removed from the space. -### unsubscribe +- ##### **update** -Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + Listen to profile update events of members. + + ```ts + space.members.subscribe('update', (member: SpaceMember) => {}) + ``` + The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. + + +#### unsubscribe() + +Remove all the event listeners or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. ```ts // Unsubscribe from all events @@ -228,7 +222,7 @@ space.members.unsubscribe('enter'); space.members.unsubscribe('leave'); ``` -### getSelf +#### getSelf() Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `undefined` if the client hasn't entered the space yet. @@ -242,7 +236,7 @@ Example: const myMember = await space.members.getSelf(); ``` -### getAll +#### getAll() Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, including any who have left and not yet timed out. (_see: [offlineTimeout](#spaceoptions)_) @@ -256,7 +250,7 @@ Example: const allMembers = await space.members.getAll(); ``` -### getOthers +#### getOthers() Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, excluding your own member object. @@ -270,9 +264,9 @@ Example: const otherMembers = await space.members.getOthers(); ``` -## Related Types +### Related Types -### SpaceMember +#### SpaceMember A SpaceMember represents a member within a Space instance. Each new connection that enters will create a new member, even if they have the same [`clientId`](https://ably.com/docs/auth/identified-clients?lang=javascript). @@ -287,31 +281,31 @@ type SpaceMember = { }; ``` -#### clientId +##### clientId The client identifier for the user, provided to the ably client instance. -#### connectionId +##### connectionId Identifier for the connection used by the user. This is a unique identifier. -#### isConnected +##### isConnected Whether the user is connected to Ably. -#### profileData +##### profileData Optional user data that can be attached to a user, such as a username or image to display in an avatar stack. -#### location +##### location The current location of the user within the space. -#### lastEvent +##### lastEvent The most recent event emitted by [presence](https://ably.com/docs/presence-occupancy/presence?lang=javascript) and its timestamp. Events will be either `enter`, `leave`, `update` or `present`. -### PresenceEvent +#### PresenceEvent ```ts type PresenceEvent = { @@ -320,19 +314,19 @@ type PresenceEvent = { }; ``` -# Locations +## Member Locations Handles the tracking of member locations within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). -## Methods +### Methods -### subscribe +#### subscribe() Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. Available events: -- #### **update** +- ##### **update** Fires when a member updates their location. The argument supplied to the event listener is a [LocationUpdate](#locationupdate-1). @@ -340,7 +334,7 @@ Available events: space.locations.subscribe('update', (locationUpdate: LocationUpdate) => {}); ``` -### set +#### set() Set your current location. [Location](#location-1) can be any JSON-serializable object. Emits a [locationUpdate](#locationupdate) event to all connected clients in this space. @@ -348,7 +342,7 @@ Set your current location. [Location](#location-1) can be any JSON-serializable type set = (update: Location) => Promise; ``` -### unsubscribe +#### unsubscribe() Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. @@ -356,7 +350,7 @@ Remove all event listeners, all event listeners for an event, or specific listen space.locations.unsubscribe('update'); ``` -### getSelf +#### getSelf() Get location for self. @@ -370,7 +364,7 @@ Example: const myLocation = await space.locations.getSelf(); ``` -### getAll +#### getAll() Get location for all members. @@ -384,7 +378,7 @@ Example: const allLocations = await space.locations.getAll(); ``` -### getOthers +#### getOthers() Get location for other members @@ -398,9 +392,9 @@ Example: const otherLocations = await space.locations.getOthers() ``` -## Related types +### Related types -### Location +#### Location Represents a location in an application. @@ -408,7 +402,7 @@ Represents a location in an application. type Location = string | Record | null; ``` -### LocationUpdate +#### LocationUpdate Represents a change between locations for a given [`SpaceMember`](#spacemember). @@ -420,19 +414,19 @@ type LocationUpdate = { }; ``` -# Cursors +## Live Cursors Handles tracking of member cursors within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). -## Methods +### Methods -### subscribe +#### subscribe() Listen to `CursorUpdate` events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. Available events: -- #### **update** +- ##### **update** Emits an event when a new cursor position is set. The argument supplied to the event listener is a [CursorUpdate](#cursorupdate). @@ -440,7 +434,7 @@ Available events: space.cursors.subscribe('update', (cursorUpdate: CursorUpdate) => {}); ``` -### set +#### set() Set the position of a cursor. This will emit a `CursorUpdate` event. If a member has not yet entered the space, this method will error. @@ -458,7 +452,7 @@ window.addEventListener('mousemove', ({ clientX, clientY }) => { }); ``` -### unsubscribe +#### unsubscribe() Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. @@ -466,7 +460,7 @@ Remove all event listeners, all event listeners for an event, or specific listen space.cursors.unsubscribe('update'); ``` -### getSelf +#### getSelf() Get the last CursorUpdate for self. @@ -480,7 +474,7 @@ Example: const selfPosition = space.cursors.getSelf(); ``` -### getAll +#### getAll() Get the last CursorUpdate for each connection. @@ -494,7 +488,7 @@ Example: const allLatestPositions = space.cursors.getAll(); ``` -### getOthers +#### getOthers() Get the last CursorUpdate for each connection. @@ -508,9 +502,9 @@ Example: const otherPositions = space.cursors.getOthers(); ``` -## Related types +### Related types -### CursorUpdate +#### CursorUpdate Represents an update to a cursor. @@ -524,7 +518,7 @@ type CursorUpdate = { }; ``` -### CursorPosition +#### CursorPosition Represents a cursors position. @@ -535,7 +529,7 @@ type CursorPosition = { }; ``` -### CursorData +#### CursorData Represent data that can be associated with a cursor update. From b454f87d1568ba538d6f4bdf6b4326e425a44531 Mon Sep 17 00:00:00 2001 From: Srushtika Neelakantam Date: Thu, 24 Aug 2023 09:33:10 +0100 Subject: [PATCH 008/191] fix: Update class-definitions --- docs/class-definitions.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 4ed4cb7b..fdc4fce7 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -59,15 +59,15 @@ type SpaceOptions = { }; ``` -#### offlineTimeout +##### offlineTimeout -Number of milliseconds after a user loses connection or closes their browser window to wait before their [SpaceMember](#spacemember) object is removed from the members list. The default is 120000ms (2 minutes). +Number of milliseconds after a user loses connection or closes their browser window to wait before their [SpaceMember](#spacemember) object is removed from the members list. The default is 120000ms (2 minutes). -#### cursors +##### cursors Options relating to configuring the cursors API (see below). -#### CursorsOptions +##### CursorsOptions ```ts type CursorsOptions = { @@ -76,11 +76,11 @@ type CursorsOptions = { }; ``` -#### outboundBatchInterval +##### outboundBatchInterval The interval in milliseconds at which a batch of cursor positions are published. This is multiplied by the number of members in the space minus 1. The default value is 100ms. -#### paginationLimit +##### paginationLimit The number of pages searched from [history](https://ably.com/docs/storage-history/history) for the last published cursor position. The default is 5. @@ -158,7 +158,7 @@ Handles members within a space. #### subscribe() -Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. +Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. ```ts space.members.subscribe((member: SpaceMember) => {}); @@ -322,7 +322,7 @@ Handles the tracking of member locations within a space. Inherits from [EventEmi #### subscribe() -Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. +Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. Available events: @@ -436,9 +436,9 @@ Available events: #### set() -Set the position of a cursor. This will emit a `CursorUpdate` event. If a member has not yet entered the space, this method will error. +Set the position of a cursor. If a member has not yet entered the space, this method will error. -A `CursorUpdate` is an object with 2 properties. `position` is an object with 2 required properties, `x` and `y`. These represent the position of the cursor on a 2D plane. A second optional property, `data` can also be passed. This is an object of any shape and is meant for data associated with the cursor movement (like drag or hover calculation results): +A event payload returned contains an object with 2 properties. `position` is an object with 2 required properties, `x` and `y`. These represent the position of the cursor on a 2D plane. A second optional property, `data` can also be passed. This is an object of any shape and is meant for data associated with the cursor movement (like drag or hover calculation results): ```ts type set = (update: { position: CursorPosition, data?: CursorData }) @@ -462,7 +462,7 @@ space.cursors.unsubscribe('update'); #### getSelf() -Get the last CursorUpdate for self. +Get the last `CursorUpdate` object for self. ```ts type getSelf = () => ; @@ -476,7 +476,7 @@ const selfPosition = space.cursors.getSelf(); #### getAll() -Get the last CursorUpdate for each connection. +Get the last `CursorUpdate` object for all the members. ```ts type getAll = () => Record; @@ -490,7 +490,7 @@ const allLatestPositions = space.cursors.getAll(); #### getOthers() -Get the last CursorUpdate for each connection. +Get the last `CursorUpdate` object for everyone else but yourself. ```ts type getOthers = () => Record; From 12eaa31aa8a99cc3e0f624faa83b62f23d42c43a Mon Sep 17 00:00:00 2001 From: Srushtika Neelakantam Date: Thu, 24 Aug 2023 09:55:35 +0100 Subject: [PATCH 009/191] Update usage instructions --- docs/usage.md | 116 ++++++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 70 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index ae199061..62d22c35 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,16 +1,18 @@ -# Usage +# Usage Instructions + +This page contains detailed documentation to help you use the Spaces SDK. ## Prerequisites ### Ably API key -To use Spaces, you will need the following: +To start using this SDK, you will need the following: * An Ably account - * You can [sign up](https://ably.com/signup) for free + * You can [sign up](https://ably.com/signup) to the generous free tier. * An Ably API key - * Create API keys in an app within your [Ably account](https://ably.com/dashboard) - * The API key needs the following [capabilities](https://ably.com/docs/realtime/authentication#capabilities-explained): `publish`, `subscribe`, `presence` and `history` + * Use the default or create a new API key in an app within your [Ably account dashboard](https://ably.com/dashboard). + * Make sure your API key has the following [capabilities](https://ably.com/docs/auth/capabilities): `publish`, `subscribe`, `presence` and `history`. ### Environment @@ -20,14 +22,10 @@ Spaces is built on top of the [Ably JavaScript SDK](https://github.com/ably/ably ### NPM -```sh -npm install @ably-labs/spaces -``` - -If you need the Ably client (see [Authentication & instantiation](#authentication-and-instantiation)) +You'll need to install both the ably client and the spaces client: ```sh -npm install ably +npm install ably @ably-labs/spaces ``` ### CDN @@ -39,23 +37,11 @@ You can also use Spaces with a CDN like [unpkg](https://www.unpkg.com/): ``` -Note that when you use a CDN, you need to include Ably Client as well, the Spaces bundle does not include it. - ## Authentication and instantiation -Spaces use an [Ably promise-based realtime client](https://github.com/ably/ably-js#using-the-async-api-style). You can either pass an existing client to Spaces or pass the [client options](https://ably.com/docs/api/realtime-sdk?lang=javascript#client-options) directly to the spaces constructor. - -To instantiate with options, you will need at minimum an [Ably API key](#ably-api-key) and a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascripts). A clientId represents an identity of an connection. In most cases this will something like the id of a user: - -_**Deprecated: the ClientOptions option will be removed in the next release. Use the Ably client instance method described underneath.**_ - -```ts -import Spaces from '@ably-labs/spaces'; - -const spaces = new Spaces({ key: "", clientId: "" }); -``` +Spaces use an [Ably promise-based realtime client](https://github.com/ably/ably-js#using-the-async-api-style). You can pass an Ably client directly to the spaces constructor. -If you already have an ably client in your application, you can just pass it directly to Spaces (the client will still need to instantiated with a clientId): +The Ably client instantiation accepts client options. You will need at minimum an [Ably API key](#ably-api-key) and a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascripts). A clientId represents an identity of an connection. In most cases this will something like the id of a user: ```ts import { Realtime } from 'ably/promise'; @@ -65,13 +51,13 @@ const client = new Realtime.Promise({ key: "", clientId: "" const spaces = new Spaces(client); ``` -In both scenarios, you can access the client via `spaces.ably`. +You can access the Ably client via `spaces.ably`. To learn more about authenticating with ably, see our [authentication documentation](https://ably.com/docs/auth). ## Create a space -A space is the virtual area of an application you want to collaborate in, such as a web page, or slideshow. A `space` is uniquely identified by its name. A space is created, or an existing space retrieved from the `spaces` collection by calling the `get()` method. You can only connect to one space in a single operation. The following is an example of creating a space called "demonSlideshow": +A space is the virtual area of an application you want users to collaborate in, such as a web page, or slideshow. A `space` is uniquely identified by its name. A space is created, or an existing space retrieved from the `spaces` collection by calling the `get()` method. You can only connect to one space in a single operation. The following is an example of creating a space called "demonSlideshow": ```ts const space = await spaces.get('demoSlideshow'); @@ -130,7 +116,7 @@ A leave event is sent when a user leaves a space. This can occur for one of the - The user closes the tab - The user is abruptly disconnected from the internet for longer than 2 minutes -A leave event does not remove the member immediately from members. Instead, they are removed after a timeout which is configurable by the [`offlineTimeout` option](#options). This allows the UI to display an intermediate state before disconnection/reconnection. +A leave event does not remove the member immediately from `space.members`. Instead, they are removed after a timeout which is configurable by the [`offlineTimeout` option](#options). This allows the UI to display an intermediate state before disconnection/reconnection. As with `enter`, you can update the `profileData` on leave: @@ -143,7 +129,7 @@ space.leave({ ### Update profileData -To update `profileData` provided when entering the space, use the `updateProfileData` method. Pass new `profileData` or a function to base the new `profileData` of the existing value: +To update `profileData` after entering the space, use the `updateProfileData()` method. Pass new `profileData` or a function to base the new `profileData` of the existing value: ```ts await space.updateProfileData((oldProfileData) => { @@ -179,15 +165,21 @@ See [SpaceMember](/docs/class-definitions.md#spacemember) for details on propert ### Member events -Subscribe to either `enter`, `leave`, `remove` or `update` events related to members in a space. +Subscribe to either all the member events or specifically to `enter`, `leave`, `remove` or `update` events related to members in a space. + +```ts +space.members.subscribe((memberUpdate) => { + console.log(memberUpdate); +}); +``` #### enter Emitted when a member enters a space. Called with the member entering the space. ```ts -space.members.subscribe('enter', (memberJoins) => { - console.log(memberJoins); +space.members.subscribe('enter', (memberJoined) => { + console.log(memberJoined); }); ``` @@ -213,20 +205,11 @@ space.members.subscribe('remove', (memberRemoved) => { #### update -Emitted when for `enter`, `leave` and `remove` events in a space: - +Emitted when a member updates their `profileData` via `space.updateProfileData()`: ```ts -space.members.subscribe('update', (memberUpdate) => { - console.log(memberUpdate); -}); -``` - -This is the same as not specifying the event: - -```ts -space.members.subscribe((memberUpdate) => { - console.log(memberUpdate); +space.members.subscribe('update', (memberProfileUpdated) => { + console.log(memberProfileUpdated); }); ``` @@ -247,21 +230,14 @@ The location property will be set on the [member](#members). A location event will be emitted when a member updates their location: -```ts -space.subscribe('update', (member) => { - console.log(member.location); -}); -``` -When a member leaves, their location is set to `null`. - -However, it's possible to listen to just location updates. `locations` is an [event emitter](#event-emitters) and will emit the `locationUpdate` event: - ```ts space.locations.subscribe('update', (locationUpdate) => { console.log(locationUpdate); }); ``` +When a member leaves, their location is set to `null`. + This event will include the member affected by the change, as well as their previous and current locations: ```json @@ -300,7 +276,7 @@ A common feature of collaborative apps is to show where a users cursors is posit The most common use case is to show the current mouse pointer position. -To start listing to cursor events, use the `.subscribe` method: +To start listening to cursor events, use the `subscribe()` method: ```ts space.cursors.subscribe('update', (cursorUpdate) => { @@ -319,7 +295,7 @@ The listener will be called with a `CursorUpdate`: } ``` -To set the position of a cursor and emit a `CursorUpdate`, first enter the space: +To set the position of a cursor and emit a `CursorUpdate`, first enter the space if you haven't already: ```ts space.enter(); @@ -369,51 +345,51 @@ const lastPositions = await space.cursors.getAll(); ## Event Emitters -`space`, `members`, `cursors` and `locations` are event emitters. Event emitters provide `subscribe` and `unsubscribe` methods to attach/detach event listeners. Both methods support overloaded versions, described below. +`space`, `members`, `cursors` and `locations` are event emitters. Event emitters provide `subscribe()` and `unsubscribe()` methods to attach/detach event listeners. Both methods support overloaded versions, described below. -Calling `subscribe` with a single function argument will subscribe to all events on that emitter. +Calling `subscribe()` with a single function argument will subscribe to all events on that emitter. ```ts space.members.subscribe(); ``` -Calling `subscribe` with a named event and a function argument will subscribe to that event only. +Calling `subscribe()` with a named event and a function argument will subscribe to that event only. ```ts -space.members.subscribe(`enter`, () => {}); +space.members.subscribe('enter', () => {}); ``` -Calling `subscribe` with an array of named events and a function argument will subscribe to those events. +Calling `subscribe()` with an array of named events and a function argument will subscribe to those events. ```ts -space.members.subscribe([`enter`, `leave`], () => {}); +space.members.subscribe(['enter', 'leave'], () => {}); ``` -Calling `unsubscribe` with no arguments will remove all registered listeners. +Calling `unsubscribe()` with no arguments will remove all registered listeners. ```ts space.members.unsubscribe(); ``` -Calling `unsubscribe` with a single named event will remove all listeners registered for that event. +Calling `unsubscribe()` with a single named event will remove all listeners registered for that event. ```ts -space.members.unsubscribe(`enter`); +space.members.unsubscribe('enter'); ``` -Calling `unsubscribe` with an array of named events will remove all listeners registered for those events. +Calling `unsubscribe()` with an array of named events will remove all listeners registered for those events. ```ts -space.members.unsubscribe([`enter`, `leave`]); +space.members.unsubscribe(['enter', 'leave']); ``` -Calling `unsubscribe` and adding a listener function as the second argument to both of the above will remove only that listener. +Calling `unsubscribe()` and adding a listener function as the second argument to both of the above will remove only that listener. ```ts const listener = () => {}; -space.members.unsubscribe(`update`, listener); -space.members.unsubscribe([`update`], listener); +space.members.unsubscribe('update', listener); +space.members.unsubscribe(['update'], listener); ``` -As with the native DOM API, this only works if the listener is the same reference as the one passed to `subscribe`. +As with the native DOM API, this only works if the listener is the same reference as the one passed to `subscribe()`. From a1b16ee1dcbd4f6bfb6c65fc1cd0c7bb0c6e47d6 Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Thu, 24 Aug 2023 14:48:26 +0100 Subject: [PATCH 010/191] Updated CDN link --- CONTRIBUTING.md | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91cafa36..698ed8d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,7 @@ - The third number represents a patch release, which represents bug-fixes and may be used when no action should be required from users. 1. The commit should update `package.json`, the `Spaces.ts` class containing a `version` property and `package-lock.json`. Running `npm install` after changing `package.json` will update `package-lock.json`. + 1. Update the README.md for any references to the new version, such as the CDN link. 1. Merge the commit into main. 1. Tag a release using [Github releases](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release). The version needs to match the one from the commit. Use the "Generate release notes" button to add changelog notes and update as required. diff --git a/README.md b/README.md index b0a8da9f..b7a6db3b 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): ```html - + ``` After this, instantiate the SDK in the same way as in the NPM option above. From fa570ede525c19683e93f96f73e3dcf5822c93b1 Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Thu, 24 Aug 2023 17:07:15 +0100 Subject: [PATCH 011/191] Ensure we're setting proper error codes The majority of errors we are throwing are default InvalidArgumentError type errors. Otherwise, we only really have two other errors: 1. Attempting to connect to a space where the name is missing 2. Trying to do something when they have entered a space I've copied the way that locks used errors. --- src/Cursors.ts | 3 ++- src/Errors.ts | 20 ++++++++++++++++---- src/Locations.ts | 3 ++- src/Locks.ts | 12 +++++++++--- src/Space.ts | 4 +++- src/Spaces.ts | 3 ++- 6 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 7abdf4c4..1744386f 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -14,6 +14,7 @@ import { CURSOR_UPDATE } from './CursorConstants.js'; import type { CursorsOptions, CursorUpdate } from './types.js'; import type { RealtimeMessage } from './utilities/types.js'; +import { ERR_OUT_OF_SPACE } from './Errors.js'; type CursorsEventMap = { update: CursorUpdate; @@ -48,7 +49,7 @@ export default class Cursors extends EventEmitter { const self = await this.space.members.getSelf(); if (!self) { - throw new Error('Must enter a space before setting a cursor update'); + throw ERR_OUT_OF_SPACE; } const channel = this.getChannel(); diff --git a/src/Errors.ts b/src/Errors.ts index 2b965308..c0a392e2 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -21,26 +21,38 @@ export class ErrorInfo extends Error { } } +export const ERR_SPACE_NAME_MISSING = new ErrorInfo({ + message: 'must have a non-empty name', + code: 101000, + statusCode: 400, +}); + +export const ERR_OUT_OF_SPACE = new ErrorInfo({ + message: 'must enter a space to perform this operation', + code: 101001, + statusCode: 400, +}); + export const ERR_LOCK_REQUEST_EXISTS = new ErrorInfo({ message: 'lock request already exists', - code: 40050, + code: 101002, statusCode: 400, }); export const ERR_LOCK_IS_LOCKED = new ErrorInfo({ message: 'lock is currently locked', - code: 40051, + code: 101003, statusCode: 400, }); export const ERR_LOCK_INVALIDATED = new ErrorInfo({ message: 'lock was invalidated by a concurrent lock request which now holds the lock', - code: 40052, + code: 101004, statusCode: 400, }); export const ERR_LOCK_RELEASED = new ErrorInfo({ message: 'lock was released', - code: 40053, + code: 101005, statusCode: 400, }); diff --git a/src/Locations.ts b/src/Locations.ts index 0564547d..b0901047 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -10,6 +10,7 @@ import EventEmitter, { import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; +import { ERR_OUT_OF_SPACE } from './Errors.js'; type LocationsEventMap = { update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown }; @@ -57,7 +58,7 @@ export default class Locations extends EventEmitter { const self = await this.space.members.getSelf(); if (!self) { - throw new Error('You must enter a space before setting a location.'); + throw ERR_OUT_OF_SPACE; } const update: PresenceMember['data'] = { diff --git a/src/Locks.ts b/src/Locks.ts index 5c9b2492..d486ea87 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -3,7 +3,13 @@ import { Types } from 'ably'; import Space from './Space.js'; import type { Lock, LockRequest, SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; -import { ERR_LOCK_IS_LOCKED, ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS, ERR_LOCK_RELEASED } from './Errors.js'; +import { + ERR_LOCK_IS_LOCKED, + ERR_LOCK_INVALIDATED, + ERR_LOCK_REQUEST_EXISTS, + ERR_LOCK_RELEASED, + ERR_OUT_OF_SPACE, +} from './Errors.js'; import EventEmitter, { InvalidArgumentError, inspect, @@ -69,7 +75,7 @@ export default class Locks extends EventEmitter { async acquire(id: string, opts?: LockOptions): Promise { const self = await this.space.members.getSelf(); if (!self) { - throw new Error('Must enter a space before acquiring a lock'); + throw ERR_OUT_OF_SPACE; } // check there isn't an existing PENDING or LOCKED request for the current @@ -99,7 +105,7 @@ export default class Locks extends EventEmitter { async release(id: string): Promise { const self = await this.space.members.getSelf(); if (!self) { - throw new Error('Must enter a space before acquiring a lock'); + throw ERR_OUT_OF_SPACE; } this.deleteLock(id, self.connectionId); diff --git a/src/Space.ts b/src/Space.ts index af898372..7ef7b156 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -12,6 +12,8 @@ import Cursors from './Cursors.js'; import Members from './Members.js'; import Locks from './Locks.js'; +import { ERR_OUT_OF_SPACE } from './Errors.js'; + import { isFunction, isObject } from './utilities/is.js'; import type { SpaceOptions, SpaceMember, ProfileData } from './types.js'; @@ -168,7 +170,7 @@ class Space extends EventEmitter { const self = await this.members.getSelf(); if (!self) { - throw new Error('You must enter a space before attempting to leave it'); + throw ERR_OUT_OF_SPACE; } const update = { diff --git a/src/Spaces.ts b/src/Spaces.ts index cd0641d3..db589d0b 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -1,4 +1,5 @@ import { Types } from 'ably'; +import { ERR_SPACE_NAME_MISSING } from './Errors.js'; import Space from './Space.js'; @@ -30,7 +31,7 @@ class Spaces { async get(name: string, options?: Subset): Promise { if (typeof name !== 'string' || name.length === 0) { - throw new Error('Spaces must have a non-empty name'); + throw ERR_SPACE_NAME_MISSING; } if (this.spaces[name]) return this.spaces[name]; From e4f7585b6d1838a2f6c4a2e212b178322e60b8af Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Fri, 25 Aug 2023 09:31:43 +0100 Subject: [PATCH 012/191] Ensure proper standard errors are being set Let's ensure we never have a throw that isn't at least a standard generic error code. --- src/Space.ts | 4 +++- src/utilities/EventEmitter.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Space.ts b/src/Space.ts index 7ef7b156..9c3f35dd 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -142,7 +142,9 @@ class Space extends EventEmitter { const self = await this.members.getSelf(); if (!isObject(profileDataOrUpdateFn) && !isFunction(profileDataOrUpdateFn)) { - throw new Error('Space.updateProfileData(): Invalid arguments: ' + inspect([profileDataOrUpdateFn])); + throw new InvalidArgumentError( + 'Space.updateProfileData(): Invalid arguments: ' + inspect([profileDataOrUpdateFn]), + ); } const update = { diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 358b74c6..247645a2 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -278,7 +278,7 @@ export default class EventEmitter { const eventThis = { event: targetState }; if (typeof targetState !== 'string' || typeof currentState !== 'string') { - throw 'whenState requires a valid event String argument'; + throw new InvalidArgumentError('whenState requires a valid event String argument'); } if (typeof listener !== 'function' && Promise) { return new Promise((resolve) => { From 7a986d82607a7a72382f9a2520f309aa361bd216 Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Fri, 25 Aug 2023 10:48:37 +0100 Subject: [PATCH 013/191] fixup! Ensure we're setting proper error codes --- src/Cursors.ts | 4 ++-- src/Errors.ts | 2 +- src/Locations.ts | 4 ++-- src/Locks.ts | 6 +++--- src/Space.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 1744386f..f973e311 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -14,7 +14,7 @@ import { CURSOR_UPDATE } from './CursorConstants.js'; import type { CursorsOptions, CursorUpdate } from './types.js'; import type { RealtimeMessage } from './utilities/types.js'; -import { ERR_OUT_OF_SPACE } from './Errors.js'; +import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; type CursorsEventMap = { update: CursorUpdate; @@ -49,7 +49,7 @@ export default class Cursors extends EventEmitter { const self = await this.space.members.getSelf(); if (!self) { - throw ERR_OUT_OF_SPACE; + throw ERR_NOT_ENTERED_SPACE; } const channel = this.getChannel(); diff --git a/src/Errors.ts b/src/Errors.ts index c0a392e2..c74070da 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -27,7 +27,7 @@ export const ERR_SPACE_NAME_MISSING = new ErrorInfo({ statusCode: 400, }); -export const ERR_OUT_OF_SPACE = new ErrorInfo({ +export const ERR_NOT_ENTERED_SPACE = new ErrorInfo({ message: 'must enter a space to perform this operation', code: 101001, statusCode: 400, diff --git a/src/Locations.ts b/src/Locations.ts index b0901047..9007ee1f 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -10,7 +10,7 @@ import EventEmitter, { import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; -import { ERR_OUT_OF_SPACE } from './Errors.js'; +import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; type LocationsEventMap = { update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown }; @@ -58,7 +58,7 @@ export default class Locations extends EventEmitter { const self = await this.space.members.getSelf(); if (!self) { - throw ERR_OUT_OF_SPACE; + throw ERR_NOT_ENTERED_SPACE; } const update: PresenceMember['data'] = { diff --git a/src/Locks.ts b/src/Locks.ts index d486ea87..06a8144a 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -8,7 +8,7 @@ import { ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS, ERR_LOCK_RELEASED, - ERR_OUT_OF_SPACE, + ERR_NOT_ENTERED_SPACE, } from './Errors.js'; import EventEmitter, { InvalidArgumentError, @@ -75,7 +75,7 @@ export default class Locks extends EventEmitter { async acquire(id: string, opts?: LockOptions): Promise { const self = await this.space.members.getSelf(); if (!self) { - throw ERR_OUT_OF_SPACE; + throw ERR_NOT_ENTERED_SPACE; } // check there isn't an existing PENDING or LOCKED request for the current @@ -105,7 +105,7 @@ export default class Locks extends EventEmitter { async release(id: string): Promise { const self = await this.space.members.getSelf(); if (!self) { - throw ERR_OUT_OF_SPACE; + throw ERR_NOT_ENTERED_SPACE; } this.deleteLock(id, self.connectionId); diff --git a/src/Space.ts b/src/Space.ts index 9c3f35dd..1b8d7654 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -12,7 +12,7 @@ import Cursors from './Cursors.js'; import Members from './Members.js'; import Locks from './Locks.js'; -import { ERR_OUT_OF_SPACE } from './Errors.js'; +import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; import { isFunction, isObject } from './utilities/is.js'; @@ -172,7 +172,7 @@ class Space extends EventEmitter { const self = await this.members.getSelf(); if (!self) { - throw ERR_OUT_OF_SPACE; + throw ERR_NOT_ENTERED_SPACE; } const update = { From 695e0dc0fa8f8e8cd8c271cbb4f2754e158776c4 Mon Sep 17 00:00:00 2001 From: Laura Martin Date: Fri, 25 Aug 2023 10:49:19 +0100 Subject: [PATCH 014/191] fixup! Ensure we're setting proper error codes --- src/Errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Errors.ts b/src/Errors.ts index c74070da..82173038 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -22,7 +22,7 @@ export class ErrorInfo extends Error { } export const ERR_SPACE_NAME_MISSING = new ErrorInfo({ - message: 'must have a non-empty name', + message: 'must have a non-empty name for the space', code: 101000, statusCode: 400, }); From 41255bc5f376b0b0b47d434a418e319d2c11c53b Mon Sep 17 00:00:00 2001 From: Dominik Date: Fri, 25 Aug 2023 14:42:29 +0200 Subject: [PATCH 015/191] Locking demo (#111) * Fix locks state not being propagated on updates * Add simple locking demo * feat: make titles editable * feat: background,cursor-disabled, rewind * feat: improvements * fix: add global state for slides data * fix: lock losing * chore: cleanup * fix: too slow updates * fix: merge conflicts * fix: locking demo on locations * chore: add remove lock on click outside and update `maxlength` * chore: rollback workarounds * fix: use locking * chore: improve max length implementation * chore: remove unlocking on slide click * chore: remove unlocking on slide click * chore: renaming based on review feedback * chore: renaming based on review feedback * fix: max length for text editing --------- Co-authored-by: evgeny --- demo/package-lock.json | 428 +++++++++++++++++++-- demo/package.json | 10 +- demo/src/App.tsx | 5 +- demo/src/components/Avatar.tsx | 2 +- demo/src/components/AvatarInfo.tsx | 2 +- demo/src/components/EditableText.tsx | 65 ++++ demo/src/components/Image.tsx | 20 +- demo/src/components/Paragraph.tsx | 83 ++-- demo/src/components/PreviewContext.tsx | 13 + demo/src/components/SlidePreview.tsx | 4 +- demo/src/components/SlidesStateContext.tsx | 24 ++ demo/src/components/SpacesContext.tsx | 4 +- demo/src/components/StickyLabel.tsx | 18 + demo/src/components/Title.tsx | 81 ++-- demo/src/components/slides.tsx | 35 +- demo/src/components/svg/LockedFilled.tsx | 19 + demo/src/hooks/index.ts | 1 + demo/src/hooks/useElementSelect.ts | 60 ++- demo/src/hooks/useLock.ts | 56 +++ demo/src/hooks/useMembers.ts | 31 +- demo/src/hooks/useSlideElementContent.ts | 13 + demo/src/hooks/useTextComponentLock.ts | 55 +++ demo/src/main.tsx | 10 +- demo/src/utils/active-member.ts | 11 +- demo/src/utils/locking.ts | 9 + demo/src/utils/types.d.ts | 2 +- 26 files changed, 923 insertions(+), 138 deletions(-) create mode 100644 demo/src/components/EditableText.tsx create mode 100644 demo/src/components/PreviewContext.tsx create mode 100644 demo/src/components/SlidesStateContext.tsx create mode 100644 demo/src/components/StickyLabel.tsx create mode 100644 demo/src/components/svg/LockedFilled.tsx create mode 100644 demo/src/hooks/useLock.ts create mode 100644 demo/src/hooks/useSlideElementContent.ts create mode 100644 demo/src/hooks/useTextComponentLock.ts create mode 100644 demo/src/utils/locking.ts diff --git a/demo/package-lock.json b/demo/package-lock.json index dd755286..e7fa68e1 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -8,8 +8,9 @@ "name": "demo", "version": "1.0.0", "dependencies": { - "@ably-labs/spaces": "^0.0.12-alpha", - "ably": "^1.2.41", + "@ably-labs/react-hooks": "file:../../react-hooks", + "@ably-labs/spaces": "file:../", + "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", "lodash.assign": "^4.2.0", @@ -18,7 +19,9 @@ "nanoid": "^4.0.2", "random-words": "^2.0.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-contenteditable": "^3.3.7", + "react-dom": "^18.2.0", + "sanitize-html": "^2.11.0" }, "devDependencies": { "@types/lodash.assign": "^4.2.7", @@ -27,6 +30,7 @@ "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-helmet": "^6.1.6", + "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", @@ -42,6 +46,56 @@ "vite": "^4.4.5" } }, + "..": { + "name": "@ably-labs/spaces", + "version": "0.0.12", + "license": "ISC", + "dependencies": { + "ably": "^1.2.43", + "nanoid": "^4.0.2" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "@vitest/coverage-c8": "^0.28.4", + "eslint": "^8.33.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsdoc": "^39.8.0", + "eslint-plugin-security": "^1.7.1", + "husky": "^8.0.0", + "mock-socket": "^9.1.5", + "prettier": "^2.8.3", + "rollup": "^3.28.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "vitest": "^0.29.8" + } + }, + "../../react-hooks": { + "version": "2.1.1", + "license": "ISC", + "devDependencies": { + "@testing-library/react": "^13.3.0", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.4.0", + "@vitejs/plugin-react": "^1.3.2", + "eslint": "^8.45.0", + "eslint-plugin-import": "^2.28.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jsdom": "^20.0.0", + "prettier": "^3.0.0", + "react": ">=18.1.0", + "react-dom": ">=18.1.0", + "typescript": ">=4.4.4", + "vitest": "^0.18.0" + }, + "peerDependencies": { + "ably": "^1.2.27", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -51,13 +105,13 @@ "node": ">=0.10.0" } }, + "node_modules/@ably-labs/react-hooks": { + "resolved": "../../react-hooks", + "link": true + }, "node_modules/@ably-labs/spaces": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@ably-labs/spaces/-/spaces-0.0.12.tgz", - "integrity": "sha512-mNPtsltJPVT5sz/TYfEgIOlZUarWMD4TH4JnPtd2mHqS+9v+gnfKQ5tdEvTLMFvv0jEc1ZonUgNkU/EUsk4yYA==", - "dependencies": { - "ably": "^1.2.39" - } + "resolved": "..", + "link": true }, "node_modules/@ably/msgpack-js": { "version": "0.4.0", @@ -1117,6 +1171,15 @@ "@types/node": "*" } }, + "node_modules/@types/sanitize-html": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz", + "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -1338,9 +1401,9 @@ } }, "node_modules/ably": { - "version": "1.2.42", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.42.tgz", - "integrity": "sha512-dUnza7cERLWaDa/2pLVXtU2PJoU5k/t6g9sQZI1dgWC5Vok39nE6tf/xH2Rat7PJs7pXl9hLpkg1AhS4Xwd2/w==", + "version": "1.2.43", + "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", + "integrity": "sha512-HZ99Nd98KzYToNUD4+ysHp4+vMp1NmYTi59yqGpejHo/VffTgg0pereoib0nRRAHYUhGUGys5HGwR5yHYESWDA==", "dependencies": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", @@ -1825,6 +1888,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -1869,6 +1940,57 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.469", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.469.tgz", @@ -1883,6 +2005,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.0.tgz", @@ -2200,8 +2333,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.1", @@ -2485,6 +2617,24 @@ "node": ">=4" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -2615,6 +2765,14 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -18859,7 +19017,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -18948,6 +19105,11 @@ "node": ">=6" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -18993,8 +19155,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -19030,7 +19191,6 @@ "version": "8.4.27", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -19161,7 +19321,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, "funding": [ { "type": "github", @@ -19184,6 +19343,16 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -19252,6 +19421,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-contenteditable": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz", + "integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -19264,6 +19445,11 @@ "react": "^18.2.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -19400,6 +19586,30 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -19480,7 +19690,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -20272,12 +20481,44 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, + "@ably-labs/react-hooks": { + "version": "file:../../react-hooks", + "requires": { + "@testing-library/react": "^13.3.0", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.4.0", + "@vitejs/plugin-react": "^1.3.2", + "eslint": "^8.45.0", + "eslint-plugin-import": "^2.28.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jsdom": "^20.0.0", + "prettier": "^3.0.0", + "react": ">=18.1.0", + "react-dom": ">=18.1.0", + "typescript": ">=4.4.4", + "vitest": "^0.18.0" + } + }, "@ably-labs/spaces": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@ably-labs/spaces/-/spaces-0.0.12.tgz", - "integrity": "sha512-mNPtsltJPVT5sz/TYfEgIOlZUarWMD4TH4JnPtd2mHqS+9v+gnfKQ5tdEvTLMFvv0jEc1ZonUgNkU/EUsk4yYA==", + "version": "file:..", "requires": { - "ably": "^1.2.39" + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "@vitest/coverage-c8": "^0.28.4", + "ably": "^1.2.43", + "eslint": "^8.33.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsdoc": "^39.8.0", + "eslint-plugin-security": "^1.7.1", + "husky": "^8.0.0", + "mock-socket": "^9.1.5", + "nanoid": "^4.0.2", + "prettier": "^2.8.3", + "rollup": "^3.28.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "vitest": "^0.29.8" } }, "@ably/msgpack-js": { @@ -20981,6 +21222,15 @@ "@types/node": "*" } }, + "@types/sanitize-html": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz", + "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", + "dev": true, + "requires": { + "htmlparser2": "^8.0.0" + } + }, "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -21107,9 +21357,9 @@ } }, "ably": { - "version": "1.2.42", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.42.tgz", - "integrity": "sha512-dUnza7cERLWaDa/2pLVXtU2PJoU5k/t6g9sQZI1dgWC5Vok39nE6tf/xH2Rat7PJs7pXl9hLpkg1AhS4Xwd2/w==", + "version": "1.2.43", + "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", + "integrity": "sha512-HZ99Nd98KzYToNUD4+ysHp4+vMp1NmYTi59yqGpejHo/VffTgg0pereoib0nRRAHYUhGUGys5HGwR5yHYESWDA==", "requires": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", @@ -21445,6 +21695,11 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, "defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -21480,6 +21735,39 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "electron-to-chromium": { "version": "1.4.469", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.469.tgz", @@ -21494,6 +21782,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "esbuild": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.0.tgz", @@ -21721,8 +22014,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.3.1", @@ -21934,6 +22226,17 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -22031,6 +22334,11 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -34129,8 +34437,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-hash": { "version": "3.0.0", @@ -34192,6 +34499,11 @@ "callsites": "^3.0.0" } }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -34225,8 +34537,7 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "picomatch": { "version": "2.3.1", @@ -34250,7 +34561,6 @@ "version": "8.4.27", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", - "dev": true, "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -34260,8 +34570,7 @@ "nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" } } }, @@ -34326,6 +34635,16 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -34368,6 +34687,15 @@ "loose-envify": "^1.1.0" } }, + "react-contenteditable": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz", + "integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==", + "requires": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + } + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -34377,6 +34705,11 @@ "scheduler": "^0.23.0" } }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -34464,6 +34797,26 @@ "queue-microtask": "^1.2.2" } }, + "sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + } + } + }, "scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -34527,8 +34880,7 @@ "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, "strip-ansi": { "version": "6.0.1", diff --git a/demo/package.json b/demo/package.json index 880cf545..4ca6863b 100644 --- a/demo/package.json +++ b/demo/package.json @@ -12,8 +12,9 @@ "deploy:production": "npm run build && netlify deploy --prod" }, "dependencies": { - "@ably-labs/spaces": "^0.0.12-alpha", - "ably": "^1.2.41", + "@ably-labs/react-hooks": "file:../../react-hooks", + "@ably-labs/spaces": "file:../", + "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", "lodash.assign": "^4.2.0", @@ -22,7 +23,9 @@ "nanoid": "^4.0.2", "random-words": "^2.0.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-contenteditable": "^3.3.7", + "react-dom": "^18.2.0", + "sanitize-html": "^2.11.0" }, "devDependencies": { "@types/lodash.assign": "^4.2.7", @@ -31,6 +34,7 @@ "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-helmet": "^6.1.6", + "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", diff --git a/demo/src/App.tsx b/demo/src/App.tsx index a58874d6..d824ad99 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -3,6 +3,7 @@ import { useContext, useEffect } from 'react'; import { Header, SlideMenu, SpacesContext, CurrentSlide, AblySvg, slides } from './components'; import { getRandomName, getRandomColor } from './utils'; import { useMembers } from './hooks'; +import { PreviewProvider } from './components/PreviewContext.tsx'; const App = () => { const space = useContext(SpacesContext); @@ -32,7 +33,9 @@ const App = () => { id="feature-display" className="absolute gap-12 bg-[#F7F6F9] w-full h-[calc(100%-80px)] -z-10 overflow-y-hidden overflow-x-hidden flex justify-between min-w-[375px] xs:flex-col md:flex-row" > - + + + diff --git a/demo/src/components/Avatar.tsx b/demo/src/components/Avatar.tsx index 820c80b1..c9d84ab0 100644 --- a/demo/src/components/Avatar.tsx +++ b/demo/src/components/Avatar.tsx @@ -1,5 +1,5 @@ import cn from 'classnames'; -import { type SpaceMember } from '../../../src/types'; +import { type SpaceMember } from '@ably-labs/spaces'; import { AvatarInfo } from './AvatarInfo'; import { LightningSvg } from './svg'; diff --git a/demo/src/components/AvatarInfo.tsx b/demo/src/components/AvatarInfo.tsx index ec6dd6fa..ae8180ef 100644 --- a/demo/src/components/AvatarInfo.tsx +++ b/demo/src/components/AvatarInfo.tsx @@ -4,7 +4,7 @@ import cn from 'classnames'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; -import { type SpaceMember } from '../../../src/types'; +import { type SpaceMember } from '@ably-labs/spaces'; import { type ProfileData } from '../utils/types'; type Props = Omit & { diff --git a/demo/src/components/EditableText.tsx b/demo/src/components/EditableText.tsx new file mode 100644 index 00000000..238990ac --- /dev/null +++ b/demo/src/components/EditableText.tsx @@ -0,0 +1,65 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; +import sanitize from 'sanitize-html'; + +interface EditableTextProps extends Omit, 'onChange' | 'children'> { + as?: string; + disabled: boolean; + value: string; + onChange(nextValue: string): void; + maxlength?: number; + className?: string; +} + +export const EditableText: React.FC = ({ + as, + disabled, + maxlength = 300, + value, + onChange, + ...restProps +}) => { + const elementRef = useRef(null); + const handleTextChange = useCallback( + (evt: ContentEditableEvent) => { + const nextValue = sanitize(evt.target.value, { + allowedTags: [], + }); + + if (nextValue.length > maxlength) { + onChange(value); + } else { + onChange(nextValue); + } + }, + [onChange, value, maxlength], + ); + + useEffect(() => { + const element = elementRef.current; + if (!disabled && element) { + moveCursorToEnd(element); + } + }, [disabled]); + + return ( + + ); +}; + +const moveCursorToEnd = (el: HTMLElement) => { + el.focus(); + const range = document.createRange(); + range.selectNodeContents(el); + range.collapse(false); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); +}; diff --git a/demo/src/components/Image.tsx b/demo/src/components/Image.tsx index 06fa9149..3a91253b 100644 --- a/demo/src/components/Image.tsx +++ b/demo/src/components/Image.tsx @@ -8,21 +8,23 @@ interface Props extends React.HTMLAttributes { className?: string; id: string; slide: string; + locatable?: boolean; } -export const Image = ({ src, children, className, id, slide }: Props) => { - const { members } = useMembers(); - const { handleSelect } = useElementSelect(id); +export const Image = ({ src, children, className, id, slide, locatable = true }: Props) => { + const { members, self } = useMembers(); + const { handleSelect } = useElementSelect(id, false); const activeMember = findActiveMember(id, slide, members); - const name = getMemberFirstName(activeMember); - const outlineClasses = getOutlineClasses(activeMember); + const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember); + const memberName = getMemberFirstName(activeMember); + const label = self?.connectionId === activeMember?.connectionId ? 'You' : memberName; return (
{ data-id="slide-image-placeholder" className="cursor-pointer block" src={src} - onClick={handleSelect} + onClick={locatable ? handleSelect : undefined} /> {children ? children : null}
diff --git a/demo/src/components/Paragraph.tsx b/demo/src/components/Paragraph.tsx index 5c559269..02512f06 100644 --- a/demo/src/components/Paragraph.tsx +++ b/demo/src/components/Paragraph.tsx @@ -1,36 +1,73 @@ +import React, { useRef } from 'react'; import cn from 'classnames'; -import { useElementSelect, useMembers } from '../hooks'; -import { findActiveMember, getMemberFirstName, getOutlineClasses } from '../utils'; +import { getMemberFirstName, getOutlineClasses } from '../utils'; +import { StickyLabel } from './StickyLabel'; +import { LockFilledSvg } from './svg/LockedFilled.tsx'; +import { EditableText } from './EditableText.tsx'; +import { useTextComponentLock } from '../hooks/useTextComponentLock.ts'; interface Props extends React.HTMLAttributes { id: string; slide: string; variant?: 'regular' | 'aside'; + children: string; + maxlength?: number; } -export const Paragraph = ({ variant = 'regular', id, slide, className, ...props }: Props) => { - const { members } = useMembers(); - const { handleSelect } = useElementSelect(id); - const activeMember = findActiveMember(id, slide, members); - const name = getMemberFirstName(activeMember); - const outlineClasses = getOutlineClasses(activeMember); +export const Paragraph = ({ + variant = 'regular', + id, + slide, + className, + children, + maxlength = 300, + ...props +}: Props) => { + const containerRef = useRef(null); + const { content, activeMember, locked, lockedByYou, editIsNotAllowed, handleSelect, handleContentUpdate } = + useTextComponentLock({ + id, + slide, + defaultText: children, + containerRef, + }); + const memberName = getMemberFirstName(activeMember); + const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember); return ( -

+ className="relative" + onClick={locked ? undefined : handleSelect} + > + + {lockedByYou ? 'You' : memberName} + {editIsNotAllowed && } + + + ); }; diff --git a/demo/src/components/PreviewContext.tsx b/demo/src/components/PreviewContext.tsx new file mode 100644 index 00000000..81066d95 --- /dev/null +++ b/demo/src/components/PreviewContext.tsx @@ -0,0 +1,13 @@ +import React, { useContext } from 'react'; + +interface PreviewContextProviderProps { + preview: boolean; + children: React.ReactNode; +} +const PreviewContext = React.createContext(false); + +export const PreviewProvider: React.FC = ({ preview, children }) => ( + {children} +); + +export const usePreview = () => useContext(PreviewContext); diff --git a/demo/src/components/SlidePreview.tsx b/demo/src/components/SlidePreview.tsx index 46ef3307..b1af292e 100644 --- a/demo/src/components/SlidePreview.tsx +++ b/demo/src/components/SlidePreview.tsx @@ -15,7 +15,7 @@ export const SlidePreview = ({ children, index }: SlidePreviewProps) => { const membersOnASlide = (members || []).filter(({ location }) => location?.slide === `${index}`); const isActive = self?.location?.slide === `${index}`; - const handleSlideClick = () => { + const handleSlideClick = async () => { if (!space || !self) return; space.locations.set({ slide: `${index}`, element: null }); }; @@ -43,7 +43,7 @@ export const SlidePreview = ({ children, index }: SlidePreviewProps) => {

{children}
diff --git a/demo/src/components/SlidesStateContext.tsx b/demo/src/components/SlidesStateContext.tsx new file mode 100644 index 00000000..1680223a --- /dev/null +++ b/demo/src/components/SlidesStateContext.tsx @@ -0,0 +1,24 @@ +import React, { useMemo, useState } from 'react'; + +interface SlidesStateContextProps { + slidesState: Record; + setContent(id: string, nextContent: string): void; +} +export const SlidesStateContext = React.createContext({ + slidesState: {}, + setContent: () => {}, +}); + +export const SlidesStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [slidesState, setSlidesState] = useState>({}); + const value = useMemo( + () => ({ + slidesState, + setContent: (id: string, nextContent: string) => { + setSlidesState((prevState) => ({ ...prevState, [id]: nextContent })); + }, + }), + [slidesState, setSlidesState], + ); + return {children}; +}; diff --git a/demo/src/components/SpacesContext.tsx b/demo/src/components/SpacesContext.tsx index 1c777450..a4d7074f 100644 --- a/demo/src/components/SpacesContext.tsx +++ b/demo/src/components/SpacesContext.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Spaces, { type Space } from '../../../src/index'; +import Spaces, { type Space } from '@ably-labs/spaces'; import { Realtime } from 'ably'; import { nanoid } from 'nanoid'; @@ -7,7 +7,7 @@ import { getSpaceNameFromUrl } from '../utils'; const clientId = nanoid(); -const ably = new Realtime.Promise({ +export const ably = new Realtime.Promise({ authUrl: `/api/ably-token-request?clientId=${clientId}`, clientId, }); diff --git a/demo/src/components/StickyLabel.tsx b/demo/src/components/StickyLabel.tsx new file mode 100644 index 00000000..7511c826 --- /dev/null +++ b/demo/src/components/StickyLabel.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +interface StickyLabelProps { + visible: boolean; + className?: string; + children: React.ReactNode; +} +export const StickyLabel: React.FC = ({ visible, className, children }) => { + if (!visible) return null; + + return ( +
+ {children} +
+ ); +}; diff --git a/demo/src/components/Title.tsx b/demo/src/components/Title.tsx index 21c79b9c..2c24d714 100644 --- a/demo/src/components/Title.tsx +++ b/demo/src/components/Title.tsx @@ -1,39 +1,68 @@ +import React, { useRef } from 'react'; import cn from 'classnames'; -import { useElementSelect, useMembers } from '../hooks'; -import { findActiveMember, getMemberFirstName, getOutlineClasses } from '../utils'; + +import { getMemberFirstName, getOutlineClasses } from '../utils'; +import { LockFilledSvg } from './svg/LockedFilled.tsx'; +import { StickyLabel } from './StickyLabel.tsx'; +import { EditableText } from './EditableText.tsx'; +import { useTextComponentLock } from '../hooks/useTextComponentLock.ts'; interface Props extends React.HTMLAttributes { id: string; slide: string; variant?: 'h1' | 'h2' | 'h3'; + children: string; + maxlength?: number; } -export const Title = ({ variant = 'h1', className, id, slide, ...props }: Props) => { - const Component = variant; - const { members } = useMembers(); - const { handleSelect } = useElementSelect(id); - const activeMember = findActiveMember(id, slide, members); - const name = getMemberFirstName(activeMember); - const outlineClasses = getOutlineClasses(activeMember); +export const Title = ({ variant = 'h1', className, id, slide, children, maxlength = 70, ...props }: Props) => { + const containerRef = useRef(null); + const { content, activeMember, locked, lockedByYou, editIsNotAllowed, handleSelect, handleContentUpdate } = + useTextComponentLock({ + id, + slide, + defaultText: children, + containerRef, + }); + const memberName = getMemberFirstName(activeMember); + const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember); return ( - + className="relative" + onClick={locked ? undefined : handleSelect} + > + + {lockedByYou ? 'You' : memberName} + {editIsNotAllowed && } + + + ); }; diff --git a/demo/src/components/slides.tsx b/demo/src/components/slides.tsx index a96a0778..d5b2f781 100644 --- a/demo/src/components/slides.tsx +++ b/demo/src/components/slides.tsx @@ -5,7 +5,10 @@ import { Image } from './Image'; export const slides = [ { children: ( -
+
<div className="absolute w-[176px] left-[20px] top-[86px] md:top-20 md:left-6 md:right-6 md:mx-auto"> <Title variant="h2" id="4" slide="0" + maxlength={15} > Contrast @@ -40,6 +45,7 @@ export const slides = [ variant="aside" id="5" slide="0" + maxlength={105} > When a design uses several elements, the goal is to make each one distinct. @@ -49,12 +55,14 @@ export const slides = [ src="/repetition.svg" id="6" slide="0" + locatable={false} >
Repetition @@ -62,6 +70,7 @@ export const slides = [ variant="aside" id="8" slide="0" + maxlength={105} > Repetition helps designers establish relationships, develop organization and strengthen unity. @@ -71,6 +80,7 @@ export const slides = [ src="/alignment.svg" id="9" slide="0" + locatable={false} >
Alignment @@ -87,6 +98,7 @@ export const slides = [ variant="aside" id="11" slide="0" + maxlength={105} > Alignment creates a clean, sophisticated look. All elements should relate to all others in some way. @@ -96,6 +108,7 @@ export const slides = [ src="/proximity.svg" id="12" slide="0" + locatable={false} >
Proximity @@ -112,6 +126,7 @@ export const slides = [ variant="aside" id="14" slide="0" + maxlength={105} > When items are grouped, they become a single visual unit, rather than several separate entities. @@ -123,7 +138,10 @@ export const slides = [ }, { children: ( -
+
- No one likes boring text blocks on a website. And{' '} - <span className="text-ably-avatar-stack-demo-slide-title-highlight font-semibold">images and icons</span>{' '} - are the fastest way to get information. + No one likes boring text blocks on a website. And images and icons are the fastest way to get information. </Paragraph> <Paragraph id="4" slide="1" > - But <span className="text-ably-avatar-stack-demo-slide-title-highlight font-semibold">don't overdo it</span> - . If you can't explain for what purpose you put this line or icon, it's better to abandon it. + But don't overdo it. If you can't explain for what purpose you put this line or icon, it's better to abandon + it. </Paragraph> </div> <Image @@ -168,7 +184,10 @@ export const slides = [ }, { children: ( - <div className="grid grid-cols-1 md:grid-cols-2 gap-8 h-full items-center p-8"> + <div + key={2} + className="grid grid-cols-1 md:grid-cols-2 gap-8 h-full items-center p-8" + > <div> <Title variant="h1" diff --git a/demo/src/components/svg/LockedFilled.tsx b/demo/src/components/svg/LockedFilled.tsx new file mode 100644 index 00000000..68762802 --- /dev/null +++ b/demo/src/components/svg/LockedFilled.tsx @@ -0,0 +1,19 @@ +import { SVGProps } from 'react'; + +export const LockFilledSvg = (props: SVGProps<SVGSVGElement>) => { + return ( + <svg + width="1em" + height="1em" + viewBox="0 0 16 16" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <path + d="M12.0003 5.8334H11.3337V4.50007C11.3337 2.66008 9.84032 1.16675 8.00033 1.16675C6.16033 1.16675 4.66699 2.66008 4.66699 4.50007V5.8334H4.00033C3.26699 5.8334 2.66699 6.43339 2.66699 7.16672V13.8334C2.66699 14.5667 3.26699 15.1667 4.00033 15.1667H12.0003C12.7337 15.1667 13.3337 14.5667 13.3337 13.8334V7.16672C13.3337 6.43339 12.7337 5.8334 12.0003 5.8334ZM8.00033 11.8334C7.26699 11.8334 6.66699 11.2334 6.66699 10.5C6.66699 9.76671 7.26699 9.16671 8.00033 9.16671C8.73366 9.16671 9.33366 9.76671 9.33366 10.5C9.33366 11.2334 8.73366 11.8334 8.00033 11.8334ZM6.00033 5.8334V4.50007C6.00033 3.39341 6.89366 2.50008 8.00033 2.50008C9.10699 2.50008 10.0003 3.39341 10.0003 4.50007V5.8334H6.00033Z" + fill="currentColor" + /> + </svg> + ); +}; diff --git a/demo/src/hooks/index.ts b/demo/src/hooks/index.ts index e39c8f68..b2ab5dab 100644 --- a/demo/src/hooks/index.ts +++ b/demo/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from './useMembers'; export * from './useElementSelect'; export * from './useTrackCursor'; +export * from './useLock'; diff --git a/demo/src/hooks/useElementSelect.ts b/demo/src/hooks/useElementSelect.ts index ff6c7ac4..be873b66 100644 --- a/demo/src/hooks/useElementSelect.ts +++ b/demo/src/hooks/useElementSelect.ts @@ -1,15 +1,67 @@ -import { useContext } from 'react'; +import { MutableRefObject, useContext, useEffect } from 'react'; import { SpacesContext } from '../components'; import { useMembers } from './useMembers'; -export const useElementSelect = (element?: string) => { +import { buildLockId, releaseMyLocks } from '../utils/locking'; +import { Member } from '../utils/types'; + +export const useElementSelect = (element?: string, lockable: boolean = true) => { const space = useContext(SpacesContext); const { self } = useMembers(); - const handleSelect = () => { + const handleSelect = async () => { if (!space || !self) return; - space.locations.set({ slide: self.location?.slide, element }); + + if (lockable) { + const lockId = buildLockId(self.location?.slide, element); + const lock = space.locks.get(lockId); + + if (!lock) { + // The lock is not set but we enter the location optimistically + await space.locations.set({ slide: self.location?.slide, element }); + // TODO delete this workaround when spaces API is ready + await delay(60); + await space.locks.acquire(lockId); + } + } else { + space.locations.set({ slide: self.location?.slide, element }); + } }; return { handleSelect }; }; + +export const useClickOutside = (ref: MutableRefObject<HTMLElement | null>, self?: Member, enabled?: boolean) => { + const space = useContext(SpacesContext); + + useEffect(() => { + if (!enabled) return; + const handleClick = async (e: DocumentEventMap['click']) => { + const clickedOutside = !ref.current?.contains(e.target as Node); + if (clickedOutside && space && self) { + await space.locations.set({ slide: self.location?.slide, element: undefined }); + // TODO delete this workaround when spaces API is ready + await delay(60); + await releaseMyLocks(space, self); + } + }; + + document.addEventListener('click', handleClick, true); + + return () => { + document.removeEventListener('click', handleClick, true); + }; + }, [space, self, enabled]); +}; + +export const useClearOnFailedLock = (lockConflict: boolean, self?: Member) => { + const space = useContext(SpacesContext); + + useEffect(() => { + if (lockConflict) { + space?.locations.set({ slide: self?.location?.slide, element: undefined }); + } + }, [lockConflict]); +}; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/demo/src/hooks/useLock.ts b/demo/src/hooks/useLock.ts new file mode 100644 index 00000000..356ca349 --- /dev/null +++ b/demo/src/hooks/useLock.ts @@ -0,0 +1,56 @@ +import { useContext, useEffect, useState } from 'react'; + +import { type Lock, LockStatus } from '@ably-labs/spaces'; + +import { SpacesContext } from '../components'; +import { buildLockId } from '../utils/locking'; +import { isMember } from '../hooks'; + +import { type Member } from '../utils/types'; + +export const useLock = (slide: string, id: string): { status?: string; member?: Member } => { + const space = useContext(SpacesContext); + const locationLockId = buildLockId(slide, id); + const [status, setStatus] = useState<LockStatus | undefined>(undefined); + const [member, setMember] = useState<Member | undefined>(undefined); + + useEffect(() => { + if (!space) return; + + const handler = (lock: Lock) => { + if (lock.request.id !== locationLockId) return; + + setStatus(lock.request.status); + + if (isMember(lock.member)) { + setMember(lock.member); + } + }; + + space.locks.subscribe('update', handler); + + return () => { + space?.locks.unsubscribe('update', handler); + }; + }, [space, slide, id]); + + useEffect(() => { + if (status !== undefined) return; + const lock = space?.locks.get(locationLockId); + if (lock) { + setMember(lock.member as any); + setStatus(lock.request.status); + } + }, [status]); + + return { status, member }; +}; + +export const useLockStatus = (slide: string, id: string, selfConnectionId?: string) => { + const { member, status } = useLock(slide, id); + + const locked = status === 'locked'; + const lockedByYou = locked && member?.connectionId === selfConnectionId; + + return { locked, lockedByYou }; +}; diff --git a/demo/src/hooks/useMembers.ts b/demo/src/hooks/useMembers.ts index 31442505..f7d5f717 100644 --- a/demo/src/hooks/useMembers.ts +++ b/demo/src/hooks/useMembers.ts @@ -1,10 +1,10 @@ import { useEffect, useState, useContext } from 'react'; -import { type SpaceMember } from '../../../src/types'; +import { type SpaceMember } from '@ably-labs/spaces'; import { SpacesContext } from '../components'; import { type Member } from '../utils/types'; -const isMember = (obj: unknown): obj is Member => { +export const isMember = (obj: unknown): obj is Member => { return !!(obj as Member)?.profileData?.name && !!(obj as Member)?.profileData?.color; }; @@ -24,6 +24,20 @@ export const useMembers: () => Partial<{ self?: Member; others: Member[]; member useEffect(() => { if (!space) return; + const handler = ({ members }: { members: SpaceMember[] }) => + (async () => { + const self = await space.members.getSelf(); + + if (isMember(self)) { + setSelf(self); + } + + if (areMembers(members)) { + setMembers([...members]); + setOthers(membersToOthers([...members], self)); + } + })(); + const init = async () => { const initSelf = await space.members.getSelf(); const initMembers = await space.members.getAll(); @@ -37,19 +51,6 @@ export const useMembers: () => Partial<{ self?: Member; others: Member[]; member setOthers(membersToOthers(initMembers, initSelf)); } - const handler = async ({ members }: { members: SpaceMember[] }) => { - const self = await space.members.getSelf(); - - if (isMember(self)) { - setSelf(self); - } - - if (areMembers(members)) { - setMembers([...members]); - setOthers(membersToOthers([...members], self)); - } - }; - space.subscribe('update', handler); }; diff --git a/demo/src/hooks/useSlideElementContent.ts b/demo/src/hooks/useSlideElementContent.ts new file mode 100644 index 00000000..5ef97f84 --- /dev/null +++ b/demo/src/hooks/useSlideElementContent.ts @@ -0,0 +1,13 @@ +import { useCallback, useContext } from 'react'; +import { SlidesStateContext } from '../components/SlidesStateContext.tsx'; + +export const useSlideElementContent = (id: string, defaultContent: string) => { + const { slidesState, setContent } = useContext(SlidesStateContext); + const updateContent = useCallback( + (nextContent: string) => { + setContent(id, nextContent); + }, + [id], + ); + return [slidesState[id] ?? defaultContent, updateContent] as const; +}; diff --git a/demo/src/hooks/useTextComponentLock.ts b/demo/src/hooks/useTextComponentLock.ts new file mode 100644 index 00000000..e67c5490 --- /dev/null +++ b/demo/src/hooks/useTextComponentLock.ts @@ -0,0 +1,55 @@ +import { MutableRefObject, useCallback } from 'react'; +import { useChannel } from '@ably-labs/react-hooks'; +import { findActiveMember, getSpaceNameFromUrl } from '../utils'; +import { buildLockId } from '../utils/locking.ts'; +import { usePreview } from '../components/PreviewContext.tsx'; +import { useMembers } from './useMembers.ts'; +import { useClearOnFailedLock, useClickOutside, useElementSelect } from './useElementSelect.ts'; +import { useLockStatus } from './useLock.ts'; +import { useSlideElementContent } from './useSlideElementContent.ts'; + +interface UseTextComponentLockArgs { + id: string; + slide: string; + defaultText: string; + containerRef: MutableRefObject<HTMLElement | null>; +} +export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: UseTextComponentLockArgs) => { + const spaceName = getSpaceNameFromUrl(); + const { members, self } = useMembers(); + const activeMember = findActiveMember(id, slide, members); + const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId); + const lockId = buildLockId(slide, id); + const channelName = `[?rewind=1]${spaceName}${lockId}`; + const [content, updateContent] = useSlideElementContent(lockId, defaultText); + const preview = usePreview(); + + const { handleSelect } = useElementSelect(id); + const handleContentUpdate = useCallback((content: string) => { + updateContent(content); + channel.publish('update', content); + }, []); + + const { channel } = useChannel(channelName, (message) => { + if (message.connectionId === self?.connectionId) return; + updateContent(message.data); + }); + + const optimisticallyLocked = !!activeMember; + const optimisticallyLockedByYou = optimisticallyLocked && activeMember?.connectionId === self?.connectionId; + const editIsNotAllowed = !optimisticallyLockedByYou && optimisticallyLocked; + const lockConflict = optimisticallyLockedByYou && locked && !lockedByYou && !preview; + + useClickOutside(containerRef, self, optimisticallyLockedByYou && !preview); + useClearOnFailedLock(lockConflict, self); + + return { + content, + activeMember, + locked: optimisticallyLocked, + lockedByYou: optimisticallyLockedByYou, + editIsNotAllowed, + handleSelect, + handleContentUpdate, + }; +}; diff --git a/demo/src/main.tsx b/demo/src/main.tsx index c3187cb1..cf155827 100644 --- a/demo/src/main.tsx +++ b/demo/src/main.tsx @@ -1,16 +1,22 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import { AblyProvider } from '@ably-labs/react-hooks'; import App from './App'; import './index.css'; -import { SpaceContextProvider } from './components'; +import { ably, SpaceContextProvider } from './components'; +import { SlidesStateContextProvider } from './components/SlidesStateContext.tsx'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( <React.StrictMode> <SpaceContextProvider> - <App /> + <AblyProvider client={ably}> + <SlidesStateContextProvider> + <App /> + </SlidesStateContextProvider> + </AblyProvider> </SpaceContextProvider> </React.StrictMode>, ); diff --git a/demo/src/utils/active-member.ts b/demo/src/utils/active-member.ts index d4ea5670..e636d4b8 100644 --- a/demo/src/utils/active-member.ts +++ b/demo/src/utils/active-member.ts @@ -11,9 +11,16 @@ export const getMemberFirstName = (member?: Member) => { }; export const getOutlineClasses = (member?: Member) => { - if (!member) return ''; + if (!member) + return { + outlineClasses: '', + stickyLabelClasses: '', + }; const { color } = member.profileData; const { name } = color; const { intensity } = color.gradientStart; - return `outline-${name}-${intensity} before:bg-${name}-${intensity}`; + return { + outlineClasses: `outline-${name}-${intensity}`, + stickyLabelClasses: `bg-${name}-${intensity}`, + }; }; diff --git a/demo/src/utils/locking.ts b/demo/src/utils/locking.ts new file mode 100644 index 00000000..35a56b25 --- /dev/null +++ b/demo/src/utils/locking.ts @@ -0,0 +1,9 @@ +import { Space } from '@ably-labs/spaces'; +import { Member } from './types'; + +export const releaseMyLocks = async (space: Space, self: Member) => { + await Promise.all([...space.locks.getLockRequests(self.connectionId).map((lock) => space.locks.release(lock.id))]); +}; + +export const buildLockId = (slide: string | undefined, element: string | undefined) => + `/slide/${slide}/element/${element}`; diff --git a/demo/src/utils/types.d.ts b/demo/src/utils/types.d.ts index a986fc55..f9b9d106 100644 --- a/demo/src/utils/types.d.ts +++ b/demo/src/utils/types.d.ts @@ -1,4 +1,4 @@ -import { type SpaceMember } from '../../../src'; +import { type SpaceMember } from '@ably-labs/spaces'; interface ProfileData { name: string; From 6bffbaadca067e9db1c6e7708d9c908b87e82c49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 08:11:50 +0000 Subject: [PATCH 016/191] build(deps-dev): bump eslint from 8.33.0 to 8.48.0 Bumps [eslint](https://github.com/eslint/eslint) from 8.33.0 to 8.48.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.33.0...v8.48.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 310 +++++++++++++++++++++++++++------------------- 1 file changed, 181 insertions(+), 129 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6ec6c2f..77bc527f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,15 @@ "vitest": "^0.29.8" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -431,15 +440,39 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -454,10 +487,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/js": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", + "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -1111,9 +1153,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1910,49 +1952,47 @@ } }, "node_modules/eslint": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", - "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", + "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.4.1", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.48.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -2141,18 +2181,21 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -2160,6 +2203,9 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/estraverse": { @@ -2172,14 +2218,14 @@ } }, "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2189,9 +2235,9 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -2533,9 +2579,9 @@ } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2624,6 +2670,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3094,16 +3146,6 @@ "node": ">=8" } }, - "node_modules/js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3453,17 +3495,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -4760,15 +4802,6 @@ "node": ">=8" } }, - "node_modules/word-wrap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", - "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -4864,6 +4897,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -5064,15 +5103,30 @@ "dev": true, "optional": true }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "dev": true + }, "@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -5081,10 +5135,16 @@ "strip-json-comments": "^3.1.1" } }, + "@eslint/js": { + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", + "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "dev": true + }, "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -5563,9 +5623,9 @@ } }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-jsx": { @@ -6162,56 +6222,54 @@ "dev": true }, "eslint": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", - "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", + "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.4.1", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.48.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -6369,26 +6427,26 @@ } }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" } }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -6648,9 +6706,9 @@ } }, "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -6712,6 +6770,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -7032,12 +7096,6 @@ "istanbul-lib-report": "^3.0.0" } }, - "js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", - "dev": true - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -7296,17 +7354,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-cancelable": { @@ -8152,12 +8210,6 @@ "stackback": "0.0.2" } }, - "word-wrap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", - "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", - "dev": true - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", From e97b8cefe5f663a151911c8a48eb4f05abf64999 Mon Sep 17 00:00:00 2001 From: Evgeny Khokhlov <khokhlov.e.n@gmail.com> Date: Tue, 29 Aug 2023 11:34:38 +0100 Subject: [PATCH 017/191] chore!: make `ably` peer dependency (#128) --- package-lock.json | 116 +++++++++++++++++++++++++++++++++++++--------- package.json | 4 +- 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77bc527f..62f13e11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.13", "license": "ISC", "dependencies": { - "ably": "^1.2.43", "nanoid": "^4.0.2" }, "devDependencies": { @@ -27,6 +26,9 @@ "ts-node": "^10.9.1", "typescript": "^4.9.5", "vitest": "^0.29.8" + }, + "peerDependencies": { + "ably": "^1.2.43" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -42,6 +44,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", "integrity": "sha512-IPt/BoiQwCWubqoNik1aw/6M/DleMdrxJOUpSja6xmMRbT2p1TA8oqKWgfZabqzrq8emRNeSl/+4XABPNnW5pQ==", + "peer": true, "dependencies": { "bops": "^1.0.1" } @@ -641,6 +644,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "peer": true, "engines": { "node": ">=10" }, @@ -652,6 +656,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "peer": true, "dependencies": { "defer-to-connect": "^2.0.0" }, @@ -687,6 +692,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "peer": true, "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", @@ -712,7 +718,8 @@ "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "peer": true }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -736,6 +743,7 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "peer": true, "dependencies": { "@types/node": "*" } @@ -749,6 +757,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "peer": true, "dependencies": { "@types/node": "*" } @@ -1143,6 +1152,7 @@ "version": "1.2.43", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", "integrity": "sha512-HZ99Nd98KzYToNUD4+ysHp4+vMp1NmYTi59yqGpejHo/VffTgg0pereoib0nRRAHYUhGUGys5HGwR5yHYESWDA==", + "peer": true, "dependencies": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", @@ -1323,7 +1333,8 @@ "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "peer": true }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -1347,6 +1358,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", "integrity": "sha512-ZXBDPMt/v/8fsIqn+Z5VwrhdR6jVka0bYobHdGia0Nxi7BJ9i/Uvml3AocHIBtIIBhZjBw5MR0aR4ROs/8+SNg==", + "peer": true, "engines": { "node": ">= 0.4" } @@ -1355,6 +1367,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", "integrity": "sha512-qCMBuZKP36tELrrgXpAfM+gHzqa0nLsWZ+L37ncsb8txYlnAoxOPpVp+g7fK0sGkMXfA0wl8uQkESqw3v4HNag==", + "peer": true, "dependencies": { "base64-js": "1.0.2", "to-utf8": "0.0.1" @@ -1427,6 +1440,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "peer": true, "engines": { "node": ">=10.6.0" } @@ -1435,6 +1449,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "peer": true, "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -1594,6 +1609,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "peer": true, "dependencies": { "mimic-response": "^1.0.0" }, @@ -1689,6 +1705,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "peer": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -1703,6 +1720,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "peer": true, "engines": { "node": ">=10" }, @@ -1732,6 +1750,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "peer": true, "engines": { "node": ">=10" } @@ -1801,6 +1820,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "peer": true, "dependencies": { "once": "^1.4.0" } @@ -2520,6 +2540,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "peer": true, "dependencies": { "pump": "^3.0.0" }, @@ -2644,6 +2665,7 @@ "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "peer": true, "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -2766,12 +2788,14 @@ "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "peer": true }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "peer": true, "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" @@ -3170,7 +3194,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -3206,6 +3231,7 @@ "version": "4.5.2", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -3269,6 +3295,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "peer": true, "engines": { "node": ">=8" } @@ -3341,6 +3368,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "peer": true, "engines": { "node": ">=4" } @@ -3426,6 +3454,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "peer": true, "engines": { "node": ">=10" }, @@ -3515,6 +3544,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "peer": true, "engines": { "node": ">=8" } @@ -3774,6 +3804,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "peer": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -3812,6 +3843,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "peer": true, "engines": { "node": ">=10" }, @@ -3892,7 +3924,8 @@ "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "peer": true }, "node_modules/resolve-from": { "version": "4.0.0", @@ -3907,6 +3940,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "peer": true, "dependencies": { "lowercase-keys": "^2.0.0" }, @@ -4381,7 +4415,8 @@ "node_modules/to-utf8": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", - "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==" + "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==", + "peer": true }, "node_modules/ts-node": { "version": "10.9.1", @@ -4828,6 +4863,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", + "peer": true, "dependencies": { "async-limiter": "~1.0.0" } @@ -4907,6 +4943,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", "integrity": "sha512-IPt/BoiQwCWubqoNik1aw/6M/DleMdrxJOUpSja6xmMRbT2p1TA8oqKWgfZabqzrq8emRNeSl/+4XABPNnW5pQ==", + "peer": true, "requires": { "bops": "^1.0.1" } @@ -5254,12 +5291,14 @@ "@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "peer": true }, "@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "peer": true, "requires": { "defer-to-connect": "^2.0.0" } @@ -5292,6 +5331,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "peer": true, "requires": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", @@ -5317,7 +5357,8 @@ "@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "peer": true }, "@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -5341,6 +5382,7 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "peer": true, "requires": { "@types/node": "*" } @@ -5354,6 +5396,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "peer": true, "requires": { "@types/node": "*" } @@ -5616,6 +5659,7 @@ "version": "1.2.43", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", "integrity": "sha512-HZ99Nd98KzYToNUD4+ysHp4+vMp1NmYTi59yqGpejHo/VffTgg0pereoib0nRRAHYUhGUGys5HGwR5yHYESWDA==", + "peer": true, "requires": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", @@ -5742,7 +5786,8 @@ "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "peer": true }, "available-typed-arrays": { "version": "1.0.5", @@ -5759,12 +5804,14 @@ "base64-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", - "integrity": "sha512-ZXBDPMt/v/8fsIqn+Z5VwrhdR6jVka0bYobHdGia0Nxi7BJ9i/Uvml3AocHIBtIIBhZjBw5MR0aR4ROs/8+SNg==" + "integrity": "sha512-ZXBDPMt/v/8fsIqn+Z5VwrhdR6jVka0bYobHdGia0Nxi7BJ9i/Uvml3AocHIBtIIBhZjBw5MR0aR4ROs/8+SNg==", + "peer": true }, "bops": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", "integrity": "sha512-qCMBuZKP36tELrrgXpAfM+gHzqa0nLsWZ+L37ncsb8txYlnAoxOPpVp+g7fK0sGkMXfA0wl8uQkESqw3v4HNag==", + "peer": true, "requires": { "base64-js": "1.0.2", "to-utf8": "0.0.1" @@ -5824,12 +5871,14 @@ "cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "peer": true }, "cacheable-request": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "peer": true, "requires": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -5946,6 +5995,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "peer": true, "requires": { "mimic-response": "^1.0.0" } @@ -6021,6 +6071,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "peer": true, "requires": { "mimic-response": "^3.1.0" }, @@ -6028,7 +6079,8 @@ "mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "peer": true } } }, @@ -6050,7 +6102,8 @@ "defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "peer": true }, "define-properties": { "version": "1.2.0", @@ -6102,6 +6155,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "peer": true, "requires": { "once": "^1.4.0" } @@ -6668,6 +6722,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "peer": true, "requires": { "pump": "^3.0.0" } @@ -6750,6 +6805,7 @@ "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "peer": true, "requires": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -6836,12 +6892,14 @@ "http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "peer": true }, "http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "peer": true, "requires": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" @@ -7114,7 +7172,8 @@ "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "peer": true }, "json-schema-traverse": { "version": "0.4.1", @@ -7147,6 +7206,7 @@ "version": "4.5.2", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "peer": true, "requires": { "json-buffer": "3.0.1" } @@ -7194,7 +7254,8 @@ "lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "peer": true }, "lru-cache": { "version": "6.0.0", @@ -7247,7 +7308,8 @@ "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "peer": true }, "minimatch": { "version": "3.1.2", @@ -7308,7 +7370,8 @@ "normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "peer": true }, "object-inspect": { "version": "1.12.3", @@ -7370,7 +7433,8 @@ "p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "peer": true }, "p-limit": { "version": "4.0.0", @@ -7535,6 +7599,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "peer": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7555,7 +7620,8 @@ "quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "peer": true }, "react-is": { "version": "17.0.2", @@ -7606,7 +7672,8 @@ "resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "peer": true }, "resolve-from": { "version": "4.0.0", @@ -7618,6 +7685,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "peer": true, "requires": { "lowercase-keys": "^2.0.0" } @@ -7958,7 +8026,8 @@ "to-utf8": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", - "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==" + "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==", + "peer": true }, "ts-node": { "version": "10.9.1", @@ -8230,6 +8299,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", + "peer": true, "requires": { "async-limiter": "~1.0.0" } diff --git a/package.json b/package.json index 08b1f8b2..6ba401bd 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,9 @@ "vitest": "^0.29.8" }, "dependencies": { - "ably": "^1.2.43", "nanoid": "^4.0.2" + }, + "peerDependencies": { + "ably": "^1.2.43" } } From 11448bfadff7ea5d76df65289f8139ed663f7a74 Mon Sep 17 00:00:00 2001 From: Evgeny Khokhlov <khokhlov.e.n@gmail.com> Date: Tue, 29 Aug 2023 11:59:49 +0100 Subject: [PATCH 018/191] fix: rename `types.d.ts` to `types.ts` (#130) `.d.ts` files designed to be backed by `.js` files, using them just for type declaration can lead to type errors after the compilation --- src/{types.d.ts => types.ts} | 1 + src/utilities/test/fakes.ts | 2 +- src/utilities/{types.d.ts => types.ts} | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) rename src/{types.d.ts => types.ts} (95%) rename src/utilities/{types.d.ts => types.ts} (82%) diff --git a/src/types.d.ts b/src/types.ts similarity index 95% rename from src/types.d.ts rename to src/types.ts index 14da65f7..9698e220 100644 --- a/src/types.d.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import { Types } from 'ably'; +import type { LockAttributes } from './Locks.js'; export interface CursorsOptions { outboundBatchInterval: number; diff --git a/src/utilities/test/fakes.ts b/src/utilities/test/fakes.ts index 988c5286..5d3d59ba 100644 --- a/src/utilities/test/fakes.ts +++ b/src/utilities/test/fakes.ts @@ -3,7 +3,7 @@ import { Types } from 'ably'; import Space from '../../Space.js'; import type { SpaceMember } from '../../types.js'; -import type { PresenceMember } from '../../utilities/types.js'; +import type { PresenceMember } from '../types.js'; // import { nanoidId } from '../../../__mocks__/nanoid/index.js'; const nanoidId = 'NanoidID'; diff --git a/src/utilities/types.d.ts b/src/utilities/types.ts similarity index 82% rename from src/utilities/types.d.ts rename to src/utilities/types.ts index 455faed2..26fb1094 100644 --- a/src/utilities/types.d.ts +++ b/src/utilities/types.ts @@ -1,7 +1,7 @@ -import { Types } from 'ably'; +import type { Types } from 'ably'; -import { EventKey, EventListener, EventMap } from './EventEmitter.js'; -import { ProfileData, LockRequest } from '../types.js'; +import type { EventKey, EventListener, EventMap } from './EventEmitter.js'; +import type { ProfileData, LockRequest } from '../types.js'; export type PresenceMember = { data: { @@ -28,12 +28,12 @@ export interface Provider<ProviderEventMap extends EventMap> { subscribe<K extends EventKey<ProviderEventMap>>( listenerOrEvents?: K | K[] | EventListener<ProviderEventMap[K]>, listener?: EventListener<ProviderEventMap[K]>, - ); + ): void; unsubscribe<K extends EventKey<ProviderEventMap>>( listenerOrEvents?: K | K[] | EventListener<ProviderEventMap[K]>, listener?: EventListener<ProviderEventMap[K]>, - ); + ): void; } export type RealtimeMessage = Omit<Types.Message, 'connectionId'> & { From cbac8e3787d424115e4798b046bfbed5d9be6d81 Mon Sep 17 00:00:00 2001 From: Lewis Marshall <lewis.marshall@ably.com> Date: Tue, 29 Aug 2023 10:49:15 +0100 Subject: [PATCH 019/191] Support async subscribe callbacks Signed-off-by: Lewis Marshall <lewis.marshall@ably.com> --- src/Space.test.ts | 10 ++++++++++ src/utilities/is.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Space.test.ts b/src/Space.test.ts index 8a60fc70..656f8cdd 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -254,6 +254,16 @@ describe('Space', () => { }); }); + it<SpaceTestContext>('accepts an async function', async ({ space, presenceMap }) => { + const callbackSpy = vi.fn(); + space.subscribe('update', async (v) => callbackSpy(v)); + + await createPresenceEvent(space, presenceMap, 'enter'); + expect(callbackSpy).toHaveBeenNthCalledWith(1, { + members: [createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } })], + }); + }); + describe('leavers', () => { beforeEach(() => { vi.useFakeTimers(); diff --git a/src/utilities/is.ts b/src/utilities/is.ts index 1a0a3df6..73e1bded 100644 --- a/src/utilities/is.ts +++ b/src/utilities/is.ts @@ -8,7 +8,7 @@ function isObject(arg: unknown): arg is Record<string, unknown> { } function isFunction(arg: unknown): arg is Function { - return typeOf(arg) === 'Function'; + return typeOf(arg) === 'Function' || typeOf(arg) === 'AsyncFunction'; } function isString(arg: unknown): arg is String { From 83c1994dfb85fe6fa09cc15352f53f6a448c8cde Mon Sep 17 00:00:00 2001 From: Lewis Marshall <lewis.marshall@ably.com> Date: Tue, 29 Aug 2023 12:52:50 +0100 Subject: [PATCH 020/191] Extend isFunction to cover GeneratorFunction and Proxy As per https://github.com/ably-labs/spaces/pull/139#discussion_r1308552589. Signed-off-by: Lewis Marshall <lewis.marshall@ably.com> --- src/utilities/is.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/is.ts b/src/utilities/is.ts index 73e1bded..8a6fd0d3 100644 --- a/src/utilities/is.ts +++ b/src/utilities/is.ts @@ -8,7 +8,7 @@ function isObject(arg: unknown): arg is Record<string, unknown> { } function isFunction(arg: unknown): arg is Function { - return typeOf(arg) === 'Function' || typeOf(arg) === 'AsyncFunction'; + return ['Function', 'AsyncFunction', 'GeneratorFunction', 'Proxy'].includes(typeOf(arg)); } function isString(arg: unknown): arg is String { From 5652f33526a0529f65658269708684d2de742041 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 29 Aug 2023 13:21:56 +0100 Subject: [PATCH 021/191] Bundle nanoid in IIFE build --- package-lock.json | 178 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + rollup.config.mjs | 6 +- 3 files changed, 183 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62f13e11..576b2985 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "nanoid": "^4.0.2" }, "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.1", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.28.4", @@ -640,6 +641,53 @@ "node": ">= 8" } }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", + "integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", + "integrity": "sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -715,6 +763,12 @@ "@types/chai": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", @@ -753,6 +807,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -1401,6 +1461,18 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/c8": { "version": "7.12.0", "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", @@ -1746,6 +1818,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -2305,6 +2386,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2925,6 +3012,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -2994,6 +3096,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -5288,6 +5396,31 @@ "fastq": "^1.6.0" } }, + "@rollup/plugin-node-resolve": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", + "integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + } + }, + "@rollup/pluginutils": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", + "integrity": "sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, "@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -5354,6 +5487,12 @@ "@types/chai": "*" } }, + "@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, "@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", @@ -5392,6 +5531,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -5842,6 +5987,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true + }, "c8": { "version": "7.12.0", "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", @@ -6099,6 +6250,12 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, "defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -6537,6 +6694,12 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6990,6 +7153,15 @@ "has-tostringtag": "^1.0.0" } }, + "is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + } + }, "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -7035,6 +7207,12 @@ "is-extglob": "^2.1.1" } }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", diff --git a/package.json b/package.json index 6ba401bd..0e4e1f39 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "keywords": [], "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.1", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.28.4", diff --git a/rollup.config.mjs b/rollup.config.mjs index a0381498..f3de6a49 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,3 +1,5 @@ +import { nodeResolve } from '@rollup/plugin-node-resolve'; + export default { input: 'dist/iife/index.js', output: { @@ -6,8 +8,8 @@ export default { file: 'dist/iife/index.bundle.js', globals: { ably: 'Ably', - nanoid: 'nanoid', }, }, - external: ['ably', 'nanoid'], + external: ['ably'], + plugins: [nodeResolve({ browser: true })], }; From c1ad250ad3147406dd8f1ff410eddc3e5743d087 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 29 Aug 2023 13:26:08 +0100 Subject: [PATCH 022/191] Minify IIFE bundle --- package-lock.json | 134 ++++++++++++++++++++++++++++++++++++++-------- package.json | 1 + rollup.config.mjs | 3 ++ 3 files changed, 115 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 576b2985..ff7c9289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.3", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.28.4", @@ -547,8 +548,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -572,8 +571,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=6.0.0" } @@ -583,8 +580,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -666,6 +661,28 @@ } } }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", + "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", + "dev": true, + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.x || ^3.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", @@ -1711,9 +1728,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/comment-parser": { "version": "1.3.1", @@ -3959,6 +3974,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -4120,6 +4144,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", @@ -4158,6 +4202,15 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4254,6 +4307,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/smob": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz", + "integrity": "sha512-MqR3fVulhjWuRNSMydnTlweu38UhQ0HXM4buStD/S3mc/BzX3CuM9OmhyQpmtYCvoYdl5ris6TI0ZqH355Ymqg==", + "dev": true + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4449,8 +4508,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -5320,8 +5377,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, - "optional": true, - "peer": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -5338,17 +5393,13 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@jridgewell/source-map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", "dev": true, - "optional": true, - "peer": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -5410,6 +5461,17 @@ "resolve": "^1.22.1" } }, + "@rollup/plugin-terser": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", + "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", + "dev": true, + "requires": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + } + }, "@rollup/pluginutils": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", @@ -6170,9 +6232,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "comment-parser": { "version": "1.3.1", @@ -7801,6 +7861,15 @@ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "peer": true }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -7901,6 +7970,12 @@ "queue-microtask": "^1.2.2" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, "safe-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", @@ -7930,6 +8005,15 @@ "lru-cache": "^6.0.0" } }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7998,6 +8082,12 @@ } } }, + "smob": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz", + "integrity": "sha512-MqR3fVulhjWuRNSMydnTlweu38UhQ0HXM4buStD/S3mc/BzX3CuM9OmhyQpmtYCvoYdl5ris6TI0ZqH355Ymqg==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8148,8 +8238,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", "dev": true, - "optional": true, - "peer": true, "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", diff --git a/package.json b/package.json index 0e4e1f39..7e382527 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "keywords": [], "devDependencies": { "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.3", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.28.4", diff --git a/rollup.config.mjs b/rollup.config.mjs index f3de6a49..313b7b5e 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,4 +1,5 @@ import { nodeResolve } from '@rollup/plugin-node-resolve'; +import terser from '@rollup/plugin-terser'; export default { input: 'dist/iife/index.js', @@ -9,6 +10,8 @@ export default { globals: { ably: 'Ably', }, + compact: true, + plugins: [terser()], }, external: ['ably'], plugins: [nodeResolve({ browser: true })], From 1037f064252b70d23d7f78dc21ea94678c899fa2 Mon Sep 17 00:00:00 2001 From: Evgeny Khokhlov <khokhlov.e.n@gmail.com> Date: Tue, 29 Aug 2023 15:16:11 +0100 Subject: [PATCH 023/191] chore: use npm versions in demo (#143) --- demo/package-lock.json | 114 ++++++++++------------------------------- demo/package.json | 4 +- 2 files changed, 29 insertions(+), 89 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index e7fa68e1..4c0392c5 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -8,8 +8,8 @@ "name": "demo", "version": "1.0.0", "dependencies": { - "@ably-labs/react-hooks": "file:../../react-hooks", - "@ably-labs/spaces": "file:../", + "@ably-labs/react-hooks": "^3.0.0-canary.1", + "@ably-labs/spaces": "^0.0.13", "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", @@ -46,56 +46,6 @@ "vite": "^4.4.5" } }, - "..": { - "name": "@ably-labs/spaces", - "version": "0.0.12", - "license": "ISC", - "dependencies": { - "ably": "^1.2.43", - "nanoid": "^4.0.2" - }, - "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.51.0", - "@typescript-eslint/parser": "^5.51.0", - "@vitest/coverage-c8": "^0.28.4", - "eslint": "^8.33.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^39.8.0", - "eslint-plugin-security": "^1.7.1", - "husky": "^8.0.0", - "mock-socket": "^9.1.5", - "prettier": "^2.8.3", - "rollup": "^3.28.0", - "ts-node": "^10.9.1", - "typescript": "^4.9.5", - "vitest": "^0.29.8" - } - }, - "../../react-hooks": { - "version": "2.1.1", - "license": "ISC", - "devDependencies": { - "@testing-library/react": "^13.3.0", - "@typescript-eslint/eslint-plugin": "^6.1.0", - "@typescript-eslint/parser": "^6.4.0", - "@vitejs/plugin-react": "^1.3.2", - "eslint": "^8.45.0", - "eslint-plugin-import": "^2.28.0", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "jsdom": "^20.0.0", - "prettier": "^3.0.0", - "react": ">=18.1.0", - "react-dom": ">=18.1.0", - "typescript": ">=4.4.4", - "vitest": "^0.18.0" - }, - "peerDependencies": { - "ably": "^1.2.27", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -106,12 +56,25 @@ } }, "node_modules/@ably-labs/react-hooks": { - "resolved": "../../react-hooks", - "link": true + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/@ably-labs/react-hooks/-/react-hooks-3.0.0-canary.1.tgz", + "integrity": "sha512-ln2XHNwiZof3xU0jZBd1tb4wzsntmbfonqX9M40V2nlNKW7OkzSSW0e2CbAyy8LtsZPEm6vsHZHVgTcdrBxKBg==", + "dependencies": { + "ably": "^1.2.27" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } }, "node_modules/@ably-labs/spaces": { - "resolved": "..", - "link": true + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@ably-labs/spaces/-/spaces-0.0.13.tgz", + "integrity": "sha512-/q4wYw3VhO07SwWo6Hrs1PP0E+9zDb/DjrTKVFrXcxZhd5onBkxQ2t2fJfAEapEnRQPNhMHIpLZXugZ9qJVD1Q==", + "dependencies": { + "ably": "^1.2.43", + "nanoid": "^4.0.2" + } }, "node_modules/@ably/msgpack-js": { "version": "0.4.0", @@ -20482,43 +20445,20 @@ "dev": true }, "@ably-labs/react-hooks": { - "version": "file:../../react-hooks", + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/@ably-labs/react-hooks/-/react-hooks-3.0.0-canary.1.tgz", + "integrity": "sha512-ln2XHNwiZof3xU0jZBd1tb4wzsntmbfonqX9M40V2nlNKW7OkzSSW0e2CbAyy8LtsZPEm6vsHZHVgTcdrBxKBg==", "requires": { - "@testing-library/react": "^13.3.0", - "@typescript-eslint/eslint-plugin": "^6.1.0", - "@typescript-eslint/parser": "^6.4.0", - "@vitejs/plugin-react": "^1.3.2", - "eslint": "^8.45.0", - "eslint-plugin-import": "^2.28.0", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "jsdom": "^20.0.0", - "prettier": "^3.0.0", - "react": ">=18.1.0", - "react-dom": ">=18.1.0", - "typescript": ">=4.4.4", - "vitest": "^0.18.0" + "ably": "^1.2.27" } }, "@ably-labs/spaces": { - "version": "file:..", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@ably-labs/spaces/-/spaces-0.0.13.tgz", + "integrity": "sha512-/q4wYw3VhO07SwWo6Hrs1PP0E+9zDb/DjrTKVFrXcxZhd5onBkxQ2t2fJfAEapEnRQPNhMHIpLZXugZ9qJVD1Q==", "requires": { - "@typescript-eslint/eslint-plugin": "^5.51.0", - "@typescript-eslint/parser": "^5.51.0", - "@vitest/coverage-c8": "^0.28.4", "ably": "^1.2.43", - "eslint": "^8.33.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^39.8.0", - "eslint-plugin-security": "^1.7.1", - "husky": "^8.0.0", - "mock-socket": "^9.1.5", - "nanoid": "^4.0.2", - "prettier": "^2.8.3", - "rollup": "^3.28.0", - "ts-node": "^10.9.1", - "typescript": "^4.9.5", - "vitest": "^0.29.8" + "nanoid": "^4.0.2" } }, "@ably/msgpack-js": { diff --git a/demo/package.json b/demo/package.json index 4ca6863b..80b8e080 100644 --- a/demo/package.json +++ b/demo/package.json @@ -12,8 +12,8 @@ "deploy:production": "npm run build && netlify deploy --prod" }, "dependencies": { - "@ably-labs/react-hooks": "file:../../react-hooks", - "@ably-labs/spaces": "file:../", + "@ably-labs/react-hooks": "^3.0.0-canary.1", + "@ably-labs/spaces": "^0.0.13", "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", From b991c71a7e661b65190d1e16a193320fbc203149 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 29 Aug 2023 14:05:07 +0100 Subject: [PATCH 024/191] Prefer null over undefined consistently --- src/Cursors.test.ts | 4 ++-- src/Cursors.ts | 8 ++++---- src/Members.ts | 8 ++++---- src/Space.test.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Cursors.test.ts b/src/Cursors.test.ts index 3330edde..42b06b14 100644 --- a/src/Cursors.test.ts +++ b/src/Cursors.test.ts @@ -455,7 +455,7 @@ describe('Cursors', () => { vi.spyOn(space.cursors, 'getAll').mockImplementation(async () => ({})); const self = await space.cursors.getSelf(); - expect(self).toBeUndefined(); + expect(self).toBeNull(); }); it<CursorsTestContext>('returns the cursor update for self', async ({ @@ -472,7 +472,7 @@ describe('Cursors', () => { it<CursorsTestContext>('returns an empty object if self is not present in cursors', async ({ space }) => { vi.spyOn(space.cursors, 'getAll').mockResolvedValue({}); - vi.spyOn(space.members, 'getSelf').mockResolvedValue(undefined); + vi.spyOn(space.members, 'getSelf').mockResolvedValue(null); const others = await space.cursors.getOthers(); expect(others).toEqual({}); diff --git a/src/Cursors.ts b/src/Cursors.ts index f973e311..b7101723 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -149,12 +149,12 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { } } - async getSelf(): Promise<CursorUpdate | undefined> { + async getSelf(): Promise<CursorUpdate | null> { const self = await this.space.members.getSelf(); - if (!self) return; + if (!self) return null; const allCursors = await this.getAll(); - return allCursors[self.connectionId] as CursorUpdate; + return allCursors[self.connectionId]; } async getOthers(): Promise<Record<string, null | CursorUpdate>> { @@ -167,7 +167,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { return allCursorsFiltered; } - async getAll() { + async getAll(): Promise<Record<string, null | CursorUpdate>> { const channel = this.getChannel(); return await this.cursorHistory.getLastCursorUpdate(channel, this.options.paginationLimit); } diff --git a/src/Members.ts b/src/Members.ts index c4f79ee4..d0d56796 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -50,8 +50,8 @@ class Members extends EventEmitter<MemberEventsMap> { } } - async getSelf(): Promise<SpaceMember | undefined> { - return this.space.connectionId ? await this.getByConnectionId(this.space.connectionId) : undefined; + async getSelf(): Promise<SpaceMember | null> { + return this.space.connectionId ? await this.getByConnectionId(this.space.connectionId) : null; } async getAll(): Promise<SpaceMember[]> { @@ -99,9 +99,9 @@ class Members extends EventEmitter<MemberEventsMap> { } } - async getByConnectionId(connectionId: string): Promise<SpaceMember | undefined> { + async getByConnectionId(connectionId: string): Promise<SpaceMember | null> { const members = await this.getAll(); - return members.find((m) => m.connectionId === connectionId); + return members.find((m) => m.connectionId === connectionId) ?? null; } createMember(message: PresenceMember): SpaceMember { diff --git a/src/Space.test.ts b/src/Space.test.ts index 656f8cdd..67db9d9a 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -77,7 +77,7 @@ describe('Space', () => { expect(member).toEqual(createSpaceMember()); const noMember = await space.members.getByConnectionId('nonExistentConnectionId'); - expect(noMember).toBe(undefined); + expect(noMember).toBeNull(); }); it<SpaceTestContext>('initialises locks', async ({ presenceMap, space }) => { From 12996ec9d870d220fb9fd2ee752d7e232e86b6b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:17:25 +0000 Subject: [PATCH 025/191] build(deps-dev): bump eslint-plugin-import from 2.27.5 to 2.28.1 Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.27.5 to 2.28.1. - [Release notes](https://github.com/import-js/eslint-plugin-import/releases) - [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md) - [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.27.5...v2.28.1) --- updated-dependencies: - dependency-name: eslint-plugin-import dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 359 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 309 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff7c9289..655e0f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1362,6 +1362,25 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", + "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", @@ -1398,6 +1417,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -1922,18 +1961,19 @@ } }, "node_modules/es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -1953,14 +1993,18 @@ "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", "safe-regex-test": "^1.0.0", "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.10" }, "engines": { "node": ">= 0.4" @@ -2168,26 +2212,28 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", "dev": true, "dependencies": { "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", "array.prototype.flat": "^1.3.1", "array.prototype.flatmap": "^1.3.1", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", + "eslint-module-utils": "^2.8.0", "has": "^1.0.3", - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "is-glob": "^4.0.3", "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "engines": { "node": ">=4" @@ -2218,9 +2264,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3055,9 +3101,9 @@ } }, "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -3251,6 +3297,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3621,6 +3673,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "node_modules/object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -4144,6 +4225,24 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4701,6 +4800,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -4967,17 +5117,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -5960,6 +6109,19 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "array.prototype.findlastindex": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", + "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } + }, "array.prototype.flat": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", @@ -5984,6 +6146,20 @@ "es-shim-unscopables": "^1.0.0" } }, + "arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -6378,18 +6554,19 @@ } }, "es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", "dev": true, "requires": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -6409,14 +6586,18 @@ "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", "safe-regex-test": "^1.0.0", "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.10" } }, "es-set-tostringtag": { @@ -6598,26 +6779,28 @@ } }, "eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", "dev": true, "requires": { "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", "array.prototype.flat": "^1.3.1", "array.prototype.flatmap": "^1.3.1", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", + "eslint-module-utils": "^2.8.0", "has": "^1.0.3", - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "is-glob": "^4.0.3", "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "dependencies": { "debug": { @@ -6639,9 +6822,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -7229,9 +7412,9 @@ "dev": true }, "is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "requires": { "has": "^1.0.3" @@ -7359,6 +7542,12 @@ "call-bind": "^1.0.2" } }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7635,6 +7824,29 @@ "object-keys": "^1.1.1" } }, + "object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -7970,6 +8182,18 @@ "queue-microtask": "^1.2.2" } }, + "safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -8372,6 +8596,42 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, "typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -8522,17 +8782,16 @@ } }, "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" } }, "why-is-node-running": { From 50a9198cc63637d72d7d5e3033a400c86f9d1359 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:17:59 +0000 Subject: [PATCH 026/191] build(deps-dev): bump vitest from 0.29.8 to 0.34.3 Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 0.29.8 to 0.34.3. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v0.34.3/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 547 +++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 399 insertions(+), 150 deletions(-) diff --git a/package-lock.json b/package-lock.json index 655e0f65..a4f600ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "rollup": "^3.28.0", "ts-node": "^10.9.1", "typescript": "^4.9.5", - "vitest": "^0.29.8" + "vitest": "^0.34.3" }, "peerDependencies": { "ably": "^1.2.43" @@ -543,6 +543,18 @@ "node": ">=8" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -705,6 +717,12 @@ } } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -766,9 +784,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", "dev": true }, "node_modules/@types/chai-subset": { @@ -1183,48 +1201,146 @@ } }, "node_modules/@vitest/expect": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.8.tgz", - "integrity": "sha512-xlcVXn5I5oTq6NiZSY3ykyWixBxr5mG8HYtjvpgg6KaqHm0mvhX18xuwl5YGxIRNt/A5jidd7CWcNHrSvgaQqQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.3.tgz", + "integrity": "sha512-F8MTXZUYRBVsYL1uoIft1HHWhwDbSzwAU9Zgh8S6WFC3YgVb4AnFV2GXO3P5Em8FjEYaZtTnQYoNwwBrlOMXgg==", "dev": true, "dependencies": { - "@vitest/spy": "0.29.8", - "@vitest/utils": "0.29.8", + "@vitest/spy": "0.34.3", + "@vitest/utils": "0.34.3", "chai": "^4.3.7" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.8.tgz", - "integrity": "sha512-FzdhnRDwEr/A3Oo1jtIk/B952BBvP32n1ObMEb23oEJNO+qO5cBet6M2XWIDQmA7BDKGKvmhUf2naXyp/2JEwQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.3.tgz", + "integrity": "sha512-lYNq7N3vR57VMKMPLVvmJoiN4bqwzZ1euTW+XXYH5kzr3W/+xQG3b41xJn9ChJ3AhYOSoweu974S1V3qDcFESA==", "dev": true, "dependencies": { - "@vitest/utils": "0.29.8", + "@vitest/utils": "0.34.3", "p-limit": "^4.0.0", - "pathe": "^1.1.0" + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.3.tgz", + "integrity": "sha512-QyPaE15DQwbnIBp/yNJ8lbvXTZxS00kRly0kfFgAD5EYmCbYcA+1EEyRalc93M0gosL/xHeg3lKAClIXYpmUiQ==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/snapshot/node_modules/pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/snapshot/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@vitest/spy": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.8.tgz", - "integrity": "sha512-VdjBe9w34vOMl5I5mYEzNX8inTxrZ+tYUVk9jxaZJmHFwmDFC/GV3KBFTA/JKswr3XHvZL+FE/yq5EVhb6pSAw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.3.tgz", + "integrity": "sha512-N1V0RFQ6AI7CPgzBq9kzjRdPIgThC340DGjdKdPSE8r86aUSmeliTUgkTqLSgtEwWWsGfBQ+UetZWhK0BgJmkQ==", "dev": true, "dependencies": { - "tinyspy": "^1.0.2" + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy/node_modules/tinyspy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", + "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "dev": true, + "engines": { + "node": ">=14.0.0" } }, "node_modules/@vitest/utils": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.8.tgz", - "integrity": "sha512-qGzuf3vrTbnoY+RjjVVIBYfuWMjn3UMUqyQtdGNZ6ZIIyte7B37exj6LaVkrZiUTvzSadVvO/tJm8AEgbGCBPg==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.3.tgz", + "integrity": "sha512-kiSnzLG6m/tiT0XEl4U2H8JDBjFtwVlaE8I3QfGiMFR0QvnRDfYfdP3YvTBWM/6iJDAyaPY6yVQiCTUc7ZzTHA==", "dev": true, "dependencies": { - "cli-truncate": "^3.1.0", - "diff": "^5.1.0", + "diff-sequences": "^29.4.3", "loupe": "^2.3.6", - "pretty-format": "^27.5.1" + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/ably": { "version": "1.2.43", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", @@ -1915,6 +2031,15 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3487,6 +3612,24 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.30.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", + "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3570,15 +3713,15 @@ } }, "node_modules/mlly": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", - "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.1.tgz", + "integrity": "sha512-SCDs78Q2o09jiZiE2WziwVBEqXQ02XkGdUy45cbJf+BpYRIjArXRJ1Wbowxkb+NaM9DWvS3UC9GiO/6eqvQ/pg==", "dev": true, "dependencies": { - "acorn": "^8.8.2", - "pathe": "^1.1.0", - "pkg-types": "^1.0.2", - "ufo": "^1.1.1" + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" } }, "node_modules/mock-socket": { @@ -3865,9 +4008,9 @@ } }, "node_modules/pathe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", - "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", "dev": true }, "node_modules/pathval": { @@ -3898,13 +4041,13 @@ } }, "node_modules/pkg-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz", - "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", "dev": true, "dependencies": { "jsonc-parser": "^3.2.0", - "mlly": "^1.1.1", + "mlly": "^1.2.0", "pathe": "^1.1.0" } }, @@ -4469,9 +4612,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", - "integrity": "sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", "dev": true }, "node_modules/string-width": { @@ -4641,9 +4784,9 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz", - "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", "dev": true }, "node_modules/tinypool": { @@ -4879,9 +5022,9 @@ } }, "node_modules/ufo": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", - "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.0.tgz", + "integrity": "sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==", "dev": true }, "node_modules/unbox-primitive": { @@ -4977,15 +5120,15 @@ } }, "node_modules/vite-node": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.29.8.tgz", - "integrity": "sha512-b6OtCXfk65L6SElVM20q5G546yu10/kNrhg08afEoWlFRJXFq9/6glsvSVY+aI6YeC1tu2TtAqI2jHEQmOmsFw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.3.tgz", + "integrity": "sha512-+0TzJf1g0tYXj6tR2vEyiA42OPq68QkRZCu/ERSo2PtsDJfBpDyEfuKbRvLmZqi/CgC7SCBtyC+WjTGNMRIaig==", "dev": true, "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", - "mlly": "^1.1.0", - "pathe": "^1.1.0", + "mlly": "^1.4.0", + "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^3.0.0 || ^4.0.0" }, @@ -4993,51 +5136,51 @@ "vite-node": "vite-node.mjs" }, "engines": { - "node": ">=v14.16.0" + "node": ">=v14.18.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" } }, "node_modules/vitest": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.29.8.tgz", - "integrity": "sha512-JIAVi2GK5cvA6awGpH0HvH/gEG9PZ0a/WoxdiV3PmqK+3CjQMf8c+J/Vhv4mdZ2nRyXFw66sAg6qz7VNkaHfDQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.3.tgz", + "integrity": "sha512-7+VA5Iw4S3USYk+qwPxHl8plCMhA5rtfwMjgoQXMT7rO5ldWcdsdo3U1QD289JgglGK4WeOzgoLTsGFu6VISyQ==", "dev": true, "dependencies": { - "@types/chai": "^4.3.4", + "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.29.8", - "@vitest/runner": "0.29.8", - "@vitest/spy": "0.29.8", - "@vitest/utils": "0.29.8", - "acorn": "^8.8.1", + "@vitest/expect": "0.34.3", + "@vitest/runner": "0.34.3", + "@vitest/snapshot": "0.34.3", + "@vitest/spy": "0.34.3", + "@vitest/utils": "0.34.3", + "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", "chai": "^4.3.7", "debug": "^4.3.4", - "local-pkg": "^0.4.2", - "pathe": "^1.1.0", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "std-env": "^3.3.1", - "strip-literal": "^1.0.0", - "tinybench": "^2.3.1", - "tinypool": "^0.4.0", - "tinyspy": "^1.0.2", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.29.8", + "vite-node": "0.34.3", "why-is-node-running": "^2.2.2" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": ">=v14.16.0" + "node": ">=v14.18.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", @@ -5077,9 +5220,9 @@ } }, "node_modules/vitest/node_modules/tinypool": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.4.0.tgz", - "integrity": "sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", "dev": true, "engines": { "node": ">=14.0.0" @@ -5521,6 +5664,15 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, "@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -5632,6 +5784,12 @@ "picomatch": "^2.3.1" } }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -5684,9 +5842,9 @@ } }, "@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", "dev": true }, "@types/chai-subset": { @@ -5969,46 +6127,114 @@ } }, "@vitest/expect": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.8.tgz", - "integrity": "sha512-xlcVXn5I5oTq6NiZSY3ykyWixBxr5mG8HYtjvpgg6KaqHm0mvhX18xuwl5YGxIRNt/A5jidd7CWcNHrSvgaQqQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.3.tgz", + "integrity": "sha512-F8MTXZUYRBVsYL1uoIft1HHWhwDbSzwAU9Zgh8S6WFC3YgVb4AnFV2GXO3P5Em8FjEYaZtTnQYoNwwBrlOMXgg==", "dev": true, "requires": { - "@vitest/spy": "0.29.8", - "@vitest/utils": "0.29.8", + "@vitest/spy": "0.34.3", + "@vitest/utils": "0.34.3", "chai": "^4.3.7" } }, "@vitest/runner": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.8.tgz", - "integrity": "sha512-FzdhnRDwEr/A3Oo1jtIk/B952BBvP32n1ObMEb23oEJNO+qO5cBet6M2XWIDQmA7BDKGKvmhUf2naXyp/2JEwQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.3.tgz", + "integrity": "sha512-lYNq7N3vR57VMKMPLVvmJoiN4bqwzZ1euTW+XXYH5kzr3W/+xQG3b41xJn9ChJ3AhYOSoweu974S1V3qDcFESA==", "dev": true, "requires": { - "@vitest/utils": "0.29.8", + "@vitest/utils": "0.34.3", "p-limit": "^4.0.0", - "pathe": "^1.1.0" + "pathe": "^1.1.1" + } + }, + "@vitest/snapshot": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.3.tgz", + "integrity": "sha512-QyPaE15DQwbnIBp/yNJ8lbvXTZxS00kRly0kfFgAD5EYmCbYcA+1EEyRalc93M0gosL/xHeg3lKAClIXYpmUiQ==", + "dev": true, + "requires": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } } }, "@vitest/spy": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.8.tgz", - "integrity": "sha512-VdjBe9w34vOMl5I5mYEzNX8inTxrZ+tYUVk9jxaZJmHFwmDFC/GV3KBFTA/JKswr3XHvZL+FE/yq5EVhb6pSAw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.3.tgz", + "integrity": "sha512-N1V0RFQ6AI7CPgzBq9kzjRdPIgThC340DGjdKdPSE8r86aUSmeliTUgkTqLSgtEwWWsGfBQ+UetZWhK0BgJmkQ==", "dev": true, "requires": { - "tinyspy": "^1.0.2" + "tinyspy": "^2.1.1" + }, + "dependencies": { + "tinyspy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", + "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "dev": true + } } }, "@vitest/utils": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.8.tgz", - "integrity": "sha512-qGzuf3vrTbnoY+RjjVVIBYfuWMjn3UMUqyQtdGNZ6ZIIyte7B37exj6LaVkrZiUTvzSadVvO/tJm8AEgbGCBPg==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.3.tgz", + "integrity": "sha512-kiSnzLG6m/tiT0XEl4U2H8JDBjFtwVlaE8I3QfGiMFR0QvnRDfYfdP3YvTBWM/6iJDAyaPY6yVQiCTUc7ZzTHA==", "dev": true, "requires": { - "cli-truncate": "^3.1.0", - "diff": "^5.1.0", + "diff-sequences": "^29.4.3", "loupe": "^2.3.6", - "pretty-format": "^27.5.1" + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } } }, "ably": { @@ -6514,6 +6740,12 @@ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", "dev": true }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7693,6 +7925,23 @@ "yallist": "^4.0.0" } }, + "magic-string": { + "version": "0.30.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", + "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + } + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -7754,15 +8003,15 @@ "dev": true }, "mlly": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", - "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.1.tgz", + "integrity": "sha512-SCDs78Q2o09jiZiE2WziwVBEqXQ02XkGdUy45cbJf+BpYRIjArXRJ1Wbowxkb+NaM9DWvS3UC9GiO/6eqvQ/pg==", "dev": true, "requires": { - "acorn": "^8.8.2", - "pathe": "^1.1.0", - "pkg-types": "^1.0.2", - "ufo": "^1.1.1" + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" } }, "mock-socket": { @@ -7961,9 +8210,9 @@ "dev": true }, "pathe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", - "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", "dev": true }, "pathval": { @@ -7985,13 +8234,13 @@ "dev": true }, "pkg-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz", - "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", "dev": true, "requires": { "jsonc-parser": "^3.2.0", - "mlly": "^1.1.1", + "mlly": "^1.2.0", "pathe": "^1.1.0" } }, @@ -8363,9 +8612,9 @@ "dev": true }, "std-env": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", - "integrity": "sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", "dev": true }, "string-width": { @@ -8487,9 +8736,9 @@ "dev": true }, "tinybench": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz", - "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", "dev": true }, "tinypool": { @@ -8650,9 +8899,9 @@ "dev": true }, "ufo": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", - "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.0.tgz", + "integrity": "sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==", "dev": true }, "unbox-primitive": { @@ -8706,55 +8955,55 @@ } }, "vite-node": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.29.8.tgz", - "integrity": "sha512-b6OtCXfk65L6SElVM20q5G546yu10/kNrhg08afEoWlFRJXFq9/6glsvSVY+aI6YeC1tu2TtAqI2jHEQmOmsFw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.3.tgz", + "integrity": "sha512-+0TzJf1g0tYXj6tR2vEyiA42OPq68QkRZCu/ERSo2PtsDJfBpDyEfuKbRvLmZqi/CgC7SCBtyC+WjTGNMRIaig==", "dev": true, "requires": { "cac": "^6.7.14", "debug": "^4.3.4", - "mlly": "^1.1.0", - "pathe": "^1.1.0", + "mlly": "^1.4.0", + "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^3.0.0 || ^4.0.0" } }, "vitest": { - "version": "0.29.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.29.8.tgz", - "integrity": "sha512-JIAVi2GK5cvA6awGpH0HvH/gEG9PZ0a/WoxdiV3PmqK+3CjQMf8c+J/Vhv4mdZ2nRyXFw66sAg6qz7VNkaHfDQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.3.tgz", + "integrity": "sha512-7+VA5Iw4S3USYk+qwPxHl8plCMhA5rtfwMjgoQXMT7rO5ldWcdsdo3U1QD289JgglGK4WeOzgoLTsGFu6VISyQ==", "dev": true, "requires": { - "@types/chai": "^4.3.4", + "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.29.8", - "@vitest/runner": "0.29.8", - "@vitest/spy": "0.29.8", - "@vitest/utils": "0.29.8", - "acorn": "^8.8.1", + "@vitest/expect": "0.34.3", + "@vitest/runner": "0.34.3", + "@vitest/snapshot": "0.34.3", + "@vitest/spy": "0.34.3", + "@vitest/utils": "0.34.3", + "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", "chai": "^4.3.7", "debug": "^4.3.4", - "local-pkg": "^0.4.2", - "pathe": "^1.1.0", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "std-env": "^3.3.1", - "strip-literal": "^1.0.0", - "tinybench": "^2.3.1", - "tinypool": "^0.4.0", - "tinyspy": "^1.0.2", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.29.8", + "vite-node": "0.34.3", "why-is-node-running": "^2.2.2" }, "dependencies": { "tinypool": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.4.0.tgz", - "integrity": "sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", "dev": true } } diff --git a/package.json b/package.json index 7e382527..8fbae1db 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "rollup": "^3.28.0", "ts-node": "^10.9.1", "typescript": "^4.9.5", - "vitest": "^0.29.8" + "vitest": "^0.34.3" }, "dependencies": { "nanoid": "^4.0.2" From 63d1a5ff20528bcb29d6a62420702d6d42465087 Mon Sep 17 00:00:00 2001 From: Evgeny Khokhlov <khokhlov.e.n@gmail.com> Date: Wed, 30 Aug 2023 10:24:45 +0100 Subject: [PATCH 027/191] fix: locking demo bugs (#146) * The 'click elsewhere to lose selection' applied to locations as well, not just locks * When more than one person is on the same component (the pictures that are not locked), the location label say "... +N". * When someone leaves, the avatar change to grey * Sanitize value from `useChannel` --- demo/src/components/Avatar.tsx | 7 +++++-- demo/src/components/Image.tsx | 23 ++++++++++++++++------- demo/src/hooks/useTextComponentLock.ts | 5 ++++- demo/src/utils/active-member.ts | 4 ++++ 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/demo/src/components/Avatar.tsx b/demo/src/components/Avatar.tsx index c9d84ab0..ee9afcd5 100644 --- a/demo/src/components/Avatar.tsx +++ b/demo/src/components/Avatar.tsx @@ -40,8 +40,11 @@ export const Avatar = ({ <div className={cn( 'rounded-full flex items-center justify-center h-[32px] w-[32px] md:h-[40px] md:w-[40px] bg-gradient-to-tr', - profileData.color.gradientStart.tw, - profileData.color.gradientEnd.tw, + { + [profileData.color.gradientStart.tw]: isConnected, + [profileData.color.gradientEnd.tw]: isConnected, + 'bg-[#D9D9DA]': !isConnected, + }, )} data-id="avatar-inner-wrapper" > diff --git a/demo/src/components/Image.tsx b/demo/src/components/Image.tsx index 3a91253b..95d36be0 100644 --- a/demo/src/components/Image.tsx +++ b/demo/src/components/Image.tsx @@ -1,6 +1,8 @@ import cn from 'classnames'; -import { useElementSelect, useMembers } from '../hooks'; -import { findActiveMember, getMemberFirstName, getOutlineClasses } from '../utils'; +import { useClickOutside, useElementSelect, useMembers } from '../hooks'; +import { findActiveMembers, getMemberFirstName, getOutlineClasses } from '../utils'; +import { useRef } from 'react'; +import { usePreview } from './PreviewContext.tsx'; interface Props extends React.HTMLAttributes<HTMLImageElement> { src: string; @@ -12,19 +14,26 @@ interface Props extends React.HTMLAttributes<HTMLImageElement> { } export const Image = ({ src, children, className, id, slide, locatable = true }: Props) => { + const containerRef = useRef<HTMLDivElement | null>(null); + const preview = usePreview(); const { members, self } = useMembers(); const { handleSelect } = useElementSelect(id, false); - const activeMember = findActiveMember(id, slide, members); - const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember); - const memberName = getMemberFirstName(activeMember); - const label = self?.connectionId === activeMember?.connectionId ? 'You' : memberName; + const activeMembers = findActiveMembers(id, slide, members); + const occupiedByMe = activeMembers.some((member) => member.connectionId === self?.connectionId); + const [firstMember] = activeMembers; + const { outlineClasses, stickyLabelClasses } = getOutlineClasses(firstMember); + const memberName = getMemberFirstName(firstMember); + const name = occupiedByMe ? 'You' : memberName; + const label = activeMembers.length > 1 ? `${name} +${activeMembers.length - 1}` : name; + + useClickOutside(containerRef, self, occupiedByMe && !preview); return ( <div data-before={label} className={cn('relative xs:my-4 md:my-0', className, { [`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses} before:${stickyLabelClasses}`]: - activeMember, + firstMember, })} > <img diff --git a/demo/src/hooks/useTextComponentLock.ts b/demo/src/hooks/useTextComponentLock.ts index e67c5490..b95a0e34 100644 --- a/demo/src/hooks/useTextComponentLock.ts +++ b/demo/src/hooks/useTextComponentLock.ts @@ -7,6 +7,7 @@ import { useMembers } from './useMembers.ts'; import { useClearOnFailedLock, useClickOutside, useElementSelect } from './useElementSelect.ts'; import { useLockStatus } from './useLock.ts'; import { useSlideElementContent } from './useSlideElementContent.ts'; +import sanitize from 'sanitize-html'; interface UseTextComponentLockArgs { id: string; @@ -14,6 +15,7 @@ interface UseTextComponentLockArgs { defaultText: string; containerRef: MutableRefObject<HTMLElement | null>; } + export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: UseTextComponentLockArgs) => { const spaceName = getSpaceNameFromUrl(); const { members, self } = useMembers(); @@ -32,7 +34,8 @@ export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: U const { channel } = useChannel(channelName, (message) => { if (message.connectionId === self?.connectionId) return; - updateContent(message.data); + const sanitizedValue = sanitize(message.data, { allowedTags: [] }); + updateContent(sanitizedValue); }); const optimisticallyLocked = !!activeMember; diff --git a/demo/src/utils/active-member.ts b/demo/src/utils/active-member.ts index e636d4b8..f860f327 100644 --- a/demo/src/utils/active-member.ts +++ b/demo/src/utils/active-member.ts @@ -5,6 +5,10 @@ export const findActiveMember = (id: string, slide: string, members?: Member[]) return members.find((member) => member.location?.element === id && member.location?.slide === slide); }; +export const findActiveMembers = (id: string, slide: string, members?: Member[]) => { + return (members ?? []).filter((member) => member.location?.element === id && member.location?.slide === slide); +}; + export const getMemberFirstName = (member?: Member) => { if (!member) return ''; return member.profileData.name.split(' ')[0]; From 22609850b284674413b28694d85338b0ba33e439 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 29 Aug 2023 17:06:33 +0100 Subject: [PATCH 028/191] Fix getAll no returing position or data values This broke when we added offsets to the OutgoingBuffer in cursors. Unfortuntely, the relevant tests need to mock the whole presence message and did not catch this change. When we implement integration tests, this should be a prime candidate for a golden path test as it needs to go via multiple separate modules as well as Ably. --- src/CursorBatching.ts | 2 +- src/CursorHistory.ts | 14 +++++++++++--- src/Cursors.test.ts | 7 +++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/CursorBatching.ts b/src/CursorBatching.ts index 43dcc8a6..58e4f507 100644 --- a/src/CursorBatching.ts +++ b/src/CursorBatching.ts @@ -4,7 +4,7 @@ import { CURSOR_UPDATE } from './CursorConstants.js'; import type { CursorUpdate } from './types.js'; import type { CursorsOptions } from './types.js'; -type OutgoingBuffer = { cursor: Pick<CursorUpdate, 'position' | 'data'>; offset: number }; +export type OutgoingBuffer = { cursor: Pick<CursorUpdate, 'position' | 'data'>; offset: number }; export default class CursorBatching { outgoingBuffer: OutgoingBuffer[] = []; diff --git a/src/CursorHistory.ts b/src/CursorHistory.ts index d99d86a6..8a8e532a 100644 --- a/src/CursorHistory.ts +++ b/src/CursorHistory.ts @@ -2,6 +2,7 @@ import { Types } from 'ably'; import type { CursorUpdate } from './types.js'; import type { CursorsOptions } from './types.js'; +import type { OutgoingBuffer } from './CursorBatching.js'; type ConnectionId = string; type ConnectionsLastPosition = Record<ConnectionId, null | CursorUpdate>; @@ -35,10 +36,17 @@ export default class CursorHistory { const lastMessage = page.items.find((item) => item.connectionId === connectionId); if (!lastMessage) return [connectionId, cursors]; - const { data, clientId }: { data: CursorUpdate[] } & Pick<Types.Message, 'clientId'> = lastMessage; + const { data = [], clientId }: { data: OutgoingBuffer[] } & Pick<Types.Message, 'clientId'> = lastMessage; - const lastUpdate = - data?.length > 0 ? this.messageToUpdate(connectionId, clientId, data[data.length - 1]) : null; + const lastPositionSet = data[data.length - 1]?.cursor; + const lastUpdate = lastPositionSet + ? { + clientId, + connectionId, + position: lastPositionSet.position, + data: lastPositionSet.data, + } + : null; return [connectionId, lastUpdate]; }), diff --git a/src/Cursors.test.ts b/src/Cursors.test.ts index 42b06b14..e51cb54a 100644 --- a/src/Cursors.test.ts +++ b/src/Cursors.test.ts @@ -387,13 +387,16 @@ describe('Cursors', () => { const client1Message = { connectionId: 'connectionId1', clientId: 'clientId1', - data: [{ position: { x: 1, y: 1 } }, { position: { x: 2, y: 3 }, data: { color: 'blue' } }], + data: [ + { cursor: { position: { x: 1, y: 1 } } }, + { cursor: { position: { x: 2, y: 3 }, data: { color: 'blue' } } }, + ], }; const client2Message = { connectionId: 'connectionId2', clientId: 'clientId2', - data: [{ position: { x: 25, y: 44 } }], + data: [{ cursor: { position: { x: 25, y: 44 } } }], }; vi.spyOn(channel.presence, 'get').mockImplementation(async () => [ From 87d3bba6818e518aab820d479ca5799bff940e29 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 29 Aug 2023 17:12:34 +0100 Subject: [PATCH 029/191] Remove mock-socket mock-socket is no longer used --- package-lock.json | 16 ---------------- package.json | 1 - 2 files changed, 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4f600ce..24277586 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "eslint-plugin-jsdoc": "^39.8.0", "eslint-plugin-security": "^1.7.1", "husky": "^8.0.0", - "mock-socket": "^9.1.5", "prettier": "^2.8.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", @@ -3724,15 +3723,6 @@ "ufo": "^1.3.0" } }, - "node_modules/mock-socket": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.1.5.tgz", - "integrity": "sha512-3DeNIcsQixWHHKk6NdoBhWI4t1VMj5/HzfnI1rE/pLl5qKx7+gd4DNA07ehTaZ6MoUU053si6Hd+YtiM/tQZfg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8014,12 +8004,6 @@ "ufo": "^1.3.0" } }, - "mock-socket": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.1.5.tgz", - "integrity": "sha512-3DeNIcsQixWHHKk6NdoBhWI4t1VMj5/HzfnI1rE/pLl5qKx7+gd4DNA07ehTaZ6MoUU053si6Hd+YtiM/tQZfg==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 8fbae1db..0b421bc7 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "eslint-plugin-jsdoc": "^39.8.0", "eslint-plugin-security": "^1.7.1", "husky": "^8.0.0", - "mock-socket": "^9.1.5", "prettier": "^2.8.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", From 8ee7dbc3cafaf7f29f59638c4d6b9b13fde3e283 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 08:24:04 +0000 Subject: [PATCH 030/191] build(deps-dev): bump rollup from 3.28.0 to 3.28.1 Bumps [rollup](https://github.com/rollup/rollup) from 3.28.0 to 3.28.1. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v3.28.0...v3.28.1) --- updated-dependencies: - dependency-name: rollup dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24277586..dffd5147 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4320,9 +4320,9 @@ } }, "node_modules/rollup": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.0.tgz", - "integrity": "sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==", + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", + "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -8398,9 +8398,9 @@ } }, "rollup": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.0.tgz", - "integrity": "sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==", + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", + "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", "dev": true, "requires": { "fsevents": "~2.3.2" From 6d2b7fa888ed4f9991e4bdbfa18afc45b66ed8f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 08:24:46 +0000 Subject: [PATCH 031/191] build(deps-dev): bump @typescript-eslint/parser from 5.51.0 to 5.62.0 Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.51.0 to 5.62.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.62.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 141 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index dffd5147..ec1209d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -897,14 +897,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.51.0.tgz", - "integrity": "sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.51.0", - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/typescript-estree": "5.51.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "engines": { @@ -923,6 +923,80 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.51.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz", @@ -5930,15 +6004,58 @@ } }, "@typescript-eslint/parser": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.51.0.tgz", - "integrity": "sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.51.0", - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/typescript-estree": "5.51.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + } + }, + "@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + } + } } }, "@typescript-eslint/scope-manager": { From c9b1c8603be3b8dc0b3fc5779d8048a92952aecf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 08:24:23 +0000 Subject: [PATCH 032/191] build(deps-dev): bump @vitest/coverage-c8 from 0.28.4 to 0.33.0 Bumps [@vitest/coverage-c8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-c8) from 0.28.4 to 0.33.0. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v0.33.0/packages/coverage-c8) --- updated-dependencies: - dependency-name: "@vitest/coverage-c8" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 670 ++++++---------------------------------------- package.json | 2 +- 2 files changed, 84 insertions(+), 588 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec1209d5..29ceee98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@rollup/plugin-terser": "^0.4.3", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", - "@vitest/coverage-c8": "^0.28.4", + "@vitest/coverage-c8": "^0.33.0", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^39.8.0", @@ -50,6 +50,19 @@ "bops": "^1.0.1" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -1125,152 +1138,23 @@ } }, "node_modules/@vitest/coverage-c8": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.28.4.tgz", - "integrity": "sha512-btelLBxaWhHnywXRQxDlrvPhGdnuIaD3XulsxcZRIcnpLPbFu39dNTT0IYu2QWP2ZZrV0AmNtdLIfD4c77zMAg==", - "dev": true, - "dependencies": { - "c8": "^7.12.0", - "picocolors": "^1.0.0", - "std-env": "^3.3.1", - "vitest": "0.28.4" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vitest/coverage-c8/node_modules/@vitest/expect": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.4.tgz", - "integrity": "sha512-JqK0NZ4brjvOSL8hXAnIsfi+jxDF7rH/ZWCGCt0FAqRnVFc1hXsfwXksQvEnKqD84avRt3gmeXoK4tNbmkoVsQ==", - "dev": true, - "dependencies": { - "@vitest/spy": "0.28.4", - "@vitest/utils": "0.28.4", - "chai": "^4.3.7" - } - }, - "node_modules/@vitest/coverage-c8/node_modules/@vitest/runner": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.4.tgz", - "integrity": "sha512-Q8UV6GjDvBSTfUoq0QXVCNpNOUrWu4P2qvRq7ssJWzn0+S0ojbVOxEjMt+8a32X6SdkhF8ak+2nkppsqV0JyNQ==", - "dev": true, - "dependencies": { - "@vitest/utils": "0.28.4", - "p-limit": "^4.0.0", - "pathe": "^1.1.0" - } - }, - "node_modules/@vitest/coverage-c8/node_modules/@vitest/spy": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.4.tgz", - "integrity": "sha512-8WuhfXLlvCXpNXEGJW6Gc+IKWI32435fQJLh43u70HnZ1otJOa2Cmg2Wy2Aym47ZnNCP4NolF+8cUPwd0MigKQ==", - "dev": true, - "dependencies": { - "tinyspy": "^1.0.2" - } - }, - "node_modules/@vitest/coverage-c8/node_modules/@vitest/utils": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.4.tgz", - "integrity": "sha512-l2QztOLdc2LkR+w/lP52RGh8hW+Ul4KESmCAgVE8q737I7e7bQoAfkARKpkPJ4JQtGpwW4deqlj1732VZD7TFw==", - "dev": true, - "dependencies": { - "cli-truncate": "^3.1.0", - "diff": "^5.1.0", - "loupe": "^2.3.6", - "picocolors": "^1.0.0", - "pretty-format": "^27.5.1" - } - }, - "node_modules/@vitest/coverage-c8/node_modules/vite-node": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.4.tgz", - "integrity": "sha512-KM0Q0uSG/xHHKOJvVHc5xDBabgt0l70y7/lWTR7Q0pR5/MrYxadT+y32cJOE65FfjGmJgxpVEEY+69btJgcXOQ==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.33.0.tgz", + "integrity": "sha512-DaF1zJz4dcOZS4k/neiQJokmOWqsGXwhthfmUdPGorXIQHjdPvV6JQSYhQDI41MyI8c+IieQUdIDs5XAMHtDDw==", + "deprecated": "v8 coverage is moved to @vitest/coverage-v8 package", "dev": true, "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.1.0", - "pathe": "^1.1.0", - "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "source-map-support": "^0.5.21", - "vite": "^3.0.0 || ^4.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": ">=v14.16.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vitest/coverage-c8/node_modules/vitest": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.4.tgz", - "integrity": "sha512-sfWIy0AdlbyGRhunm+TLQEJrFH9XuRPdApfubsyLcDbCRrUX717BRQKInTgzEfyl2Ipi1HWoHB84Nqtcwxogcg==", - "dev": true, - "dependencies": { - "@types/chai": "^4.3.4", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.28.4", - "@vitest/runner": "0.28.4", - "@vitest/spy": "0.28.4", - "@vitest/utils": "0.28.4", - "acorn": "^8.8.1", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.7", - "debug": "^4.3.4", - "local-pkg": "^0.4.2", - "pathe": "^1.1.0", + "@ampproject/remapping": "^2.2.1", + "c8": "^7.14.0", + "magic-string": "^0.30.1", "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "std-env": "^3.3.1", - "strip-literal": "^1.0.0", - "tinybench": "^2.3.1", - "tinypool": "^0.3.1", - "tinyspy": "^1.0.2", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.28.4", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": ">=v14.16.0" + "std-env": "^3.3.3" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } + "vitest": ">=0.30.0 <1" } }, "node_modules/@vitest/expect": { @@ -1719,9 +1603,9 @@ } }, "node_modules/c8": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", - "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -1845,72 +1729,6 @@ "node": "*" } }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -2095,15 +1913,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -2137,12 +1946,6 @@ "node": ">=6.0.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3517,23 +3320,23 @@ } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -3704,29 +3507,20 @@ "dev": true }, "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "dependencies": { - "semver": "^6.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -4185,32 +3979,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4271,12 +4039,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "node_modules/regexp-tree": { "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", @@ -4494,9 +4256,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4573,46 +4335,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/smob": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz", @@ -4853,24 +4575,6 @@ "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", "dev": true }, - "node_modules/tinypool": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz", - "integrity": "sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.1.1.tgz", - "integrity": "sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5122,9 +4826,9 @@ "dev": true }, "node_modules/v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -5469,6 +5173,16 @@ "bops": "^1.0.1" } }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -6128,109 +5842,16 @@ } }, "@vitest/coverage-c8": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.28.4.tgz", - "integrity": "sha512-btelLBxaWhHnywXRQxDlrvPhGdnuIaD3XulsxcZRIcnpLPbFu39dNTT0IYu2QWP2ZZrV0AmNtdLIfD4c77zMAg==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.33.0.tgz", + "integrity": "sha512-DaF1zJz4dcOZS4k/neiQJokmOWqsGXwhthfmUdPGorXIQHjdPvV6JQSYhQDI41MyI8c+IieQUdIDs5XAMHtDDw==", "dev": true, "requires": { - "c8": "^7.12.0", + "@ampproject/remapping": "^2.2.1", + "c8": "^7.14.0", + "magic-string": "^0.30.1", "picocolors": "^1.0.0", - "std-env": "^3.3.1", - "vitest": "0.28.4" - }, - "dependencies": { - "@vitest/expect": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.4.tgz", - "integrity": "sha512-JqK0NZ4brjvOSL8hXAnIsfi+jxDF7rH/ZWCGCt0FAqRnVFc1hXsfwXksQvEnKqD84avRt3gmeXoK4tNbmkoVsQ==", - "dev": true, - "requires": { - "@vitest/spy": "0.28.4", - "@vitest/utils": "0.28.4", - "chai": "^4.3.7" - } - }, - "@vitest/runner": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.4.tgz", - "integrity": "sha512-Q8UV6GjDvBSTfUoq0QXVCNpNOUrWu4P2qvRq7ssJWzn0+S0ojbVOxEjMt+8a32X6SdkhF8ak+2nkppsqV0JyNQ==", - "dev": true, - "requires": { - "@vitest/utils": "0.28.4", - "p-limit": "^4.0.0", - "pathe": "^1.1.0" - } - }, - "@vitest/spy": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.4.tgz", - "integrity": "sha512-8WuhfXLlvCXpNXEGJW6Gc+IKWI32435fQJLh43u70HnZ1otJOa2Cmg2Wy2Aym47ZnNCP4NolF+8cUPwd0MigKQ==", - "dev": true, - "requires": { - "tinyspy": "^1.0.2" - } - }, - "@vitest/utils": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.4.tgz", - "integrity": "sha512-l2QztOLdc2LkR+w/lP52RGh8hW+Ul4KESmCAgVE8q737I7e7bQoAfkARKpkPJ4JQtGpwW4deqlj1732VZD7TFw==", - "dev": true, - "requires": { - "cli-truncate": "^3.1.0", - "diff": "^5.1.0", - "loupe": "^2.3.6", - "picocolors": "^1.0.0", - "pretty-format": "^27.5.1" - } - }, - "vite-node": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.4.tgz", - "integrity": "sha512-KM0Q0uSG/xHHKOJvVHc5xDBabgt0l70y7/lWTR7Q0pR5/MrYxadT+y32cJOE65FfjGmJgxpVEEY+69btJgcXOQ==", - "dev": true, - "requires": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.1.0", - "pathe": "^1.1.0", - "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "source-map-support": "^0.5.21", - "vite": "^3.0.0 || ^4.0.0" - } - }, - "vitest": { - "version": "0.28.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.4.tgz", - "integrity": "sha512-sfWIy0AdlbyGRhunm+TLQEJrFH9XuRPdApfubsyLcDbCRrUX717BRQKInTgzEfyl2Ipi1HWoHB84Nqtcwxogcg==", - "dev": true, - "requires": { - "@types/chai": "^4.3.4", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.28.4", - "@vitest/runner": "0.28.4", - "@vitest/spy": "0.28.4", - "@vitest/utils": "0.28.4", - "acorn": "^8.8.1", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.7", - "debug": "^4.3.4", - "local-pkg": "^0.4.2", - "pathe": "^1.1.0", - "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "std-env": "^3.3.1", - "strip-literal": "^1.0.0", - "tinybench": "^2.3.1", - "tinypool": "^0.3.1", - "tinyspy": "^1.0.2", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.28.4", - "why-is-node-running": "^2.2.2" - } - } + "std-env": "^3.3.3" } }, "@vitest/expect": { @@ -6565,9 +6186,9 @@ "dev": true }, "c8": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", - "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", + "integrity": "sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", @@ -6658,50 +6279,6 @@ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, - "cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "requires": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -6841,12 +6418,6 @@ "object-keys": "^1.1.1" } }, - "diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true - }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -6871,12 +6442,6 @@ "esutils": "^2.0.2" } }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7900,20 +7465,20 @@ "dev": true }, "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "requires": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -8050,20 +7615,12 @@ } }, "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "semver": "^7.5.3" } }, "make-error": { @@ -8376,25 +7933,6 @@ "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", "dev": true }, - "pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -8432,12 +7970,6 @@ "safe-buffer": "^5.1.0" } }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "regexp-tree": { "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", @@ -8571,9 +8103,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -8632,30 +8164,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true - } - } - }, "smob": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz", @@ -8842,18 +8350,6 @@ "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", "dev": true }, - "tinypool": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz", - "integrity": "sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==", - "dev": true - }, - "tinyspy": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.1.1.tgz", - "integrity": "sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9033,9 +8529,9 @@ "dev": true }, "v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.12", diff --git a/package.json b/package.json index 0b421bc7..5ba0a557 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@rollup/plugin-terser": "^0.4.3", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", - "@vitest/coverage-c8": "^0.28.4", + "@vitest/coverage-c8": "^0.33.0", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^39.8.0", From 6f396c394b551009d8185715df30431c93abe051 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Wed, 30 Aug 2023 11:17:30 +0100 Subject: [PATCH 033/191] Update channel behaviours doc --- docs/channel-behaviors.md | 41 +++++++++------------------------------ 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/docs/channel-behaviors.md b/docs/channel-behaviors.md index e3949151..99362840 100644 --- a/docs/channel-behaviors.md +++ b/docs/channel-behaviors.md @@ -1,44 +1,21 @@ -# Channel Behaviors - -**Contents** - -- [Channel Behaviors](#channel-behaviors) - - [Introduction \& Context](#introduction--context) - - [Channels](#channels) - - [Space](#space) - - [Events Fired](#events-fired) - - [Cursors](#cursors) - - [Events Fired](#events-fired-1) +# Channel usage ## Introduction & Context -These channels are used by the Spaces library and features of the Spaces library including Live Cursors. - -It is not recommended that you rely on these channels directly, without the Spaces library, so this documentation is provided with the expectation that the channels are used for the purposes of monitoring, debugging, or learning more about how the Space API works. - -## Channels +The below channels are used by the Spaces library internally. -### Space +### Space channel -Each `Space` (as defined by the [`Space` class](/docs/class-definitions.md#space)) has its own `channel` assigned to it. +Each `Space` (as defined by the [`Space` class](/docs/class-definitions.md#space)) creates its own [Ably Channel](https://ably.com/docs/channels). -The `channel` name is defined by the `name` of the `Space` and takes the form: `_ably_space_${name}`. - -The full name of a `channel` belonging to a `Space` called 'my_space' would therefore be `_ably_space_my_space`. - -#### Events Fired - -1. `membersUpdate` - an update to any number of members of the Space -2. `locationUpdate` - an update specifically to the `Location` of any number of members of the Space +The channel name is defined by the `name` of the Space and takes the form: `_ably_space_${name}`. The full name of a `channel` belonging to a `Space` called 'my_space' would therefore be `_ably_space_my_space`. ### Cursors -Any `Space` may also make use of the Spaces library's `Cursors` feature. - -The `Cursors` `channel` name is defined by the `channelName` of the Space with the `_cursors` suffix, taking the form `${space.getChannelName()}_cursors`. +If any member of a `Space` subscribes to or sets cursor updates a channel is created for `cursors` updates. -The full name of a `channel` belonging to a set of `Cursors` belonging to a `Space` called 'my_space' would therefore be `_ably_space_my_space_cursors`. +The channel name is defined by the channel name of the Space with the `_cursors` suffix, taking the form: `${space.getChannelName()}_cursors`. The full name of a channel belonging to a `Space` called 'my_space' would therefore be `_ably_space_my_space_cursors`. -#### Events Fired +#### Events published -1. `cursorUpdate` - an update to any number of cursors associated with any number of members of the Space +1. `cursorUpdate` - a batch of cursor updates passed to [`set`](/docs/class-definitions.md#set). From 34d1b0b44268652c0a9b8946b58190e36c47badb Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Wed, 30 Aug 2023 11:17:56 +0100 Subject: [PATCH 034/191] Rename doc --- docs/{channel-behaviors.md => channel-usage.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{channel-behaviors.md => channel-usage.md} (100%) diff --git a/docs/channel-behaviors.md b/docs/channel-usage.md similarity index 100% rename from docs/channel-behaviors.md rename to docs/channel-usage.md From c5bd90996479a75a106d9e9e2e945b8c357c8c7e Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Wed, 30 Aug 2023 17:17:58 +0100 Subject: [PATCH 035/191] Flatten lock structure Move properties from the request key to the lock. Having the request property proved to be not-intuitive and wordy. --- src/Locks.test.ts | 15 +++---- src/Locks.ts | 88 ++++++++++++++++++++++-------------------- src/index.ts | 1 - src/types.ts | 6 +-- src/utilities/types.ts | 4 +- 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/Locks.test.ts b/src/Locks.test.ts index 18d510b8..5c1023d3 100644 --- a/src/Locks.test.ts +++ b/src/Locks.test.ts @@ -106,7 +106,8 @@ describe('Locks (mockClient)', () => { const lockEvent = (member: SpaceMember, status: LockStatus) => expect.objectContaining({ member: member, - request: expect.objectContaining({ id: lockID, status }), + id: lockID, + status, }); it<SpaceTestContext>('sets a PENDING request to LOCKED', async ({ space }) => { @@ -130,7 +131,7 @@ describe('Locks (mockClient)', () => { }); await space.locks.processPresenceMessage(msg); - const lock = space.locks.getLockRequest(lockID, member.connectionId)!; + const lock = space.locks.getLock(lockID, member.connectionId)!; expect(lock.status).toBe('locked'); expect(emitSpy).toHaveBeenCalledWith('update', lockEvent(member, 'locked')); }); @@ -214,10 +215,10 @@ describe('Locks (mockClient)', () => { }); await space.locks.processPresenceMessage(msg); const selfMember = (await space.members.getByConnectionId(client.connection.id!))!; - const selfLock = space.locks.getLockRequest(lockID, selfMember.connectionId)!; + const selfLock = space.locks.getLock(lockID, selfMember.connectionId)!; expect(selfLock.status).toBe(expectedSelfStatus); const otherMember = (await space.members.getByConnectionId(otherConnId))!; - const otherLock = space.locks.getLockRequest(lockID, otherMember.connectionId)!; + const otherLock = space.locks.getLock(lockID, otherMember.connectionId)!; expect(otherLock.status).toBe(expectedOtherStatus); if (expectedSelfStatus === 'unlocked') { @@ -259,7 +260,7 @@ describe('Locks (mockClient)', () => { }); await space.locks.processPresenceMessage(msg); - const lock = space.locks.getLockRequest(lockID, member.connectionId); + const lock = space.locks.getLock(lockID, member.connectionId); expect(lock).not.toBeDefined(); expect(emitSpy).toHaveBeenCalledWith('update', lockEvent(member, 'unlocked')); }); @@ -393,7 +394,7 @@ describe('Locks (mockClient)', () => { const locks = space.locks.getAll(); expect(locks.length).toEqual(3); for (const lock of locks) { - switch (lock.request.id) { + switch (lock.id) { case 'lock1': case 'lock2': expect(lock.member).toEqual(member1); @@ -402,7 +403,7 @@ describe('Locks (mockClient)', () => { expect(lock.member).toEqual(member2); break; default: - throw new Error(`unexpected lock id: ${lock.request.id}`); + throw new Error(`unexpected lock id: ${lock.id}`); } } }); diff --git a/src/Locks.ts b/src/Locks.ts index 06a8144a..feb20e3f 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -1,7 +1,7 @@ import { Types } from 'ably'; import Space from './Space.js'; -import type { Lock, LockRequest, SpaceMember } from './types.js'; +import type { Lock, SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import { ERR_LOCK_IS_LOCKED, @@ -52,18 +52,18 @@ export default class Locks extends EventEmitter<LockEventMap> { const locks = this.locks.get(id); if (!locks) return; for (const lock of locks.values()) { - if (lock.request.status === 'locked') { + if (lock.status === 'locked') { return lock; } } } getAll(): Lock[] { - const allLocks = []; + const allLocks: Lock[] = []; for (const locks of this.locks.values()) { for (const lock of locks.values()) { - if (lock.request.status === 'locked') { + if (lock.status === 'locked') { allLocks.push(lock); } } @@ -72,7 +72,7 @@ export default class Locks extends EventEmitter<LockEventMap> { return allLocks; } - async acquire(id: string, opts?: LockOptions): Promise<LockRequest> { + async acquire(id: string, opts?: LockOptions): Promise<Lock> { const self = await this.space.members.getSelf(); if (!self) { throw ERR_NOT_ENTERED_SPACE; @@ -80,26 +80,29 @@ export default class Locks extends EventEmitter<LockEventMap> { // check there isn't an existing PENDING or LOCKED request for the current // member, since we do not support nested locks - let req = this.getLockRequest(id, self.connectionId); - if (req && req.status !== 'unlocked') { + let lock = this.getLock(id, self.connectionId); + if (lock && lock.status !== 'unlocked') { throw ERR_LOCK_REQUEST_EXISTS; } // initialise a new PENDING request - req = { + lock = { id, status: 'pending', timestamp: Date.now(), + member: self, }; + if (opts) { - req.attributes = opts.attributes; + lock.attributes = opts.attributes; } - this.setLock({ member: self, request: req }); + + this.setLock(lock); // reflect the change in the member's presence data await this.updatePresence(self); - return req; + return lock; } async release(id: string): Promise<void> { @@ -156,18 +159,20 @@ export default class Locks extends EventEmitter<LockEventMap> { // existing locks for that member for (const locks of this.locks.values()) { const lock = locks.get(member.connectionId); + if (lock) { - lock.request.status = 'unlocked'; - lock.request.reason = ERR_LOCK_RELEASED; + lock.status = 'unlocked'; + lock.reason = ERR_LOCK_RELEASED; locks.delete(member.connectionId); this.emit('update', lock); } } + return; } - message.extras.locks.forEach((lock: LockRequest) => { - const existing = this.getLockRequest(lock.id, member.connectionId); + message.extras.locks.forEach((lock: Lock) => { + const existing = this.getLock(lock.id, member.connectionId); // special-case the handling of PENDING requests, which will eventually // be done by the Ably system, at which point this can be removed @@ -176,21 +181,23 @@ export default class Locks extends EventEmitter<LockEventMap> { } if (!existing || existing.status !== lock.status) { - this.emit('update', { member, request: lock }); + this.emit('update', { ...lock, member }); } - this.setLock({ member, request: lock }); + this.setLock({ ...lock, member }); }); // handle locks which have been removed from presence extras for (const locks of this.locks.values()) { const lock = locks.get(member.connectionId); + if (!lock) { continue; } - if (!message.extras.locks.some((req: LockRequest) => req.id === lock.request.id)) { - lock.request.status = 'unlocked'; - lock.request.reason = ERR_LOCK_RELEASED; + + if (!message.extras.locks.some((l: Lock) => l.id === lock.id)) { + lock.status = 'unlocked'; + lock.reason = ERR_LOCK_RELEASED; locks.delete(member.connectionId); this.emit('update', lock); } @@ -203,12 +210,12 @@ export default class Locks extends EventEmitter<LockEventMap> { // // TODO: remove this once the Ably system processes PENDING requests // internally using this same logic. - processPending(member: SpaceMember, pendingReq: LockRequest) { + processPending(member: SpaceMember, pendingLock: Lock) { // if the requested lock ID is not currently locked, then mark the PENDING - // request as LOCKED - const lock = this.get(pendingReq.id); + // lock request as LOCKED + const lock = this.get(pendingLock.id); if (!lock) { - pendingReq.status = 'locked'; + pendingLock.status = 'locked'; return; } @@ -231,20 +238,20 @@ export default class Locks extends EventEmitter<LockEventMap> { // a connectionId which sorts lexicographically before the connectionId of // the LOCKED request. if ( - pendingReq.timestamp < lock.request.timestamp || - (pendingReq.timestamp == lock.request.timestamp && member.connectionId < lock.member.connectionId) + pendingLock.timestamp < lock.timestamp || + (pendingLock.timestamp == lock.timestamp && member.connectionId < lock.member.connectionId) ) { - pendingReq.status = 'locked'; - lock.request.status = 'unlocked'; - lock.request.reason = ERR_LOCK_INVALIDATED; + pendingLock.status = 'locked'; + lock.status = 'unlocked'; + lock.reason = ERR_LOCK_INVALIDATED; this.emit('update', lock); return; } // the lock is LOCKED and the PENDING request did not invalidate it, so // mark the PENDING request as UNLOCKED with a reason. - pendingReq.status = 'unlocked'; - pendingReq.reason = ERR_LOCK_IS_LOCKED; + pendingLock.status = 'unlocked'; + pendingLock.reason = ERR_LOCK_IS_LOCKED; } updatePresence(member: SpaceMember) { @@ -269,10 +276,10 @@ export default class Locks extends EventEmitter<LockEventMap> { } setLock(lock: Lock) { - let locks = this.locks.get(lock.request.id); + let locks = this.locks.get(lock.id); if (!locks) { locks = new Map(); - this.locks.set(lock.request.id, locks); + this.locks.set(lock.id, locks); } locks.set(lock.member.connectionId, lock); } @@ -283,25 +290,22 @@ export default class Locks extends EventEmitter<LockEventMap> { return locks.delete(connectionId); } - getLockRequest(id: string, connectionId: string): LockRequest | undefined { - const lock = this.getLock(id, connectionId); - if (!lock) return; - return lock.request; - } + getLocksForConnectionId(connectionId: string): Lock[] { + const requests: Lock[] = []; - getLockRequests(connectionId: string): LockRequest[] { - const requests = []; for (const locks of this.locks.values()) { const lock = locks.get(connectionId); + if (lock) { - requests.push(lock.request); + requests.push(lock); } } + return requests; } getLockExtras(connectionId: string): PresenceMember['extras'] { - const locks = this.getLockRequests(connectionId); + const locks = this.getLocksForConnectionId(connectionId); if (locks.length === 0) return; return { locks }; } diff --git a/src/index.ts b/src/index.ts index 4b47cb95..f51aacdb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,6 @@ export type { ProfileData, SpaceMember, Lock, - LockRequest, LockStatus, } from './types.js'; diff --git a/src/types.ts b/src/types.ts index 9698e220..9641b53c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,13 +42,9 @@ export interface SpaceMember { export type LockStatus = 'pending' | 'locked' | 'unlocked'; export type Lock = { - member: SpaceMember; - request: LockRequest; -}; - -export type LockRequest = { id: string; status: LockStatus; + member: SpaceMember; timestamp: number; attributes?: LockAttributes; reason?: Types.ErrorInfo; diff --git a/src/utilities/types.ts b/src/utilities/types.ts index 26fb1094..cd3e2f5a 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -1,7 +1,7 @@ import type { Types } from 'ably'; import type { EventKey, EventListener, EventMap } from './EventEmitter.js'; -import type { ProfileData, LockRequest } from '../types.js'; +import type { ProfileData, Lock } from '../types.js'; export type PresenceMember = { data: { @@ -16,7 +16,7 @@ export type PresenceMember = { }; }; extras?: { - locks: LockRequest[]; + locks: Lock[]; }; } & Omit<Types.PresenceMessage, 'data'>; From bde04489e96f88a07c79714e53fd3fd219ee411c Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 29 Aug 2023 13:19:05 +0100 Subject: [PATCH 036/191] Documentation tweaks --- README.md | 28 ++++++++++++++-------------- docs/class-definitions.md | 18 +++++++++--------- docs/usage.md | 6 +++--- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b7a6db3b..b4f66116 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ To instantiate the Spaces SDK, create an [Ably client](https://ably.com/docs/get import Spaces from '@ably-labs/spaces'; import { Realtime } from 'ably'; -const client = new Realtime.Promise({key: "<API-key>", clientId: "<client-ID>"}); +const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); const spaces = new Spaces(client); ``` You can use [basic authentication](https://ably.com/docs/auth/basic) i.e. the API Key directly for testing purposes, however it is strongly recommended that you use [token authentication](https://ably.com/docs/auth/token) in production environments. @@ -107,7 +107,7 @@ space.subscribe('update', (spaceState) => { }); // Enter a space, publishing an update event, including optional profile data -space.enter({ +await space.enter({ username: 'Claire Lemons', avatar: 'https://slides-internal.com/users/clemons.png', }); @@ -136,7 +136,7 @@ The following is an example event payload received by subscribers when a user en ## Space members -The `members` namespace within a Space is a client-side filtered listener optimized for building avatar stacks. Subscribe to members entering, leaving, being removed from the Space (after a timeout) or updating their profile information. +The `members` namespace contains methods dedicated to building avatar stacks. Subscribe to members entering, leaving, being removed from the Space (after a timeout) or updating their profile information. ```ts // Subscribe to all member events in a space @@ -201,17 +201,17 @@ const othersMemberInfo = await space.members.getOthers(); ## Member locations -The `locations` namespace within a Space is a client-side filtered listener optimized for building member locations which enable you to track where users are within an application. A location could be a form field, multiple cells in a spreadsheet or a slide in a slide deck editor. +The `locations` namespace contains methods dedicated to building member locations, enabling you to track where users are within an application. A location could be a form field, multiple cells in a spreadsheet or a slide in a slide deck editor. ```ts // You need to enter a space before publishing your location -space.enter({ +await space.enter({ username: 'Claire Lemons', avatar: 'https://slides-internal.com/users/clemons.png', }); // Publish your location based on the UI element selected -space.locations.set({ slide: '3', component: 'slide-title' }); +await space.locations.set({ slide: '3', component: 'slide-title' }); // Subscribe to location events from all members in a space space.locations.subscribe('update', (locationUpdate) => { @@ -258,22 +258,22 @@ Member locations has methods to get the current snapshot of member state: ```ts // Get a snapshot of all the member locations -const allLocations = space.locations.getAll(); +const allLocations = await space.locations.getAll(); // Get a snapshot of my location -const myLocation = space.locations.getSelf(); +const myLocation = await space.locations.getSelf(); // Get a snapshot of everyone else's locations -const othersLocations = space.locations.getOthers(); +const othersLocations = await space.locations.getOthers(); ``` ## Live cursors -The `cursors` namespace within a Space is a client-side filtered listener optimized for building live cursors which allows you to track a member's pointer position updates across an application. Events can also include associated data, such as pointer attributes and the IDs of associated UI elements: +The `cursors` namespace contains methods dedicated to building live cursors, allowing you to track a member's pointer position updates across an application. Events can also include associated data, such as pointer attributes and the IDs of associated UI elements: ```ts // You need to enter a space before publishing your cursor updates -space.enter({ +await space.enter({ username: 'Claire Lemons', avatar: 'https://slides-internal.com/users/clemons.png', }); @@ -306,11 +306,11 @@ Member cursors has methods to get the current snapshot of member state: ```ts // Get a snapshot of all the cursors -const allCursors = space.cursors.getAll(); +const allCursors = await space.cursors.getAll(); // Get a snapshot of my cursor -const myCursor = space.cursors.getSelf(); +const myCursor = await space.cursors.getSelf(); // Get a snapshot of everyone else's cursors -const othersCursors = space.cursors.getOthers(); +const othersCursors = await space.cursors.getOthers(); ``` diff --git a/docs/class-definitions.md b/docs/class-definitions.md index fdc4fce7..4c087364 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -224,10 +224,10 @@ space.members.unsubscribe('leave'); #### getSelf() -Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `undefined` if the client hasn't entered the space yet. +Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `null` if the client hasn't entered the space yet. ```ts -type getSelf = () => Promise<SpaceMember | undefined>; +type getSelf = () => Promise<SpaceMember | null>; ``` Example: @@ -355,7 +355,7 @@ space.locations.unsubscribe('update'); Get location for self. ```ts -type getSelf = () => Promise<Location | undefined>; +type getSelf = () => Promise<Location>; ``` Example: @@ -465,13 +465,13 @@ space.cursors.unsubscribe('update'); Get the last `CursorUpdate` object for self. ```ts -type getSelf = () => <CursorUpdate | undefined>; +type getSelf = () => Promise<CursorUpdate | null>; ``` Example: ```ts -const selfPosition = space.cursors.getSelf(); +const selfPosition = await space.cursors.getSelf(); ``` #### getAll() @@ -479,13 +479,13 @@ const selfPosition = space.cursors.getSelf(); Get the last `CursorUpdate` object for all the members. ```ts -type getAll = () => Record<ConnectionId, CursorUpdate>; +type getAll = () => Promise<Record<string, CursorUpdate | null>>; ``` Example: ```ts -const allLatestPositions = space.cursors.getAll(); +const allLatestPositions = await space.cursors.getAll(); ``` #### getOthers() @@ -493,13 +493,13 @@ const allLatestPositions = space.cursors.getAll(); Get the last `CursorUpdate` object for everyone else but yourself. ```ts -type getOthers = () => Record<ConnectionId, CursorUpdate>; +type getOthers = () => Promise<Record<string, CursorUpdate | null>>; ``` Example: ```ts -const otherPositions = space.cursors.getOthers(); +const otherPositions = await space.cursors.getOthers(); ``` ### Related types diff --git a/docs/usage.md b/docs/usage.md index 62d22c35..82b69a4d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -96,13 +96,13 @@ space.unsubscribe(); To become a member of a space (and use the other APIs, like location or cursors) a client needs to enter a space. ```ts -space.enter(); +await space.enter(); ``` This method can take an optional object called `profileData` so that users can include convenient metadata to update an avatar stack, such as a username and profile picture. ```ts -space.enter({ +await space.enter({ username: 'Claire Lemons', avatar: 'https://slides-internal.com/users/clemons.png', }); @@ -121,7 +121,7 @@ A leave event does not remove the member immediately from `space.members`. Inste As with `enter`, you can update the `profileData` on leave: ```ts -space.leave({ +await space.leave({ username: 'Claire Lemons', avatar: 'https://slides-internal.com/users/inactive.png', }); From 32d9c14d1d3a4d033a166a8ca28f8a11a367a0eb Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 11 Aug 2023 13:06:15 +0100 Subject: [PATCH 037/191] Add documentation for simple connection and channels management --- docs/class-definitions.md | 12 ++++- docs/connection-and-channel-management.md | 53 +++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 docs/connection-and-channel-management.md diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 4c087364..993ab045 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -20,12 +20,20 @@ Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/se ### Properties -#### ably +#### client Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) client that was passed to the [constructor](#constructor). ```ts -type ably = Ably.RealtimePromise; +type client = Ably.RealtimePromise; +``` + +#### connection + +Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) connection, belonging to the client that was passed to the [constructor](#constructor). + +```ts +type connection = Ably.ConnectionPromise; ``` #### version diff --git a/docs/connection-and-channel-management.md b/docs/connection-and-channel-management.md new file mode 100644 index 00000000..d72dd016 --- /dev/null +++ b/docs/connection-and-channel-management.md @@ -0,0 +1,53 @@ +# Connection and channel management + +Spaces SDK uses the Ably Core SDK client [connections](https://ably.com/docs/connect) and [channels](https://ably.com/docs/channels) to provide higher level features like cursors or locations. Both connections and channels will transition through multiple states throughout their lifecycle. Most state transitions (like a short loss in connectivity) will be handled by the Ably SDK, but there will be use cases where developers will need to observe these states and handle them accordingly. + +This document describes how to access a connection and channels on Spaces, and where to find information about how to handle their state changes. + +## Connection + +When initializing the Spaces SDK, an Ably client is passed as a required argument: + +```ts +const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); +const spaces = new Spaces(client); +``` + +The Spaces instance exposes the underlying connection which is an event emitter. It can be used to listen for changes in connection state. The client and connection are both available on the Spaces instance: + +```ts +spaces.client.connection.on('disconnected', () => {}) +spaces.connection.on('disconnected', () => {}) +``` + +### Read more on: + +- [Connections](https://ably.com/docs/connect) +- [Connection state and recovery](https://ably.com/docs/connect/states) + +## Channels + +When a Space is instantiated, it creates an underlying [Ably Channel](https://ably.com/docs/channels) which is used to deliver the functionality of each space. The Ably Channel object is available in the `.channel` attribute. + +Similar to a connection, a channel is an event emitter, allowing us to listen for state changes: + +```ts +const mySpace = spaces.get('mySpace'); +mySpace.channel.once('suspended', () => {}); +``` + +When using the Cursors API, an additional channel is used, but it will only be created if we attach a subscriber or set a cursor position: + +```ts +mySpace.cursors.channel.once('attached', () => {}); + +// but .channel will only be defined if one of these was called before +mySpace.cursors.set({ position: { x, y } });    +mySpace.cursors.subscribe('update', () => {}); +``` + +### Read more on: + +- [Channels](https://ably.com/docs/channels) +- [Channel states](https://ably.com/docs/channels#states) +- [Handle channel failure](https://ably.com/docs/channels#failure) \ No newline at end of file From e2b935d3edcd63d1858fea179c2b37a34d6e79ff Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 11 Aug 2023 13:17:05 +0100 Subject: [PATCH 038/191] Make channel on Cursors public --- src/Cursors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index b7101723..7cc02a1a 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -24,9 +24,10 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { private readonly cursorBatching: CursorBatching; private readonly cursorDispensing: CursorDispensing; private readonly cursorHistory: CursorHistory; - private channel?: Types.RealtimeChannelPromise; readonly options: CursorsOptions; + public channel?: Types.RealtimeChannelPromise; + constructor(private space: Space) { super(); From 207dc952838dea9301778c1be23727c78218337f Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 29 Aug 2023 17:22:00 +0100 Subject: [PATCH 039/191] Rename .ably to .client --- src/Spaces.test.ts | 4 ++-- src/Spaces.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index f204abab..a061fe35 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -16,7 +16,7 @@ describe('Spaces', () => { it<SpacesTestContext>('expects the injected client to be of the type RealtimePromise', ({ client }) => { const spaces = new Spaces(client); - expectTypeOf(spaces.ably).toMatchTypeOf<Types.RealtimePromise>(); + expectTypeOf(spaces.client).toMatchTypeOf<Types.RealtimePromise>(); }); it<SpacesTestContext>('creates and retrieves spaces successfully', async ({ client }) => { @@ -41,7 +41,7 @@ describe('Spaces', () => { it<SpacesTestContext>('extend the agents array when it already exists', ({ client }) => { (client as ClientWithOptions).options.agents = { 'some-client': '1.2.3' }; const spaces = new Spaces(client); - const ablyClient = spaces.ably as ClientWithOptions; + const ablyClient = spaces.client as ClientWithOptions; expect(ablyClient.options.agents).toEqual({ 'some-client': '1.2.3', diff --git a/src/Spaces.ts b/src/Spaces.ts index db589d0b..2f9c8012 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -14,14 +14,14 @@ export interface ClientWithOptions extends Types.RealtimePromise { class Spaces { private spaces: Record<string, Space> = {}; - ably: Types.RealtimePromise; + client: Types.RealtimePromise; readonly version = '0.0.13'; constructor(client: Types.RealtimePromise) { - this.ably = client; - this.addAgent((this.ably as ClientWithOptions)['options']); - this.ably.time(); + this.client = client; + this.addAgent((this.client as ClientWithOptions)['options']); + this.client.time(); } private addAgent(options: { agents?: Record<string, string | boolean> }) { @@ -36,11 +36,11 @@ class Spaces { if (this.spaces[name]) return this.spaces[name]; - if (this.ably.connection.state !== 'connected') { - await this.ably.connection.once('connected'); + if (this.client.connection.state !== 'connected') { + await this.client.connection.once('connected'); } - const space = new Space(name, this.ably, options); + const space = new Space(name, this.client, options); this.spaces[name] = space; return space; From 32ce87efdcf67bc8975a99b3296ba9a47bee94d2 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 29 Aug 2023 17:23:38 +0100 Subject: [PATCH 040/191] Expose connection on Spaces directly --- src/Spaces.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Spaces.ts b/src/Spaces.ts index 2f9c8012..bd9a5f8a 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -15,11 +15,13 @@ export interface ClientWithOptions extends Types.RealtimePromise { class Spaces { private spaces: Record<string, Space> = {}; client: Types.RealtimePromise; + connection: Types.ConnectionPromise; readonly version = '0.0.13'; constructor(client: Types.RealtimePromise) { this.client = client; + this.connection = client.connection; this.addAgent((this.client as ClientWithOptions)['options']); this.client.time(); } @@ -36,8 +38,8 @@ class Spaces { if (this.spaces[name]) return this.spaces[name]; - if (this.client.connection.state !== 'connected') { - await this.client.connection.once('connected'); + if (this.connection.state !== 'connected') { + await this.connection.once('connected'); } const space = new Space(name, this.client, options); From 9883e1f03ec97a778cc368fa79ec2f4d2714ea25 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 1 Sep 2023 15:37:18 +0100 Subject: [PATCH 041/191] Add getSelf and getOthers methods to locks --- src/Locks.test.ts | 84 +++++++++++++++++++++++++++++++++++++---------- src/Locks.ts | 21 +++++++++++- 2 files changed, 86 insertions(+), 19 deletions(-) diff --git a/src/Locks.test.ts b/src/Locks.test.ts index 5c1023d3..1db845be 100644 --- a/src/Locks.test.ts +++ b/src/Locks.test.ts @@ -343,8 +343,8 @@ describe('Locks (mockClient)', () => { }); }); - describe('getAll', () => { - it<SpaceTestContext>('returns all locks in the LOCKED state', async ({ space }) => { + describe('get*', () => { + beforeEach<SpaceTestContext>(async ({ space }) => { await space.locks.processPresenceMessage( Realtime.PresenceMessage.fromValues({ action: 'update', @@ -381,31 +381,79 @@ describe('Locks (mockClient)', () => { }, }), ); + }); - const member1 = await space.members.getByConnectionId('1')!; - const member2 = await space.members.getByConnectionId('2')!; + it<SpaceTestContext>('correctly sets up locks', ({ space }) => { const lock1 = space.locks.get('lock1'); expect(lock1).toBeDefined(); + const lock2 = space.locks.get('lock2'); expect(lock2).toBeDefined(); + const lock3 = space.locks.get('lock3'); expect(lock3).toBeDefined(); + }); - const locks = space.locks.getAll(); - expect(locks.length).toEqual(3); - for (const lock of locks) { - switch (lock.id) { - case 'lock1': - case 'lock2': - expect(lock.member).toEqual(member1); - break; - case 'lock3': - expect(lock.member).toEqual(member2); - break; - default: - throw new Error(`unexpected lock id: ${lock.id}`); + describe('getSelf', () => { + it<SpaceTestContext>('returns all locks in the LOCKED state that belong to self', async ({ space }) => { + const member1 = await space.members.getByConnectionId('1')!; + + const locks = await space.locks.getSelf(); + expect(locks.length).toEqual(2); + + for (const lock of locks) { + switch (lock.id) { + case 'lock1': + case 'lock2': + expect(lock.member).toEqual(member1); + break; + default: + throw new Error(`unexpected lock id: ${lock.id}`); + } } - } + }); + }); + + describe('getOthers', () => { + it<SpaceTestContext>('returns all locks in the LOCKED state for every member but self', async ({ space }) => { + const member2 = await space.members.getByConnectionId('2')!; + + const locks = await space.locks.getOthers(); + expect(locks.length).toEqual(1); + + for (const lock of locks) { + switch (lock.id) { + case 'lock3': + expect(lock.member).toEqual(member2); + break; + default: + throw new Error(`unexpected lock id: ${lock.id}`); + } + } + }); + }); + + describe('getAll', () => { + it<SpaceTestContext>('returns all locks in the LOCKED state', async ({ space }) => { + const member1 = await space.members.getByConnectionId('1')!; + const member2 = await space.members.getByConnectionId('2')!; + + const locks = await space.locks.getAll(); + expect(locks.length).toEqual(3); + for (const lock of locks) { + switch (lock.id) { + case 'lock1': + case 'lock2': + expect(lock.member).toEqual(member1); + break; + case 'lock3': + expect(lock.member).toEqual(member2); + break; + default: + throw new Error(`unexpected lock id: ${lock.id}`); + } + } + }); }); }); }); diff --git a/src/Locks.ts b/src/Locks.ts index feb20e3f..0b08b2d7 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -58,7 +58,9 @@ export default class Locks extends EventEmitter<LockEventMap> { } } - getAll(): Lock[] { + // This will be async in the future, when pending requests are no longer processed + // in the library. + async getAll(): Promise<Lock[]> { const allLocks: Lock[] = []; for (const locks of this.locks.values()) { @@ -72,6 +74,23 @@ export default class Locks extends EventEmitter<LockEventMap> { return allLocks; } + async getSelf(): Promise<Lock[]> { + const self = await this.space.members.getSelf(); + + if (!self) return []; + + return this.getLocksForConnectionId(self.connectionId).filter((lock) => lock.status === 'locked'); + } + + async getOthers(): Promise<Lock[]> { + const self = await this.space.members.getSelf(); + const allLocks = await this.getAll(); + + if (!self) return allLocks; + + return allLocks.filter((lock) => lock.member.connectionId !== self.connectionId); + } + async acquire(id: string, opts?: LockOptions): Promise<Lock> { const self = await this.space.members.getSelf(); if (!self) { From ec80ff6d17a22d76123fdcd710aa1918533e80be Mon Sep 17 00:00:00 2001 From: Lewis Marshall <lewis.marshall@ably.com> Date: Mon, 14 Aug 2023 16:20:25 +0100 Subject: [PATCH 042/191] docs: Add Component Locking section to README Signed-off-by: Lewis Marshall <lewis.marshall@ably.com> --- README.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/README.md b/README.md index b4f66116..eea4ae84 100644 --- a/README.md +++ b/README.md @@ -314,3 +314,103 @@ const myCursor = await space.cursors.getSelf(); // Get a snapshot of everyone else's cursors const othersCursors = await space.cursors.getOthers(); ``` + +### Component Locking + +Use the Component Locking API to lock stateful components whilst being edited by members to reduce the chances of conflicting changes being made. + +Locks are identified using a unique string, and the Spaces SDK maintains that at most one member holds a lock with a given string at any given time. + +The Component Locking API supports four operations: Query, Acquire, Release, and Subscribe. + +### Query + +`space.locks.get` is used to query whether a lock identifier is currently locked and by whom. It returns a `Lock` type which has the following fields: + +```ts +type Lock = { + id: string; + status: LockStatus; + member: SpaceMember; + timestamp: number; + attributes?: LockAttributes; + reason?: Types.ErrorInfo; +}; +``` + +For example: + +```ts +// check if the id is locked +const isLocked = space.locks.get(id) !== undefined; + +// check which member has the lock +const { member } = space.locks.get(id); + +// check the lock attributes assigned by the member holding the lock +const { attributes } = space.locks.get(id); +const value = attributes.get(key); +``` + +`space.locks.getAll` returns all lock identifiers which are currently locked as an array of `Lock`: + +```ts +const allLocks = space.locks.getAll(); + +for (const lock of allLocks) { + // ... +} +``` + +### Acquire + +`space.locks.acquire` sends a request to acquire a lock using presence. + +It returns a Promise which resolves once the presence request has been sent. + +```ts +const req = await space.locks.acquire(id); + +// or with some attributes +const attributes = new Map(); +attributes.set('key', 'value'); +const req = await space.locks.acquire(id, { attributes }); +``` + +It throws an error if a lock request already exists for the given identifier with a status of `pending` or `locked`. + +### Release + +`space.locks.release` releases a previously requested lock by removing it from presence. + +It returns a Promise which resolves once the presence request has been sent. + +```ts +await space.locks.release(id); +``` + +### Subscribe + +`space.locks.subscribe` subscribes to changes in lock status across all members. + +The callback is called with a value of type `Lock`. + +```ts +space.locks.subscribe('update', (lock) => { + // lock.member is the requesting member + // lock.request is the request made by the member +}); + +// or with destructuring: +space.locks.subscribe('update', ({ member, request }) => { + // request.status is the status of the request, one of PENDING, LOCKED, or UNLOCKED + // request.reason is an ErrorInfo if the status is UNLOCKED +}); +``` + +Such changes occur when: + +- a `pending` request transitions to `locked` because the requesting member now holds the lock +- a `pending` request transitions to `unlocked` because the requesting member does not hold the lock since another member already does +- a `locked` request transitions to `unlocked` because the lock was either released or invalidated by a concurrent request which took precedence +- an `unlocked` request transitions to `locked` because the requesting member reacquired a lock From a94389dfddbc50935bed76c9f557c8b5721f0a05 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 4 Sep 2023 10:24:15 +0100 Subject: [PATCH 043/191] Add class definitions for locks --- docs/class-definitions.md | 166 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 6 deletions(-) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 993ab045..86fc5c13 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -168,13 +168,15 @@ Handles members within a space. Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. - ```ts - space.members.subscribe((member: SpaceMember) => {}); - ``` +The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event. + +Example: - The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event. +```ts +space.members.subscribe((member: SpaceMember) => {}); +``` - Available events: +Available events: - ##### **enter** @@ -449,7 +451,7 @@ Set the position of a cursor. If a member has not yet entered the space, this me A event payload returned contains an object with 2 properties. `position` is an object with 2 required properties, `x` and `y`. These represent the position of the cursor on a 2D plane. A second optional property, `data` can also be passed. This is an object of any shape and is meant for data associated with the cursor movement (like drag or hover calculation results): ```ts -type set = (update: { position: CursorPosition, data?: CursorData }) +type set = (update: { position: CursorPosition, data?: CursorData }) => void; ``` Example usage: @@ -544,3 +546,155 @@ Represent data that can be associated with a cursor update. ```ts type CursorData = Record<string, unknown>; ``` + +## Component Locking + +Provides a mechanism to "lock" a component, reducing the chances of conflict in an application whilst being edited by multiple members. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + +### Methods + +#### acquire() + +Send a request to acquire a lock. Returns a Promise which resolves once the request has been sent. A resolved Promise holds a `pending` [Lock](#lock). An error will be thrown if a lock request with a status of `pending` or `locked` already exists, returning a rejected promise. + +When a lock acquisition by a member is confirmed with the `locked` status, an `update` event will be emitted. Hence to handle lock acquisition, `acquire()` needs to always be used together with `subscribe()`. + +```ts +type acquire = (lockId: string) => Promise<Lock>; +``` + +Example: + +```ts +const id = "/slide/1/element/3"; +const lockRequest = await space.locks.acquire(id); +``` + +#### release() + +Releases a previously requested lock. + +```ts +type release = (lockId: string) => Promise<void>; +``` + +Example: + +```ts +const id = "/slide/1/element/3"; +await space.locks.release(id); +``` + +#### subscribe() + +Listen to lock events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + +Available events: + +- ##### **update** + + Listen to changes to locks. + + ```ts + space.locks.subscribe('update', (lock: Lock) => {}) + ``` + + The argument supplied to the callback is a [Lock](#lock), representing the lock request and it's status. + +#### unsubscribe() + +Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + +```ts +space.locks.unsubscribe('update'); +``` + +#### get() + +Get a lock by its id. + +```ts +type get = (lockId: string) => Lock | undefined +``` + +Example: + +```ts +const id = "/slide/1/element/3"; +const lock = space.locks.get(id); +``` + +#### getSelf() + +Get all locks belonging to self that have the `locked` status. + +```ts +type getSelf = () => Promise<Lock[]> +``` + +Example: + +```ts +const locks = await space.locks.getSelf(); +``` + +#### getOthers() + +Get all locks belonging to all members except self that have the `locked` status. + +```ts +type getOthers = () => Promise<Lock[]> +``` + +Example: + +```ts +const locks = await space.locks.getOthers(); +``` + +#### getAll() + +Get all locks that have the `locked` status. + +```ts +type getAll = () => Promise<Lock[]> +``` + +Example: + +```ts +const locks = await space.locks.getAll(); +``` + +### Related types + +#### Lock + +Represents a Lock. + +```ts +type Lock = { + id: string; + status: LockStatus; + member: SpaceMember; + timestamp: number; + attributes?: LockAttributes; + reason?: Types.ErrorInfo; +}; +``` + +#### LockStatus + +Represents a status of a lock. + +```ts +type LockStatus = 'pending' | 'locked' | 'unlocked'; +``` + +#### LockAttributes + +Additional attributes that can be set when acquiring a lock. + +```ts +type LockAttributes = Map<string, string>; +``` \ No newline at end of file From 053af865eedcfba4a984e46276dcd706ef5d722c Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 4 Sep 2023 13:16:58 +0100 Subject: [PATCH 044/191] Change name of test watch task --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ba0a557..d823d3cf 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "format": "prettier --write --ignore-path .gitignore src demo", "format:check": "prettier --check --ignore-path .gitignore src demo", "test": "vitest run", - "watch": "vitest watch", + "test:watch": "vitest watch", "coverage": "vitest run --coverage", "build": "npm run build:mjs && npm run build:cjs && npm run build:iife", "build:mjs": "npx tsc --project tsconfig.mjs.json && cp res/package.mjs.json dist/mjs/package.json", From 7f9acf9a8d07c39369d2f83db190481801f2d89d Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 4 Sep 2023 12:22:05 +0100 Subject: [PATCH 045/191] Emit update event as a catch all for all members namespace events This change bring "update" inline with how we describe "update" in the docs - as an events that catches all events, instead of just profile updates. To accomodate only profile update changes, an updateProfile event was added. This is based on feedback, where users found that "update" did was inconsistent with other APIs. --- src/Members.test.ts | 181 ++++++++++++++++++++++++++++++++++++++++++++ src/Members.ts | 5 ++ src/Space.test.ts | 136 +++++++-------------------------- 3 files changed, 213 insertions(+), 109 deletions(-) create mode 100644 src/Members.test.ts diff --git a/src/Members.test.ts b/src/Members.test.ts new file mode 100644 index 00000000..08203d23 --- /dev/null +++ b/src/Members.test.ts @@ -0,0 +1,181 @@ +import { it, describe, expect, vi, beforeEach, afterEach } from 'vitest'; +import { Types, Realtime } from 'ably/promises'; + +import Space from './Space.js'; + +import { createPresenceEvent, createSpaceMember, createProfileUpdate } from './utilities/test/fakes.js'; + +interface SpaceTestContext { + client: Types.RealtimePromise; + space: Space; + presence: Types.RealtimePresencePromise; + presenceMap: Map<string, Types.PresenceMessage>; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('Members', () => { + beforeEach<SpaceTestContext>((context) => { + const client = new Realtime({}); + const space = new Space('test', client); + const presence = space.channel.presence; + const presenceMap = new Map(); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(presenceMap.values()); + }); + + context.client = client; + context.space = space; + context.presence = presence; + context.presenceMap = presenceMap; + }); + + describe('subscribe', () => { + it<SpaceTestContext>('calls enter and update on enter presence events', async ({ space, presenceMap }) => { + const updateSpy = vi.fn(); + const enterSpy = vi.fn(); + space.members.subscribe('update', updateSpy); + space.members.subscribe('enter', enterSpy); + + await createPresenceEvent(space, presenceMap, 'enter'); + + const member1 = createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } }); + expect(updateSpy).toHaveBeenNthCalledWith(1, member1); + expect(enterSpy).toHaveBeenNthCalledWith(1, member1); + + await createPresenceEvent(space, presenceMap, 'enter', { + clientId: '2', + connectionId: '2', + data: createProfileUpdate({ current: { name: 'Betty' } }), + }); + + const member2 = createSpaceMember({ + clientId: '2', + connectionId: '2', + lastEvent: { name: 'enter', timestamp: 1 }, + profileData: { name: 'Betty' }, + }); + + expect(updateSpy).toHaveBeenNthCalledWith(2, member2); + expect(enterSpy).toHaveBeenNthCalledWith(2, member2); + }); + + it<SpaceTestContext>('calls updateProfile and update on update presence events', async ({ space, presenceMap }) => { + const updateSpy = vi.fn(); + const updateProfileSpy = vi.fn(); + space.members.subscribe('update', updateSpy); + space.members.subscribe('updateProfile', updateProfileSpy); + + await createPresenceEvent(space, presenceMap, 'enter'); + expect(updateSpy).toHaveBeenNthCalledWith(1, createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } })); + + await createPresenceEvent(space, presenceMap, 'update', { + data: createProfileUpdate({ current: { name: 'Betty' } }), + }); + + const memberUpdate = createSpaceMember({ profileData: { name: 'Betty' } }); + expect(updateSpy).toHaveBeenNthCalledWith(2, memberUpdate); + expect(updateProfileSpy).toHaveBeenNthCalledWith(1, memberUpdate); + }); + + it<SpaceTestContext>('updates the connected status of clients who have left', async ({ space, presenceMap }) => { + const updateSpy = vi.fn(); + const leaveSpy = vi.fn(); + space.members.subscribe('update', updateSpy); + space.members.subscribe('leave', leaveSpy); + + await createPresenceEvent(space, presenceMap, 'enter'); + expect(updateSpy).toHaveBeenNthCalledWith(1, createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } })); + + await createPresenceEvent(space, presenceMap, 'leave'); + const memberUpdate = createSpaceMember({ isConnected: false, lastEvent: { name: 'leave', timestamp: 1 } }); + expect(updateSpy).toHaveBeenNthCalledWith(2, memberUpdate); + expect(leaveSpy).toHaveBeenNthCalledWith(1, memberUpdate); + }); + + describe('leavers', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it<SpaceTestContext>('removes a member who has left after the offlineTimeout', async ({ space, presenceMap }) => { + const leaveSpy = vi.fn(); + const removeSpy = vi.fn(); + space.members.subscribe('leave', leaveSpy); + space.members.subscribe('remove', removeSpy); + + await createPresenceEvent(space, presenceMap, 'enter'); + await createPresenceEvent(space, presenceMap, 'leave'); + + const memberUpdate = createSpaceMember({ isConnected: false, lastEvent: { name: 'leave', timestamp: 1 } }); + expect(leaveSpy).toHaveBeenNthCalledWith(1, memberUpdate); + + await vi.advanceTimersByTimeAsync(130_000); + + expect(removeSpy).toHaveBeenNthCalledWith(1, memberUpdate); + }); + + it<SpaceTestContext>('does not remove a member that has rejoined', async ({ space, presenceMap }) => { + const callbackSpy = vi.fn(); + space.members.subscribe('update', callbackSpy); + + await createPresenceEvent(space, presenceMap, 'enter'); + expect(callbackSpy).toHaveBeenNthCalledWith( + 1, + createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } }), + ); + await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2', connectionId: '2' }); + expect(callbackSpy).toHaveBeenNthCalledWith( + 2, + createSpaceMember({ clientId: '2', connectionId: '2', lastEvent: { name: 'enter', timestamp: 1 } }), + ); + + await createPresenceEvent(space, presenceMap, 'leave'); + expect(callbackSpy).toHaveBeenNthCalledWith( + 3, + createSpaceMember({ lastEvent: { name: 'leave', timestamp: 1 }, isConnected: false }), + ); + + await vi.advanceTimersByTimeAsync(60_000); + await createPresenceEvent(space, presenceMap, 'enter'); + + expect(callbackSpy).toHaveBeenNthCalledWith( + 4, + createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } }), + ); + + await vi.advanceTimersByTimeAsync(130_000); // 2:10 passed, default timeout is 2 min + expect(callbackSpy).toHaveBeenCalledTimes(4); + }); + + it<SpaceTestContext>('unsubscribes when unsubscribe is called', async ({ space, presenceMap }) => { + const spy = vi.fn(); + space.members.subscribe('update', spy); + await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2' }); + space.members.unsubscribe('update', spy); + await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2' }); + + expect(spy).toHaveBeenCalledOnce(); + }); + + it<SpaceTestContext>('unsubscribes when unsubscribe is called with no arguments', async ({ + space, + presenceMap, + }) => { + const spy = vi.fn(); + space.members.subscribe('update', spy); + await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2' }); + space.members.unsubscribe(); + await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2' }); + + expect(spy).toHaveBeenCalledOnce(); + }); + }); + }); +}); diff --git a/src/Members.ts b/src/Members.ts index d0d56796..2edae1f3 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -14,6 +14,7 @@ type MemberEventsMap = { leave: SpaceMember; enter: SpaceMember; update: SpaceMember; + updateProfile: SpaceMember; remove: SpaceMember; }; @@ -34,18 +35,21 @@ class Members extends EventEmitter<MemberEventsMap> { if (action === 'leave') { this.leavers.addLeaver(member, () => this.onMemberOffline(member)); this.emit('leave', member); + this.emit('update', member); } else if (isLeaver) { this.leavers.removeLeaver(connectionId); } if (action === 'enter') { this.emit('enter', member); + this.emit('update', member); } // Emit profileData updates only if they are different then the last held update. // A locationUpdate is handled in Locations. if (message.data.profileUpdate.id && this.lastMemberUpdate[connectionId] !== message.data.profileUpdate.id) { this.lastMemberUpdate[message.connectionId] = message.data.profileUpdate.id; + this.emit('updateProfile', member); this.emit('update', member); } } @@ -122,6 +126,7 @@ class Members extends EventEmitter<MemberEventsMap> { this.leavers.removeLeaver(member.connectionId); this.emit('remove', member); + this.emit('update', member); if (member.location) { this.space.locations.emit('update', { diff --git a/src/Space.test.ts b/src/Space.test.ts index 67db9d9a..b2810cee 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -1,4 +1,4 @@ -import { it, describe, expect, vi, beforeEach, expectTypeOf, afterEach } from 'vitest'; +import { it, describe, expect, vi, beforeEach, expectTypeOf } from 'vitest'; import { Realtime, Types } from 'ably/promises'; import Space from './Space.js'; @@ -10,6 +10,7 @@ import { createPresenceMessage, createSpaceMember, createProfileUpdate, + createLocationUpdate, } from './utilities/test/fakes.js'; interface SpaceTestContext { @@ -192,7 +193,7 @@ describe('Space', () => { expect(spy).toHaveBeenCalledTimes(1); }); - it<SpaceTestContext>('adds new members', async ({ space, presenceMap }) => { + it<SpaceTestContext>('is called when members enter', async ({ space, presenceMap }) => { const callbackSpy = vi.fn(); space.subscribe('update', callbackSpy); await createPresenceEvent(space, presenceMap, 'enter'); @@ -208,49 +209,53 @@ describe('Space', () => { data: createProfileUpdate({ current: { name: 'Betty' } }), }); + const member2 = createSpaceMember({ + clientId: '2', + connectionId: '2', + lastEvent: { name: 'enter', timestamp: 1 }, + profileData: { name: 'Betty' }, + }); + expect(callbackSpy).toHaveBeenNthCalledWith(2, { - members: [ - member1, - createSpaceMember({ - clientId: '2', - connectionId: '2', - lastEvent: { name: 'enter', timestamp: 1 }, - profileData: { name: 'Betty' }, - }), - ], + members: [member1, member2], }); }); - it<SpaceTestContext>('updates the data of members', async ({ space, presenceMap }) => { + it<SpaceTestContext>('is called when members leave', async ({ space, presenceMap }) => { const callbackSpy = vi.fn(); space.subscribe('update', callbackSpy); await createPresenceEvent(space, presenceMap, 'enter'); - expect(callbackSpy).toHaveBeenNthCalledWith(1, { - members: [createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } })], - }); + let member = createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } }); - await createPresenceEvent(space, presenceMap, 'update', { - data: createProfileUpdate({ current: { name: 'Betty' } }), + expect(callbackSpy).toHaveBeenNthCalledWith(1, { + members: [member], }); + await createPresenceEvent(space, presenceMap, 'leave'); + member = createSpaceMember({ isConnected: false, lastEvent: { name: 'leave', timestamp: 1 } }); expect(callbackSpy).toHaveBeenNthCalledWith(2, { - members: [createSpaceMember({ profileData: { name: 'Betty' } })], + members: [member], }); }); - it<SpaceTestContext>('updates the connected status of clients who have left', async ({ space, presenceMap }) => { + it<SpaceTestContext>('is called when members location changes', async ({ space, presenceMap }) => { const callbackSpy = vi.fn(); space.subscribe('update', callbackSpy); await createPresenceEvent(space, presenceMap, 'enter'); + let member = createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } }); expect(callbackSpy).toHaveBeenNthCalledWith(1, { - members: [createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } })], + members: [member], }); - await createPresenceEvent(space, presenceMap, 'leave'); + await createPresenceEvent(space, presenceMap, 'update', { + data: createLocationUpdate({ current: 'newLocation' }), + }); + + member = createSpaceMember({ lastEvent: { name: 'update', timestamp: 1 }, location: 'newLocation' }); expect(callbackSpy).toHaveBeenNthCalledWith(2, { - members: [createSpaceMember({ isConnected: false, lastEvent: { name: 'leave', timestamp: 1 } })], + members: [member], }); }); @@ -263,93 +268,6 @@ describe('Space', () => { members: [createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } })], }); }); - - describe('leavers', () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it<SpaceTestContext>('removes a member who has left after the offlineTimeout', async ({ space, presenceMap }) => { - const callbackSpy = vi.fn(); - space.subscribe('update', callbackSpy); - - await createPresenceEvent(space, presenceMap, 'enter'); - expect(callbackSpy).toHaveBeenNthCalledWith(1, { - members: [createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } })], - }); - - await createPresenceEvent(space, presenceMap, 'leave'); - expect(callbackSpy).toHaveBeenNthCalledWith(2, { - members: [createSpaceMember({ isConnected: false, lastEvent: { name: 'leave', timestamp: 1 } })], - }); - - await vi.advanceTimersByTimeAsync(130_000); - - expect(callbackSpy).toHaveBeenNthCalledWith(3, { members: [] }); - expect(callbackSpy).toHaveBeenCalledTimes(3); - }); - - it<SpaceTestContext>('does not remove a member that has rejoined', async ({ space, presenceMap }) => { - const callbackSpy = vi.fn(); - space.subscribe('update', callbackSpy); - - await createPresenceEvent(space, presenceMap, 'enter'); - await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2', connectionId: '2' }); - expect(callbackSpy).toHaveBeenNthCalledWith(2, { - members: [ - createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } }), - createSpaceMember({ clientId: '2', connectionId: '2', lastEvent: { name: 'enter', timestamp: 1 } }), - ], - }); - - await createPresenceEvent(space, presenceMap, 'leave'); - expect(callbackSpy).toHaveBeenNthCalledWith(3, { - members: [ - createSpaceMember({ clientId: '2', connectionId: '2', lastEvent: { name: 'enter', timestamp: 1 } }), - createSpaceMember({ lastEvent: { name: 'leave', timestamp: 1 }, isConnected: false }), - ], - }); - - await vi.advanceTimersByTimeAsync(60_000); - await createPresenceEvent(space, presenceMap, 'enter'); - expect(callbackSpy).toHaveBeenNthCalledWith(4, { - members: [ - createSpaceMember({ clientId: '2', connectionId: '2', lastEvent: { name: 'enter', timestamp: 1 } }), - createSpaceMember({ lastEvent: { name: 'enter', timestamp: 1 } }), - ], - }); - - await vi.advanceTimersByTimeAsync(130_000); // 2:10 passed, default timeout is 2 min - expect(callbackSpy).toHaveBeenCalledTimes(4); - }); - - it<SpaceTestContext>('unsubscribes when unsubscribe is called', async ({ space, presenceMap }) => { - const spy = vi.fn(); - space.subscribe('update', spy); - await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2' }); - space.unsubscribe('update', spy); - await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2' }); - - expect(spy).toHaveBeenCalledOnce(); - }); - - it<SpaceTestContext>('unsubscribes when unsubscribe is called with no arguments', async ({ - space, - presenceMap, - }) => { - const spy = vi.fn(); - space.subscribe('update', spy); - await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2' }); - space.unsubscribe(); - await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2' }); - - expect(spy).toHaveBeenCalledOnce(); - }); - }); }); describe('locations', () => { From a16eef3cbdec58549aedaaf360c0687830928b53 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 4 Sep 2023 12:42:04 +0100 Subject: [PATCH 046/191] Update docs --- README.md | 9 +++++++-- docs/class-definitions.md | 14 ++++++++++++-- docs/usage.md | 16 +++++++++++++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index eea4ae84..39d2e82b 100644 --- a/README.md +++ b/README.md @@ -159,10 +159,15 @@ space.members.subscribe('remove', (memberRemoved) => { console.log(memberRemoved); }); -// Subscribe to member profile update events only -space.members.subscribe('update', (memberProfileUpdated) => { +// Subscribe to profile updates on members only +space.members.subscribe('updateProfile', (memberProfileUpdated) => { console.log(memberProfileUpdated); }); + +// Subscribe to all updates to members +space.members.subscribe('update', (memberUpdate) => { + console.log(memberUpdate); +}); ``` The following is an example event payload received by subscribers when member updates occur in a space: diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 86fc5c13..2c8d5543 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -207,15 +207,25 @@ Available events: The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member removed from the space. -- ##### **update** +- ##### **updateProfile** Listen to profile update events of members. ```ts - space.members.subscribe('update', (member: SpaceMember) => {}) + space.members.subscribe('updateProfile', (member: SpaceMember) => {}) ``` The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. +- ##### **update** + + Listen to `enter`, `leave`, `updateProfile` and `remove` events. + + ```ts + space.members.subscribe('update', (member: SpaceMember) => {}) + ``` + + The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member affected by the change. + #### unsubscribe() diff --git a/docs/usage.md b/docs/usage.md index 82b69a4d..110a6688 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -165,7 +165,9 @@ See [SpaceMember](/docs/class-definitions.md#spacemember) for details on propert ### Member events -Subscribe to either all the member events or specifically to `enter`, `leave`, `remove` or `update` events related to members in a space. +Subscribe to either all the member events or specifically to `enter`, `leave`, `remove` or `updateProfile` events related to members in a space. + +To listen to all events pass either no event name or `update`: ```ts space.members.subscribe((memberUpdate) => { @@ -173,6 +175,12 @@ space.members.subscribe((memberUpdate) => { }); ``` +```ts +space.members.subscribe('update', (memberUpdate) => { + console.log(memberUpdate); +}); +``` + #### enter Emitted when a member enters a space. Called with the member entering the space. @@ -203,16 +211,18 @@ space.members.subscribe('remove', (memberRemoved) => { }); ``` -#### update +#### updateProfile Emitted when a member updates their `profileData` via `space.updateProfileData()`: ```ts -space.members.subscribe('update', (memberProfileUpdated) => { +space.members.subscribe('updateProfile', (memberProfileUpdated) => { console.log(memberProfileUpdated); }); ``` + + To stop listening to member events, users can call the `space.members.unsubscribe()` method. See [Event emitters](#event-emitters) for options and usage. From d44b0b463ee5b9866a42a160f92da8ddd9001e53 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 4 Sep 2023 12:48:07 +0100 Subject: [PATCH 047/191] Update repository references --- .eslintrc.js | 2 +- README.md | 6 +++--- demo/src/utils/types.d.ts | 2 +- docs/class-definitions.md | 2 +- docs/usage.md | 6 +++--- examples/avatar-stack.ts | 2 +- examples/live-cursors.ts | 2 +- examples/locking.ts | 2 +- package-lock.json | 4 ++-- package.json | 6 +++--- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 7868213f..a540ebff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,7 +26,7 @@ module.exports = { 'no-undef': 'off', 'no-dupe-class-members': 'off', // see: - // https://github.com/ably-labs/spaces/issues/76 + // https://github.com/ably/spaces/issues/76 // https://github.com/microsoft/TypeScript/issues/16577#issuecomment-703190339 'import/extensions': [ 'error', diff --git a/README.md b/README.md index 39d2e82b..24c6524c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ <img src="https://badgen.net/badge/development-status/alpha/yellow?icon=github" alt="Development status" /> </a> <a href=""> - <img src="https://github.com/ably-labs/spaces/actions/workflows/dev-ci.yml/badge.svg?branch=main" alt="CI status" /> + <img src="https://github.com/ably/spaces/actions/workflows/dev-ci.yml/badge.svg?branch=main" alt="CI status" /> </a> <a href=""> <img src="https://badgen.net/github/license/3scale/saas-operator" alt="License" /> @@ -65,13 +65,13 @@ To start using this SDK, you will need the following: Install the Ably JavaScript SDK and the Spaces SDK: ```sh -npm install ably @ably-labs/spaces +npm install ably @ably/spaces ``` To instantiate the Spaces SDK, create an [Ably client](https://ably.com/docs/getting-started/setup) and pass it into the Spaces constructor: ```ts -import Spaces from '@ably-labs/spaces'; +import Spaces from '@ably/spaces'; import { Realtime } from 'ably'; const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); diff --git a/demo/src/utils/types.d.ts b/demo/src/utils/types.d.ts index f9b9d106..2ec5aee3 100644 --- a/demo/src/utils/types.d.ts +++ b/demo/src/utils/types.d.ts @@ -1,4 +1,4 @@ -import { type SpaceMember } from '@ably-labs/spaces'; +import { type SpaceMember } from '@ably/spaces'; interface ProfileData { name: string; diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 2c8d5543..dbc12d0c 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -6,7 +6,7 @@ Create a new instance of the Space SDK by passing an instance of the realtime, p ```ts import { Realtime } from 'ably/promise'; -import Spaces from '@ably-labs/spaces'; +import Spaces from '@ably/spaces'; const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); const spaces = new Spaces(client); diff --git a/docs/usage.md b/docs/usage.md index 110a6688..fd2c64b5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -25,7 +25,7 @@ Spaces is built on top of the [Ably JavaScript SDK](https://github.com/ably/ably You'll need to install both the ably client and the spaces client: ```sh -npm install ably @ably-labs/spaces +npm install ably @ably/spaces ``` ### CDN @@ -34,7 +34,7 @@ You can also use Spaces with a CDN like [unpkg](https://www.unpkg.com/): ```html <script src="https://cdn.ably.com/lib/ably.min-1.js"></script> -<script src="https://unpkg.com/@ably-labs/spaces@0.0.10/dist/iife/index.bundle.js"></script> +<script src="https://unpkg.com/@ably/spaces@0.0.10/dist/iife/index.bundle.js"></script> ``` ## Authentication and instantiation @@ -45,7 +45,7 @@ The Ably client instantiation accepts client options. You will need at minimum a ```ts import { Realtime } from 'ably/promise'; -import Spaces from '@ably-labs/spaces'; +import Spaces from '@ably/spaces'; const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); const spaces = new Spaces(client); diff --git a/examples/avatar-stack.ts b/examples/avatar-stack.ts index 8112ad57..d847be67 100644 --- a/examples/avatar-stack.ts +++ b/examples/avatar-stack.ts @@ -1,4 +1,4 @@ -import Spaces from '@ably-labs/spaces'; +import Spaces from '@ably/spaces'; import { Realtime } from 'ably'; import { renderAvatars, renderNotification } from './my-application'; diff --git a/examples/live-cursors.ts b/examples/live-cursors.ts index 438f2b6c..a319ff15 100644 --- a/examples/live-cursors.ts +++ b/examples/live-cursors.ts @@ -1,4 +1,4 @@ -import Spaces from '@ably-labs/spaces'; +import Spaces from '@ably/spaces'; import { Realtime } from 'ably'; import renderCursor from './my-application'; diff --git a/examples/locking.ts b/examples/locking.ts index d41c8b91..13ff339c 100644 --- a/examples/locking.ts +++ b/examples/locking.ts @@ -1,4 +1,4 @@ -import Spaces, { LockStatus } from '@ably-labs/spaces'; +import Spaces, { LockStatus } from '@ably/spaces'; import { Realtime } from 'ably'; import { enableLocationEditing, lockId } from './my-application'; diff --git a/package-lock.json b/package-lock.json index 29ceee98..904b1d89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@ably-labs/spaces", + "name": "@ably/spaces", "version": "0.0.13", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@ably-labs/spaces", + "name": "@ably/spaces", "version": "0.0.13", "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index d823d3cf..34589e41 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@ably-labs/spaces", + "name": "@ably/spaces", "version": "0.0.13", "description": "", "main": "dist/cjs/index.js", @@ -27,14 +27,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/ably-labs/spaces.git" + "url": "git+https://github.com/ably/spaces.git" }, "author": "", "license": "ISC", "bugs": { "url": "" }, - "homepage": "https://github.com/ably-labs/spaces", + "homepage": "https://github.com/ably/spaces", "publishConfig": { "access": "public" }, From a2b599417bcc76e41fae7d8e60ad2397b366c287 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 4 Sep 2023 15:29:25 +0100 Subject: [PATCH 048/191] Bump version to 0.1.0 --- CHANGELOG.md | 21 +++++++++++++-------- demo/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/Spaces.ts | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df536080..52855dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,20 @@ # CHANGELOG -The Collaborative Spaces SDK is currently under development, see [Github releases](https://github.com/ably-labs/spaces/releases) for our alpha releases. +## v0.1.0 -## 0.0.13 - 2023-08-24 +In this release, we're advancing Spaces from alpha to beta. Along with introducing this library to a wider audience, we've decided to move it to the `ably` organisation as Spaces is no longer an experiment, it's something we see as an excellent supplement to our core SDKs to help developers build collaborative environments in their apps. We are committed to grow and officially maintain it. -### Added +If you are one of our early adopters, this means you need to update your `package.json` from `@ably-labs/spaces` to `@ably/spaces`. -- Component locking -- Members namespace -- getSelf() and getOthers() methods for cursors +Visit [official documentation on Ably's website](https://ably.com/docs/spaces) to learn more about Spaces and understand what the beta status means for you. -### Changed +The following APIs are currently available: +- **Space** - a virtual area of your application in which realtime collaboration between users can take place. +- **Avatar stack** - the most common way of showing the online status of users in an application. +- **Member locations** - track where users are to see which part of your application they're interacting with. +- **Live cursors** - track the cursor positions of users in realtime. +- **Component locking** - optimistically lock stateful UI components before editing them. -- Cursor replay intervals +Your feedback will help prioritize improvements and fixes in subsequent releases. Spaces features have been validated for a set of use-cases, but breaking changes may still occur between minor releases until we reach 1.0.0. The beta is implemented based on real world situations and loads, but we'd advise to take caution when adding it to production environments. + +Please reach out to [beta@ably.com](mailto:beta@ably.com) for any questions or share feedback through [this form]( https://go.ably.com/spaces-feedback). diff --git a/demo/package.json b/demo/package.json index 80b8e080..95676f83 100644 --- a/demo/package.json +++ b/demo/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@ably-labs/react-hooks": "^3.0.0-canary.1", - "@ably-labs/spaces": "^0.0.13", + "@ably/spaces": "^0.1.0", "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", diff --git a/package-lock.json b/package-lock.json index 904b1d89..c4e888d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ably/spaces", - "version": "0.0.13", + "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ably/spaces", - "version": "0.0.13", + "version": "0.1.0", "license": "ISC", "dependencies": { "nanoid": "^4.0.2" diff --git a/package.json b/package.json index 34589e41..e236842a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/spaces", - "version": "0.0.13", + "version": "0.1.0", "description": "", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", diff --git a/src/Spaces.ts b/src/Spaces.ts index bd9a5f8a..a9b36949 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -17,7 +17,7 @@ class Spaces { client: Types.RealtimePromise; connection: Types.ConnectionPromise; - readonly version = '0.0.13'; + readonly version = '0.1.0'; constructor(client: Types.RealtimePromise) { this.client = client; From 48a992dc1ec564766c3911fd8615eb11d9c49a21 Mon Sep 17 00:00:00 2001 From: Srushtika Neelakantam <n.srushtika@gmail.com> Date: Mon, 4 Sep 2023 17:18:58 +0100 Subject: [PATCH 049/191] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24c6524c..9bc75dbd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ <p align="left"> <a href=""> - <img src="https://badgen.net/badge/development-status/alpha/yellow?icon=github" alt="Development status" /> + <img src="https://badgen.net/badge/development-status/beta/yellow?icon=github" alt="Development status" /> </a> <a href=""> <img src="https://github.com/ably/spaces/actions/workflows/dev-ci.yml/badge.svg?branch=main" alt="CI status" /> From 8b9b24dd44db2f9ff51a36eadbd12028537d9bd7 Mon Sep 17 00:00:00 2001 From: Srushtika Neelakantam <n.srushtika@gmail.com> Date: Mon, 4 Sep 2023 17:35:10 +0100 Subject: [PATCH 050/191] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9bc75dbd..c3322a94 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Ably Spaces SDK +The **Spaces SDK** contains a purpose built set of APIs that help you build collaborative environments for your apps to quickly enable remote team collaboration. Try out a [live demo](https://space.ably.dev) of a slideshow application for an example of realtime collaboration powered by the Spaces SDK. + +![Example collaboration GIF](/docs/images/collab.gif) + <p align="left"> <a href=""> <img src="https://badgen.net/badge/development-status/beta/yellow?icon=github" alt="Development status" /> @@ -16,9 +20,7 @@ _[Ably](https://ably.com) is the scalable and five nines reliable middleware, us --- -The **Spaces SDK** contains a purpose built set of APIs that help you build collaborative environments for your apps to quickly enable remote team collaboration. Try out a [live demo](https://space.ably.dev) of a slideshow application for an example of realtime collaboration powered by the Spaces SDK. -![Example collaboration GIF](/docs/images/collab.gif) ## Realtime collaboration From 6513cd0daf62458f6e5bbb1009eeee744f0bdd3b Mon Sep 17 00:00:00 2001 From: Owen Pearson <owen.pearson@ably.com> Date: Tue, 5 Sep 2023 12:07:52 +0100 Subject: [PATCH 051/191] refactor: avoid early initialisation of common errors JavaScript stack straces are created upon initialisation of the `Error` object, so when errors are initialised ahead of the time they are thrown, the stack trace will not show the context of where/why the error was thrown. --- src/Cursors.ts | 2 +- src/Errors.ts | 76 ++++++++++++++++++++++++++---------------------- src/Locations.ts | 2 +- src/Locks.ts | 14 ++++----- src/Space.ts | 2 +- src/Spaces.ts | 2 +- 6 files changed, 52 insertions(+), 46 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 7cc02a1a..45495b65 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -50,7 +50,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { const self = await this.space.members.getSelf(); if (!self) { - throw ERR_NOT_ENTERED_SPACE; + throw ERR_NOT_ENTERED_SPACE(); } const channel = this.getChannel(); diff --git a/src/Errors.ts b/src/Errors.ts index 82173038..6ba6ca52 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -21,38 +21,44 @@ export class ErrorInfo extends Error { } } -export const ERR_SPACE_NAME_MISSING = new ErrorInfo({ - message: 'must have a non-empty name for the space', - code: 101000, - statusCode: 400, -}); - -export const ERR_NOT_ENTERED_SPACE = new ErrorInfo({ - message: 'must enter a space to perform this operation', - code: 101001, - statusCode: 400, -}); - -export const ERR_LOCK_REQUEST_EXISTS = new ErrorInfo({ - message: 'lock request already exists', - code: 101002, - statusCode: 400, -}); - -export const ERR_LOCK_IS_LOCKED = new ErrorInfo({ - message: 'lock is currently locked', - code: 101003, - statusCode: 400, -}); - -export const ERR_LOCK_INVALIDATED = new ErrorInfo({ - message: 'lock was invalidated by a concurrent lock request which now holds the lock', - code: 101004, - statusCode: 400, -}); - -export const ERR_LOCK_RELEASED = new ErrorInfo({ - message: 'lock was released', - code: 101005, - statusCode: 400, -}); +export const ERR_SPACE_NAME_MISSING = () => + new ErrorInfo({ + message: 'must have a non-empty name for the space', + code: 101000, + statusCode: 400, + }); + +export const ERR_NOT_ENTERED_SPACE = () => + new ErrorInfo({ + message: 'must enter a space to perform this operation', + code: 101001, + statusCode: 400, + }); + +export const ERR_LOCK_REQUEST_EXISTS = () => + new ErrorInfo({ + message: 'lock request already exists', + code: 101002, + statusCode: 400, + }); + +export const ERR_LOCK_IS_LOCKED = () => + new ErrorInfo({ + message: 'lock is currently locked', + code: 101003, + statusCode: 400, + }); + +export const ERR_LOCK_INVALIDATED = () => + new ErrorInfo({ + message: 'lock was invalidated by a concurrent lock request which now holds the lock', + code: 101004, + statusCode: 400, + }); + +export const ERR_LOCK_RELEASED = () => + new ErrorInfo({ + message: 'lock was released', + code: 101005, + statusCode: 400, + }); diff --git a/src/Locations.ts b/src/Locations.ts index 9007ee1f..fc42d569 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -58,7 +58,7 @@ export default class Locations extends EventEmitter<LocationsEventMap> { const self = await this.space.members.getSelf(); if (!self) { - throw ERR_NOT_ENTERED_SPACE; + throw ERR_NOT_ENTERED_SPACE(); } const update: PresenceMember['data'] = { diff --git a/src/Locks.ts b/src/Locks.ts index 0b08b2d7..69d86bff 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -94,14 +94,14 @@ export default class Locks extends EventEmitter<LockEventMap> { async acquire(id: string, opts?: LockOptions): Promise<Lock> { const self = await this.space.members.getSelf(); if (!self) { - throw ERR_NOT_ENTERED_SPACE; + throw ERR_NOT_ENTERED_SPACE(); } // check there isn't an existing PENDING or LOCKED request for the current // member, since we do not support nested locks let lock = this.getLock(id, self.connectionId); if (lock && lock.status !== 'unlocked') { - throw ERR_LOCK_REQUEST_EXISTS; + throw ERR_LOCK_REQUEST_EXISTS(); } // initialise a new PENDING request @@ -127,7 +127,7 @@ export default class Locks extends EventEmitter<LockEventMap> { async release(id: string): Promise<void> { const self = await this.space.members.getSelf(); if (!self) { - throw ERR_NOT_ENTERED_SPACE; + throw ERR_NOT_ENTERED_SPACE(); } this.deleteLock(id, self.connectionId); @@ -181,7 +181,7 @@ export default class Locks extends EventEmitter<LockEventMap> { if (lock) { lock.status = 'unlocked'; - lock.reason = ERR_LOCK_RELEASED; + lock.reason = ERR_LOCK_RELEASED(); locks.delete(member.connectionId); this.emit('update', lock); } @@ -216,7 +216,7 @@ export default class Locks extends EventEmitter<LockEventMap> { if (!message.extras.locks.some((l: Lock) => l.id === lock.id)) { lock.status = 'unlocked'; - lock.reason = ERR_LOCK_RELEASED; + lock.reason = ERR_LOCK_RELEASED(); locks.delete(member.connectionId); this.emit('update', lock); } @@ -262,7 +262,7 @@ export default class Locks extends EventEmitter<LockEventMap> { ) { pendingLock.status = 'locked'; lock.status = 'unlocked'; - lock.reason = ERR_LOCK_INVALIDATED; + lock.reason = ERR_LOCK_INVALIDATED(); this.emit('update', lock); return; } @@ -270,7 +270,7 @@ export default class Locks extends EventEmitter<LockEventMap> { // the lock is LOCKED and the PENDING request did not invalidate it, so // mark the PENDING request as UNLOCKED with a reason. pendingLock.status = 'unlocked'; - pendingLock.reason = ERR_LOCK_IS_LOCKED; + pendingLock.reason = ERR_LOCK_IS_LOCKED(); } updatePresence(member: SpaceMember) { diff --git a/src/Space.ts b/src/Space.ts index 1b8d7654..5e6c9777 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -172,7 +172,7 @@ class Space extends EventEmitter<SpaceEventsMap> { const self = await this.members.getSelf(); if (!self) { - throw ERR_NOT_ENTERED_SPACE; + throw ERR_NOT_ENTERED_SPACE(); } const update = { diff --git a/src/Spaces.ts b/src/Spaces.ts index a9b36949..740e6dd1 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -33,7 +33,7 @@ class Spaces { async get(name: string, options?: Subset<SpaceOptions>): Promise<Space> { if (typeof name !== 'string' || name.length === 0) { - throw ERR_SPACE_NAME_MISSING; + throw ERR_SPACE_NAME_MISSING(); } if (this.spaces[name]) return this.spaces[name]; From a5e4babec0cc6105a97fcad1d534fe85ce53bedf Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 4 Sep 2023 17:58:43 +0100 Subject: [PATCH 052/191] Update demo to use latest version of Spaces --- demo/package-lock.json | 39 ++++++++++++++------------- demo/src/components/Avatar.tsx | 2 +- demo/src/components/AvatarInfo.tsx | 2 +- demo/src/components/SpacesContext.tsx | 2 +- demo/src/hooks/useLock.ts | 8 +++--- demo/src/hooks/useMembers.ts | 4 +-- demo/src/utils/locking.ts | 6 +++-- 7 files changed, 33 insertions(+), 30 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index 4c0392c5..4d63d346 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@ably-labs/react-hooks": "^3.0.0-canary.1", - "@ably-labs/spaces": "^0.0.13", + "@ably/spaces": "^0.1.0", "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", @@ -67,15 +67,6 @@ "react-dom": ">=16.8.0" } }, - "node_modules/@ably-labs/spaces": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@ably-labs/spaces/-/spaces-0.0.13.tgz", - "integrity": "sha512-/q4wYw3VhO07SwWo6Hrs1PP0E+9zDb/DjrTKVFrXcxZhd5onBkxQ2t2fJfAEapEnRQPNhMHIpLZXugZ9qJVD1Q==", - "dependencies": { - "ably": "^1.2.43", - "nanoid": "^4.0.2" - } - }, "node_modules/@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -84,6 +75,17 @@ "bops": "^1.0.1" } }, + "node_modules/@ably/spaces": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.0.tgz", + "integrity": "sha512-NryakCmQW1nsGtlkJzuKirC2xhfb3uaB6RheImjo9vC0cwAmwK+yYzoCBWtfUVbBKdYeOae1YE6DKC9zumnqeA==", + "dependencies": { + "nanoid": "^4.0.2" + }, + "peerDependencies": { + "ably": "^1.2.43" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -20452,15 +20454,6 @@ "ably": "^1.2.27" } }, - "@ably-labs/spaces": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@ably-labs/spaces/-/spaces-0.0.13.tgz", - "integrity": "sha512-/q4wYw3VhO07SwWo6Hrs1PP0E+9zDb/DjrTKVFrXcxZhd5onBkxQ2t2fJfAEapEnRQPNhMHIpLZXugZ9qJVD1Q==", - "requires": { - "ably": "^1.2.43", - "nanoid": "^4.0.2" - } - }, "@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -20469,6 +20462,14 @@ "bops": "^1.0.1" } }, + "@ably/spaces": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.0.tgz", + "integrity": "sha512-NryakCmQW1nsGtlkJzuKirC2xhfb3uaB6RheImjo9vC0cwAmwK+yYzoCBWtfUVbBKdYeOae1YE6DKC9zumnqeA==", + "requires": { + "nanoid": "^4.0.2" + } + }, "@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", diff --git a/demo/src/components/Avatar.tsx b/demo/src/components/Avatar.tsx index ee9afcd5..d079b157 100644 --- a/demo/src/components/Avatar.tsx +++ b/demo/src/components/Avatar.tsx @@ -1,5 +1,5 @@ import cn from 'classnames'; -import { type SpaceMember } from '@ably-labs/spaces'; +import { type SpaceMember } from '@ably/spaces'; import { AvatarInfo } from './AvatarInfo'; import { LightningSvg } from './svg'; diff --git a/demo/src/components/AvatarInfo.tsx b/demo/src/components/AvatarInfo.tsx index ae8180ef..7cc55a9e 100644 --- a/demo/src/components/AvatarInfo.tsx +++ b/demo/src/components/AvatarInfo.tsx @@ -4,7 +4,7 @@ import cn from 'classnames'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; -import { type SpaceMember } from '@ably-labs/spaces'; +import { type SpaceMember } from '@ably/spaces'; import { type ProfileData } from '../utils/types'; type Props = Omit<SpaceMember, 'profileData'> & { diff --git a/demo/src/components/SpacesContext.tsx b/demo/src/components/SpacesContext.tsx index a4d7074f..b37fc460 100644 --- a/demo/src/components/SpacesContext.tsx +++ b/demo/src/components/SpacesContext.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Spaces, { type Space } from '@ably-labs/spaces'; +import Spaces, { type Space } from '@ably/spaces'; import { Realtime } from 'ably'; import { nanoid } from 'nanoid'; diff --git a/demo/src/hooks/useLock.ts b/demo/src/hooks/useLock.ts index 356ca349..7a3d2181 100644 --- a/demo/src/hooks/useLock.ts +++ b/demo/src/hooks/useLock.ts @@ -1,6 +1,6 @@ import { useContext, useEffect, useState } from 'react'; -import { type Lock, LockStatus } from '@ably-labs/spaces'; +import { type Lock, LockStatus } from '@ably/spaces'; import { SpacesContext } from '../components'; import { buildLockId } from '../utils/locking'; @@ -18,9 +18,9 @@ export const useLock = (slide: string, id: string): { status?: string; member?: if (!space) return; const handler = (lock: Lock) => { - if (lock.request.id !== locationLockId) return; + if (lock.id !== locationLockId) return; - setStatus(lock.request.status); + setStatus(lock.status); if (isMember(lock.member)) { setMember(lock.member); @@ -39,7 +39,7 @@ export const useLock = (slide: string, id: string): { status?: string; member?: const lock = space?.locks.get(locationLockId); if (lock) { setMember(lock.member as any); - setStatus(lock.request.status); + setStatus(lock.status); } }, [status]); diff --git a/demo/src/hooks/useMembers.ts b/demo/src/hooks/useMembers.ts index f7d5f717..a218b347 100644 --- a/demo/src/hooks/useMembers.ts +++ b/demo/src/hooks/useMembers.ts @@ -1,5 +1,5 @@ import { useEffect, useState, useContext } from 'react'; -import { type SpaceMember } from '@ably-labs/spaces'; +import { type SpaceMember } from '@ably/spaces'; import { SpacesContext } from '../components'; import { type Member } from '../utils/types'; @@ -12,7 +12,7 @@ const areMembers = (arr: unknown): arr is Member[] => { return (arr as Member[]).every((m) => isMember(m)); }; -const membersToOthers = (members: Member[] = [], self: SpaceMember | undefined): Member[] => +const membersToOthers = (members: Member[] = [], self: SpaceMember | null): Member[] => members.filter((m) => m.connectionId !== self?.connectionId); export const useMembers: () => Partial<{ self?: Member; others: Member[]; members: Member[] }> = () => { diff --git a/demo/src/utils/locking.ts b/demo/src/utils/locking.ts index 35a56b25..b1fbe331 100644 --- a/demo/src/utils/locking.ts +++ b/demo/src/utils/locking.ts @@ -1,8 +1,10 @@ -import { Space } from '@ably-labs/spaces'; +import { Space } from '@ably/spaces'; import { Member } from './types'; export const releaseMyLocks = async (space: Space, self: Member) => { - await Promise.all([...space.locks.getLockRequests(self.connectionId).map((lock) => space.locks.release(lock.id))]); + await Promise.all([ + ...space.locks.getLocksForConnectionId(self.connectionId).map((lock) => space.locks.release(lock.id)), + ]); }; export const buildLockId = (slide: string | undefined, element: string | undefined) => From 928cacea4c9aebb38a82c545570bfd342ccdb938 Mon Sep 17 00:00:00 2001 From: evgeny <khokhlov.e.n@gmail.com> Date: Wed, 6 Sep 2023 09:26:35 +0100 Subject: [PATCH 053/191] fix: unlock update --- src/Locks.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Locks.ts b/src/Locks.ts index 69d86bff..486b72d1 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -130,7 +130,12 @@ export default class Locks extends EventEmitter<LockEventMap> { throw ERR_NOT_ENTERED_SPACE(); } + const lock = this.locks.get(id)?.get(self.connectionId); this.deleteLock(id, self.connectionId); + if (lock) { + lock.status = 'unlocked'; + this.emit('update', lock); + } await this.updatePresence(self); } From 4d50aba1b9e6c3368910a77c308d602da096fe8c Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 7 Sep 2023 10:14:59 +0100 Subject: [PATCH 054/191] Remove ERR_LOCK_RELEASED A lock release operation is not an error so it's confusing to have one as a reason when a lock is moved to `unlocked`. It's valid for a lock to change state and have an empty reason field for 2 main cases: - a lock has become locked without issue - a lock has becomne unlocked without issue --- src/Errors.ts | 7 ------- src/Locks.test.ts | 21 +++++++++++---------- src/Locks.ts | 45 +++++++++++++++++---------------------------- 3 files changed, 28 insertions(+), 45 deletions(-) diff --git a/src/Errors.ts b/src/Errors.ts index 6ba6ca52..5d0e6278 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -55,10 +55,3 @@ export const ERR_LOCK_INVALIDATED = () => code: 101004, statusCode: 400, }); - -export const ERR_LOCK_RELEASED = () => - new ErrorInfo({ - message: 'lock was released', - code: 101005, - statusCode: 400, - }); diff --git a/src/Locks.test.ts b/src/Locks.test.ts index 1db845be..a96d6a93 100644 --- a/src/Locks.test.ts +++ b/src/Locks.test.ts @@ -15,7 +15,7 @@ interface SpaceTestContext { vi.mock('ably/promises'); -describe('Locks (mockClient)', () => { +describe('Locks', () => { beforeEach<SpaceTestContext>((context) => { const client = new Realtime({}); const presence = client.channels.get('').presence; @@ -147,7 +147,7 @@ describe('Locks (mockClient)', () => { desc: 'assigns the lock to the other member', otherConnId: '0', otherTimestamp: now - 100, - expectedSelfStatus: 'unlocked', + expectedSelfStatus: undefined, expectedOtherStatus: 'locked', }, { @@ -155,7 +155,7 @@ describe('Locks (mockClient)', () => { desc: 'assigns the lock to the other member', otherConnId: '0', otherTimestamp: now, - expectedSelfStatus: 'unlocked', + expectedSelfStatus: undefined, expectedOtherStatus: 'locked', }, { @@ -164,7 +164,7 @@ describe('Locks (mockClient)', () => { otherConnId: '2', otherTimestamp: now, expectedSelfStatus: 'locked', - expectedOtherStatus: 'unlocked', + expectedOtherStatus: undefined, }, { name: 'when the other member has the lock with a later timestamp', @@ -172,7 +172,7 @@ describe('Locks (mockClient)', () => { otherConnId: '0', otherTimestamp: now + 100, expectedSelfStatus: 'locked', - expectedOtherStatus: 'unlocked', + expectedOtherStatus: undefined, }, ])('$name', ({ desc, otherConnId, otherTimestamp, expectedSelfStatus, expectedOtherStatus }) => { it<SpaceTestContext>(desc, async ({ client, space }) => { @@ -216,12 +216,12 @@ describe('Locks (mockClient)', () => { await space.locks.processPresenceMessage(msg); const selfMember = (await space.members.getByConnectionId(client.connection.id!))!; const selfLock = space.locks.getLock(lockID, selfMember.connectionId)!; - expect(selfLock.status).toBe(expectedSelfStatus); + expect(selfLock?.status).toBe(expectedSelfStatus); const otherMember = (await space.members.getByConnectionId(otherConnId))!; const otherLock = space.locks.getLock(lockID, otherMember.connectionId)!; - expect(otherLock.status).toBe(expectedOtherStatus); + expect(otherLock?.status).toBe(expectedOtherStatus); - if (expectedSelfStatus === 'unlocked') { + if (!expectedSelfStatus) { expect(emitSpy).toHaveBeenCalledTimes(1); expect(emitSpy).toHaveBeenNthCalledWith(1, 'update', lockEvent(selfMember, 'unlocked')); } else { @@ -318,6 +318,7 @@ describe('Locks (mockClient)', () => { const member = (await space.members.getSelf())!; const lockID = 'test'; + const timestamp = Date.now(); const msg = Realtime.PresenceMessage.fromValues({ connectionId: member.connectionId, extras: { @@ -325,7 +326,7 @@ describe('Locks (mockClient)', () => { { id: lockID, status: 'pending', - timestamp: Date.now(), + timestamp, }, ], }, @@ -339,7 +340,7 @@ describe('Locks (mockClient)', () => { expect(presenceUpdate).toHaveBeenCalledTimes(1); const updateMsg = presenceUpdate.mock.calls[0][0]; - expect(updateMsg.extras).not.toBeDefined(); + expect(updateMsg.extras).toEqual({ locks: [{ id: lockID, member, timestamp, status: 'unlocked' }] }); }); }); diff --git a/src/Locks.ts b/src/Locks.ts index 486b72d1..282221a1 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -3,13 +3,7 @@ import { Types } from 'ably'; import Space from './Space.js'; import type { Lock, SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; -import { - ERR_LOCK_IS_LOCKED, - ERR_LOCK_INVALIDATED, - ERR_LOCK_REQUEST_EXISTS, - ERR_LOCK_RELEASED, - ERR_NOT_ENTERED_SPACE, -} from './Errors.js'; +import { ERR_LOCK_IS_LOCKED, ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS, ERR_NOT_ENTERED_SPACE } from './Errors.js'; import EventEmitter, { InvalidArgumentError, inspect, @@ -126,18 +120,20 @@ export default class Locks extends EventEmitter<LockEventMap> { async release(id: string): Promise<void> { const self = await this.space.members.getSelf(); + if (!self) { throw ERR_NOT_ENTERED_SPACE(); } - const lock = this.locks.get(id)?.get(self.connectionId); - this.deleteLock(id, self.connectionId); - if (lock) { - lock.status = 'unlocked'; - this.emit('update', lock); - } + const lock = this.getLock(id, self.connectionId); + if (!lock) return; - await this.updatePresence(self); + lock.status = 'unlocked'; + lock.reason = undefined; + // Send presence update with the updated lock, but delete afterwards so when the + // message is processed an update event is fired + this.updatePresence(self); + this.deleteLock(id, self.connectionId); } subscribe<K extends EventKey<LockEventMap>>( @@ -186,9 +182,9 @@ export default class Locks extends EventEmitter<LockEventMap> { if (lock) { lock.status = 'unlocked'; - lock.reason = ERR_LOCK_RELEASED(); - locks.delete(member.connectionId); + lock.reason = undefined; this.emit('update', lock); + locks.delete(member.connectionId); } } @@ -211,19 +207,12 @@ export default class Locks extends EventEmitter<LockEventMap> { this.setLock({ ...lock, member }); }); - // handle locks which have been removed from presence extras + // handle locks which have been unlocked and longer need to be held locally for (const locks of this.locks.values()) { - const lock = locks.get(member.connectionId); - - if (!lock) { - continue; - } - - if (!message.extras.locks.some((l: Lock) => l.id === lock.id)) { - lock.status = 'unlocked'; - lock.reason = ERR_LOCK_RELEASED(); - locks.delete(member.connectionId); - this.emit('update', lock); + for (const lock of locks.values()) { + if (lock.status === 'unlocked') { + this.deleteLock(lock.id, lock.member.connectionId); + } } } } From 78c2c27df7b29b9e796ec7949da4d36088554ff8 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 7 Sep 2023 14:26:49 +0100 Subject: [PATCH 055/191] Return existing space reference after connection state check This ensure that if multiple connection state checks are in progress, each returns the same instance and does not create a new one. --- src/Spaces.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Spaces.ts b/src/Spaces.ts index 740e6dd1..87c2d318 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -36,12 +36,12 @@ class Spaces { throw ERR_SPACE_NAME_MISSING(); } - if (this.spaces[name]) return this.spaces[name]; - if (this.connection.state !== 'connected') { await this.connection.once('connected'); } + if (this.spaces[name]) return this.spaces[name]; + const space = new Space(name, this.client, options); this.spaces[name] = space; From 4054de360a097b39b9467365513e4ddfec756890 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 7 Sep 2023 14:28:59 +0100 Subject: [PATCH 056/191] Fix spaces being set multiple times on init --- demo/src/components/SpacesContext.tsx | 39 +++++++++++++++------------ demo/src/main.tsx | 11 +++----- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/demo/src/components/SpacesContext.tsx b/demo/src/components/SpacesContext.tsx index b37fc460..24abb9d5 100644 --- a/demo/src/components/SpacesContext.tsx +++ b/demo/src/components/SpacesContext.tsx @@ -1,37 +1,38 @@ import * as React from 'react'; +import { AblyProvider } from '@ably-labs/react-hooks'; + import Spaces, { type Space } from '@ably/spaces'; import { Realtime } from 'ably'; import { nanoid } from 'nanoid'; import { getSpaceNameFromUrl } from '../utils'; -const clientId = nanoid(); - -export const ably = new Realtime.Promise({ - authUrl: `/api/ably-token-request?clientId=${clientId}`, - clientId, -}); - -const spaces = new Spaces(ably); - export const SpacesContext = React.createContext<Space | undefined>(undefined); const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [space, setSpace] = React.useState<Space | undefined>(undefined); + const spaceName = getSpaceNameFromUrl(); + + const [spaces, ably] = React.useMemo(() => { + const clientId = nanoid(); + + const ably = new Realtime.Promise({ + authUrl: `/api/ably-token-request?clientId=${clientId}`, + clientId, + }); + + return [new Spaces(ably), ably]; + }, []); React.useEffect(() => { - let ignore: boolean = false; + let ignore = false; const init = async () => { - if (ignore) { - return; - } - const spaceInstance = await spaces.get(getSpaceNameFromUrl(), { offlineTimeout: 10_000, }); - if (spaceInstance && !space) { + if (spaceInstance && !space && !ignore) { setSpace(spaceInstance); } }; @@ -41,9 +42,13 @@ const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ childre return () => { ignore = true; }; - }); + }, [spaceName, spaces]); - return <SpacesContext.Provider value={space}>{children}</SpacesContext.Provider>; + return ( + <AblyProvider client={ably}> + <SpacesContext.Provider value={space}>{children}</SpacesContext.Provider>{' '} + </AblyProvider> + ); }; export { SpaceContextProvider }; diff --git a/demo/src/main.tsx b/demo/src/main.tsx index cf155827..c264e519 100644 --- a/demo/src/main.tsx +++ b/demo/src/main.tsx @@ -1,10 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { AblyProvider } from '@ably-labs/react-hooks'; import App from './App'; import './index.css'; -import { ably, SpaceContextProvider } from './components'; +import { SpaceContextProvider } from './components'; import { SlidesStateContextProvider } from './components/SlidesStateContext.tsx'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); @@ -12,11 +11,9 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) root.render( <React.StrictMode> <SpaceContextProvider> - <AblyProvider client={ably}> - <SlidesStateContextProvider> - <App /> - </SlidesStateContextProvider> - </AblyProvider> + <SlidesStateContextProvider> + <App /> + </SlidesStateContextProvider> </SpaceContextProvider> </React.StrictMode>, ); From 34c928760331922ff77458f1dd2f49ae791b4966 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 8 Sep 2023 11:16:50 +0100 Subject: [PATCH 057/191] Update internal channel names --- docs/channel-usage.md | 4 ++-- src/Cursors.ts | 7 +++++-- src/Space.test.ts | 2 +- src/Space.ts | 7 +++++-- src/Spaces.test.ts | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/channel-usage.md b/docs/channel-usage.md index 99362840..d881db82 100644 --- a/docs/channel-usage.md +++ b/docs/channel-usage.md @@ -8,13 +8,13 @@ The below channels are used by the Spaces library internally. Each `Space` (as defined by the [`Space` class](/docs/class-definitions.md#space)) creates its own [Ably Channel](https://ably.com/docs/channels). -The channel name is defined by the `name` of the Space and takes the form: `_ably_space_${name}`. The full name of a `channel` belonging to a `Space` called 'my_space' would therefore be `_ably_space_my_space`. +The channel name is defined by the `name` of the Space and takes the form: `${name}-space`. The full name of a `channel` belonging to a `Space` called 'slides' would therefore be `slides-space`. ### Cursors If any member of a `Space` subscribes to or sets cursor updates a channel is created for `cursors` updates. -The channel name is defined by the channel name of the Space with the `_cursors` suffix, taking the form: `${space.getChannelName()}_cursors`. The full name of a channel belonging to a `Space` called 'my_space' would therefore be `_ably_space_my_space_cursors`. +The channel name is defined by the name of the Space with the `::$cursors` suffix, taking the form: `${space.name}::$cursors`. The full name of a channel belonging to a `Space` called 'slides' would therefore be `slides::$cursors`. #### Events published diff --git a/src/Cursors.ts b/src/Cursors.ts index 45495b65..34512b79 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -20,11 +20,14 @@ type CursorsEventMap = { update: CursorUpdate; }; +const CURSORS_CHANNEL_TAG = '::$cursors'; + export default class Cursors extends EventEmitter<CursorsEventMap> { private readonly cursorBatching: CursorBatching; private readonly cursorDispensing: CursorDispensing; private readonly cursorHistory: CursorHistory; readonly options: CursorsOptions; + readonly channelName: string; public channel?: Types.RealtimeChannelPromise; @@ -32,6 +35,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { super(); this.options = this.space.options.cursors; + this.channelName = `${this.space.name}${CURSORS_CHANNEL_TAG}`; this.cursorHistory = new CursorHistory(); this.cursorBatching = new CursorBatching(this.options.outboundBatchInterval); @@ -62,8 +66,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { } private initializeCursorsChannel(): Types.RealtimeChannelPromise { - const spaceChannelName = this.space.channelName; - const channel = this.space.client.channels.get(`${spaceChannelName}_cursors`); + const channel = this.space.client.channels.get(this.channelName); channel.presence.subscribe(this.onPresenceUpdate.bind(this)); channel.presence.enter(); return channel; diff --git a/src/Space.test.ts b/src/Space.test.ts index b2810cee..1f89cd66 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -46,7 +46,7 @@ describe('Space', () => { const channelSpy = vi.spyOn(channels, 'get'); const space = new Space('test', client); - expect(channelSpy).toHaveBeenNthCalledWith(1, '_ably_space_test'); + expect(channelSpy).toHaveBeenNthCalledWith(1, 'test-space'); expectTypeOf(space).toMatchTypeOf<Space>(); }); }); diff --git a/src/Space.ts b/src/Space.ts index 5e6c9777..79effba9 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -19,7 +19,8 @@ import { isFunction, isObject } from './utilities/is.js'; import type { SpaceOptions, SpaceMember, ProfileData } from './types.js'; import type { Subset, PresenceMember } from './utilities/types.js'; -const SPACE_CHANNEL_PREFIX = '_ably_space_'; +// Replace by ::$spaces when that channel tag will be available +const SPACE_CHANNEL_TAG = '-space'; const SPACE_OPTIONS_DEFAULTS = { offlineTimeout: 120_000, @@ -42,13 +43,15 @@ class Space extends EventEmitter<SpaceEventsMap> { readonly members: Members; readonly channel: Types.RealtimeChannelPromise; readonly locks: Locks; + readonly name: string; constructor(name: string, readonly client: Types.RealtimePromise, options?: Subset<SpaceOptions>) { super(); this.options = this.setOptions(options); this.connectionId = this.client.connection.id; - this.channelName = `${SPACE_CHANNEL_PREFIX}${name}`; + this.name = name; + this.channelName = `${name}${SPACE_CHANNEL_TAG}`; this.channel = this.client.channels.get(this.channelName); this.onPresenceUpdate = this.onPresenceUpdate.bind(this); diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index a061fe35..47e42eb1 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -27,7 +27,7 @@ describe('Spaces', () => { await spaces.get('test'); expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith('_ably_space_test'); + expect(spy).toHaveBeenCalledWith('test-space'); }); it<SpacesTestContext>('applies the agent header to an existing SDK instance', ({ client }) => { From 4aead8f4e0fe8ea991526c0e5dafdec489f53511 Mon Sep 17 00:00:00 2001 From: Dominik <do.piatek@gmail.com> Date: Fri, 8 Sep 2023 12:20:18 +0100 Subject: [PATCH 058/191] Update comment Co-authored-by: Lewis Marshall <lewis.marshall@ably.com> --- src/Space.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Space.ts b/src/Space.ts index 79effba9..5ec98a55 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -19,7 +19,7 @@ import { isFunction, isObject } from './utilities/is.js'; import type { SpaceOptions, SpaceMember, ProfileData } from './types.js'; import type { Subset, PresenceMember } from './utilities/types.js'; -// Replace by ::$spaces when that channel tag will be available +// Replace by ::$space when that channel tag will be available const SPACE_CHANNEL_TAG = '-space'; const SPACE_OPTIONS_DEFAULTS = { From 7cd8a52b514a0204e52242259f1163fa060e3d03 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 8 Sep 2023 18:21:11 +0100 Subject: [PATCH 059/191] Bump to 0.1.1 --- CHANGELOG.md | 11 +++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/Spaces.ts | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52855dae..680e1cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## v0.1.1 + +No breaking changes were introduced in this release. + +* Update README.md by @Srushtika in https://github.com/ably/spaces/pull/159 +* Update README.md by @Srushtika in https://github.com/ably/spaces/pull/160 +* refactor: avoid early initialisation of common errors by @owenpearson in https://github.com/ably/spaces/pull/163 +* Update demo to use latest version of Spaces by @dpiatek in https://github.com/ably/spaces/pull/161 +* fix: unlock update hasn't triggered after lock release by @ttypic in https://github.com/ably/spaces/pull/164 +* [MMB-247] Channel tagging by @dpiatek in https://github.com/ably/spaces/pull/166 + ## v0.1.0 In this release, we're advancing Spaces from alpha to beta. Along with introducing this library to a wider audience, we've decided to move it to the `ably` organisation as Spaces is no longer an experiment, it's something we see as an excellent supplement to our core SDKs to help developers build collaborative environments in their apps. We are committed to grow and officially maintain it. diff --git a/package-lock.json b/package-lock.json index c4e888d7..6fe1d7e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ably/spaces", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ably/spaces", - "version": "0.1.0", + "version": "0.1.1", "license": "ISC", "dependencies": { "nanoid": "^4.0.2" diff --git a/package.json b/package.json index e236842a..809725a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/spaces", - "version": "0.1.0", + "version": "0.1.1", "description": "", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", diff --git a/src/Spaces.ts b/src/Spaces.ts index 87c2d318..be94263b 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -17,7 +17,7 @@ class Spaces { client: Types.RealtimePromise; connection: Types.ConnectionPromise; - readonly version = '0.1.0'; + readonly version = '0.1.1'; constructor(client: Types.RealtimePromise) { this.client = client; From bb90d74df55005f57c269688e41c463d03a7e1bf Mon Sep 17 00:00:00 2001 From: Lewis Marshall <lewis.marshall@ably.com> Date: Sun, 10 Sep 2023 15:12:21 +0100 Subject: [PATCH 060/191] cursors: Fix cursor batching calculation The cursor batching algorithm was designed so that the overall channel message rate remains lower than an upper bound limit, and the algorithm expects the batching to multiply the batch interval by the number of members, not the number of members minus one as we're currently doing. Signed-off-by: Lewis Marshall <lewis.marshall@ably.com> --- src/Cursors.test.ts | 4 ++-- src/Cursors.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cursors.test.ts b/src/Cursors.test.ts index e51cb54a..e1e9c3fe 100644 --- a/src/Cursors.test.ts +++ b/src/Cursors.test.ts @@ -116,7 +116,7 @@ describe('Cursors', () => { vi.spyOn(channel.presence, 'get').mockImplementation(createPresenceCount(2)); await cursors['onPresenceUpdate'](); expect(batching.shouldSend).toBeTruthy(); - expect(batching.batchTime).toEqual(100); + expect(batching.batchTime).toEqual(200); }); it<CursorsTestContext>('batchTime is updated when multiple people are present', async ({ @@ -126,7 +126,7 @@ describe('Cursors', () => { }) => { vi.spyOn(channel.presence, 'get').mockImplementation(createPresenceCount(2)); await cursors['onPresenceUpdate'](); - expect(batching.batchTime).toEqual(100); + expect(batching.batchTime).toEqual(200); }); describe('pushCursorPosition', () => { diff --git a/src/Cursors.ts b/src/Cursors.ts index 34512b79..e3318bc2 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -76,7 +76,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { const channel = this.getChannel(); const cursorsMembers = await channel.presence.get(); this.cursorBatching.setShouldSend(cursorsMembers.length > 1); - this.cursorBatching.setBatchTime((cursorsMembers.length - 1) * this.options.outboundBatchInterval); + this.cursorBatching.setBatchTime(cursorsMembers.length * this.options.outboundBatchInterval); } private isUnsubscribed() { From f6464717457bddbfaffd26737efa4d3636c06c4d Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 8 Sep 2023 11:21:08 +0100 Subject: [PATCH 061/191] Update default batch time to 25ms --- docs/class-definitions.md | 2 +- src/Cursors.test.ts | 4 ++-- src/Space.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index dbc12d0c..dc9069ff 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -86,7 +86,7 @@ type CursorsOptions = { ##### outboundBatchInterval -The interval in milliseconds at which a batch of cursor positions are published. This is multiplied by the number of members in the space minus 1. The default value is 100ms. +The interval in milliseconds at which a batch of cursor positions are published. This is multiplied by the number of members in the space minus 1. The default value is 25ms. ##### paginationLimit diff --git a/src/Cursors.test.ts b/src/Cursors.test.ts index e1e9c3fe..3f1daa60 100644 --- a/src/Cursors.test.ts +++ b/src/Cursors.test.ts @@ -116,7 +116,7 @@ describe('Cursors', () => { vi.spyOn(channel.presence, 'get').mockImplementation(createPresenceCount(2)); await cursors['onPresenceUpdate'](); expect(batching.shouldSend).toBeTruthy(); - expect(batching.batchTime).toEqual(200); + expect(batching.batchTime).toEqual(50); }); it<CursorsTestContext>('batchTime is updated when multiple people are present', async ({ @@ -126,7 +126,7 @@ describe('Cursors', () => { }) => { vi.spyOn(channel.presence, 'get').mockImplementation(createPresenceCount(2)); await cursors['onPresenceUpdate'](); - expect(batching.batchTime).toEqual(200); + expect(batching.batchTime).toEqual(50); }); describe('pushCursorPosition', () => { diff --git a/src/Space.ts b/src/Space.ts index 5ec98a55..ccf314f9 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -25,7 +25,7 @@ const SPACE_CHANNEL_TAG = '-space'; const SPACE_OPTIONS_DEFAULTS = { offlineTimeout: 120_000, cursors: { - outboundBatchInterval: 100, + outboundBatchInterval: 25, paginationLimit: 5, }, }; From bab95701b558407b849c95819ea89487b603f1d9 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Sun, 10 Sep 2023 16:27:54 +0100 Subject: [PATCH 062/191] Bump to 0.1.2 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/Spaces.ts | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 680e1cc5..b75833a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## v0.1.2 + +No breaking changes were introduced in this release. + +* cursors: Fix cursor batching calculation by @lmars in https://github.com/ably/spaces/pull/169 +* [MMB-260] Update default batch time to 25ms by @dpiatek in https://github.com/ably/spaces/pull/167 + ## v0.1.1 No breaking changes were introduced in this release. diff --git a/package-lock.json b/package-lock.json index 6fe1d7e4..30081ee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ably/spaces", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ably/spaces", - "version": "0.1.1", + "version": "0.1.2", "license": "ISC", "dependencies": { "nanoid": "^4.0.2" diff --git a/package.json b/package.json index 809725a1..18d44e97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/spaces", - "version": "0.1.1", + "version": "0.1.2", "description": "", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", diff --git a/src/Spaces.ts b/src/Spaces.ts index be94263b..86780063 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -17,7 +17,7 @@ class Spaces { client: Types.RealtimePromise; connection: Types.ConnectionPromise; - readonly version = '0.1.1'; + readonly version = '0.1.2'; constructor(client: Types.RealtimePromise) { this.client = client; From 192bc53c12a2221865d664acf40f6e69a5ef99cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:52:58 +0000 Subject: [PATCH 063/191] build(deps-dev): bump rollup from 3.28.1 to 3.29.1 Bumps [rollup](https://github.com/rollup/rollup) from 3.28.1 to 3.29.1. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v3.28.1...v3.29.1) --- updated-dependencies: - dependency-name: rollup dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30081ee6..395c7206 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4156,9 +4156,9 @@ } }, "node_modules/rollup": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", - "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.1.tgz", + "integrity": "sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -8047,9 +8047,9 @@ } }, "rollup": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", - "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.1.tgz", + "integrity": "sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==", "dev": true, "requires": { "fsevents": "~2.3.2" From 8804c0c421ad5077f4c80cde0451a61da2227fd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 08:18:29 +0000 Subject: [PATCH 064/191] build(deps-dev): bump eslint from 8.48.0 to 8.49.0 Bumps [eslint](https://github.com/eslint/eslint) from 8.48.0 to 8.49.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.48.0...v8.49.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 395c7206..6a30cf3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -505,18 +505,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -2113,16 +2113,16 @@ } }, "node_modules/eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", @@ -5408,15 +5408,15 @@ } }, "@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -6578,16 +6578,16 @@ "dev": true }, "eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", From fba24a8197fa1ea5aaf2114568eafc4eb061cc36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Sep 2023 08:29:28 +0000 Subject: [PATCH 065/191] build(deps-dev): bump eslint-plugin-jsdoc from 39.8.0 to 46.7.0 Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 39.8.0 to 46.7.0. - [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases) - [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc) - [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v39.8.0...v46.7.0) --- updated-dependencies: - dependency-name: eslint-plugin-jsdoc dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 101 +++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a30cf3f..6e71e448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@vitest/coverage-c8": "^0.33.0", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^39.8.0", + "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", "husky": "^8.0.0", "prettier": "^2.8.3", @@ -92,17 +92,17 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz", - "integrity": "sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg==", + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", + "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", "dev": true, "dependencies": { - "comment-parser": "1.3.1", - "esquery": "^1.4.0", - "jsdoc-type-pratt-parser": "~3.1.0" + "comment-parser": "1.4.0", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" }, "engines": { - "node": "^14 || ^16 || ^17 || ^18 || ^19" + "node": ">=16" } }, "node_modules/@esbuild/android-arm": { @@ -1382,6 +1382,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1777,9 +1786,9 @@ "dev": true }, "node_modules/comment-parser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", - "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", + "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", "dev": true, "engines": { "node": ">= 12.0.0" @@ -2274,21 +2283,23 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "39.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.8.0.tgz", - "integrity": "sha512-ZwGmk0jJoJD/NILeDRBKrpq/PCgddUdATjeU5JGTqTzKsOWfeaHOnaAwZjuOh7T8EB4hSoZ/9pR4+Qns2ldQVg==", + "version": "46.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.7.0.tgz", + "integrity": "sha512-VuNF+5WaiqocDDA6zvm+/D6DYo+DPFuSBOb8oSWbu0CVh+aaL3TAtpB0L0XdYYib1HHudMCHd2QeA25Tn1Pkfw==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.36.1", - "comment-parser": "1.3.1", + "@es-joy/jsdoccomment": "~0.40.1", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.0", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", - "esquery": "^1.4.0", - "semver": "^7.3.8", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", "spdx-expression-parse": "^3.0.1" }, "engines": { - "node": "^14 || ^16 || ^17 || ^18 || ^19" + "node": ">=16" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" @@ -3359,9 +3370,9 @@ } }, "node_modules/jsdoc-type-pratt-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz", - "integrity": "sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true, "engines": { "node": ">=12.0.0" @@ -5211,14 +5222,14 @@ } }, "@es-joy/jsdoccomment": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz", - "integrity": "sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg==", + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", + "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", "dev": true, "requires": { - "comment-parser": "1.3.1", - "esquery": "^1.4.0", - "jsdoc-type-pratt-parser": "~3.1.0" + "comment-parser": "1.4.0", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" } }, "@esbuild/android-arm": { @@ -6022,6 +6033,12 @@ "color-convert": "^2.0.1" } }, + "are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -6321,9 +6338,9 @@ "dev": true }, "comment-parser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", - "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", + "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", "dev": true }, "concat-map": { @@ -6734,17 +6751,19 @@ } }, "eslint-plugin-jsdoc": { - "version": "39.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.8.0.tgz", - "integrity": "sha512-ZwGmk0jJoJD/NILeDRBKrpq/PCgddUdATjeU5JGTqTzKsOWfeaHOnaAwZjuOh7T8EB4hSoZ/9pR4+Qns2ldQVg==", + "version": "46.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.7.0.tgz", + "integrity": "sha512-VuNF+5WaiqocDDA6zvm+/D6DYo+DPFuSBOb8oSWbu0CVh+aaL3TAtpB0L0XdYYib1HHudMCHd2QeA25Tn1Pkfw==", "dev": true, "requires": { - "@es-joy/jsdoccomment": "~0.36.1", - "comment-parser": "1.3.1", + "@es-joy/jsdoccomment": "~0.40.1", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.0", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", - "esquery": "^1.4.0", - "semver": "^7.3.8", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", "spdx-expression-parse": "^3.0.1" } }, @@ -7495,9 +7514,9 @@ } }, "jsdoc-type-pratt-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz", - "integrity": "sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, "json-buffer": { diff --git a/package.json b/package.json index 18d44e97..0b00dc90 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@vitest/coverage-c8": "^0.33.0", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^39.8.0", + "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", "husky": "^8.0.0", "prettier": "^2.8.3", From 2844c95f67e0ab069a668a507fa0925105cb763a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 08:12:39 +0000 Subject: [PATCH 066/191] build(deps-dev): bump vitest from 0.34.3 to 0.34.4 Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 0.34.3 to 0.34.4. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v0.34.4/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 762 +++++++++++++++++++++------------------------- 1 file changed, 355 insertions(+), 407 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e71e448..8a905d07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,9 +106,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.15.tgz", - "integrity": "sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "cpu": [ "arm" ], @@ -122,9 +122,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.15.tgz", - "integrity": "sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "cpu": [ "arm64" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.15.tgz", - "integrity": "sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "cpu": [ "x64" ], @@ -154,9 +154,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.15.tgz", - "integrity": "sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], @@ -170,9 +170,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.15.tgz", - "integrity": "sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "cpu": [ "x64" ], @@ -186,9 +186,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.15.tgz", - "integrity": "sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "cpu": [ "arm64" ], @@ -202,9 +202,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.15.tgz", - "integrity": "sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "cpu": [ "x64" ], @@ -218,9 +218,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.15.tgz", - "integrity": "sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "cpu": [ "arm" ], @@ -234,9 +234,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.15.tgz", - "integrity": "sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "cpu": [ "arm64" ], @@ -250,9 +250,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.15.tgz", - "integrity": "sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "cpu": [ "ia32" ], @@ -266,9 +266,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.15.tgz", - "integrity": "sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "cpu": [ "loong64" ], @@ -282,9 +282,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.15.tgz", - "integrity": "sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "cpu": [ "mips64el" ], @@ -298,9 +298,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.15.tgz", - "integrity": "sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "cpu": [ "ppc64" ], @@ -314,9 +314,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.15.tgz", - "integrity": "sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "cpu": [ "riscv64" ], @@ -330,9 +330,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.15.tgz", - "integrity": "sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "cpu": [ "s390x" ], @@ -346,9 +346,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.15.tgz", - "integrity": "sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ "x64" ], @@ -362,9 +362,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.15.tgz", - "integrity": "sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "cpu": [ "x64" ], @@ -378,9 +378,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.15.tgz", - "integrity": "sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "cpu": [ "x64" ], @@ -394,9 +394,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.15.tgz", - "integrity": "sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "cpu": [ "x64" ], @@ -410,9 +410,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.15.tgz", - "integrity": "sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "cpu": [ "arm64" ], @@ -426,9 +426,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.15.tgz", - "integrity": "sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "cpu": [ "ia32" ], @@ -442,9 +442,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz", - "integrity": "sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ "x64" ], @@ -1158,13 +1158,13 @@ } }, "node_modules/@vitest/expect": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.3.tgz", - "integrity": "sha512-F8MTXZUYRBVsYL1uoIft1HHWhwDbSzwAU9Zgh8S6WFC3YgVb4AnFV2GXO3P5Em8FjEYaZtTnQYoNwwBrlOMXgg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.4.tgz", + "integrity": "sha512-XlMKX8HyYUqB8dsY8Xxrc64J2Qs9pKMt2Z8vFTL4mBWXJsg4yoALHzJfDWi8h5nkO4Zua4zjqtapQ/IluVkSnA==", "dev": true, "dependencies": { - "@vitest/spy": "0.34.3", - "@vitest/utils": "0.34.3", + "@vitest/spy": "0.34.4", + "@vitest/utils": "0.34.4", "chai": "^4.3.7" }, "funding": { @@ -1172,12 +1172,12 @@ } }, "node_modules/@vitest/runner": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.3.tgz", - "integrity": "sha512-lYNq7N3vR57VMKMPLVvmJoiN4bqwzZ1euTW+XXYH5kzr3W/+xQG3b41xJn9ChJ3AhYOSoweu974S1V3qDcFESA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.4.tgz", + "integrity": "sha512-hwwdB1StERqUls8oV8YcpmTIpVeJMe4WgYuDongVzixl5hlYLT2G8afhcdADeDeqCaAmZcSgLTLtqkjPQF7x+w==", "dev": true, "dependencies": { - "@vitest/utils": "0.34.3", + "@vitest/utils": "0.34.4", "p-limit": "^4.0.0", "pathe": "^1.1.1" }, @@ -1186,9 +1186,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.3.tgz", - "integrity": "sha512-QyPaE15DQwbnIBp/yNJ8lbvXTZxS00kRly0kfFgAD5EYmCbYcA+1EEyRalc93M0gosL/xHeg3lKAClIXYpmUiQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.4.tgz", + "integrity": "sha512-GCsh4coc3YUSL/o+BPUo7lHQbzpdttTxL6f4q0jRx2qVGoYz/cyTRDJHbnwks6TILi6560bVWoBpYC10PuTLHw==", "dev": true, "dependencies": { "magic-string": "^0.30.1", @@ -1199,42 +1199,10 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", - "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "node_modules/@vitest/spy": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.3.tgz", - "integrity": "sha512-N1V0RFQ6AI7CPgzBq9kzjRdPIgThC340DGjdKdPSE8r86aUSmeliTUgkTqLSgtEwWWsGfBQ+UetZWhK0BgJmkQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.4.tgz", + "integrity": "sha512-PNU+fd7DUPgA3Ya924b1qKuQkonAW6hL7YUjkON3wmBwSTIlhOSpy04SJ0NrRsEbrXgMMj6Morh04BMf8k+w0g==", "dev": true, "dependencies": { "tinyspy": "^2.1.1" @@ -1243,19 +1211,10 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/spy/node_modules/tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@vitest/utils": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.3.tgz", - "integrity": "sha512-kiSnzLG6m/tiT0XEl4U2H8JDBjFtwVlaE8I3QfGiMFR0QvnRDfYfdP3YvTBWM/6iJDAyaPY6yVQiCTUc7ZzTHA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.4.tgz", + "integrity": "sha512-yR2+5CHhp/K4ySY0Qtd+CAL9f5Yh1aXrKfAT42bq6CtlGPh92jIDDDSg7ydlRow1CP+dys4TrOrbELOyNInHSg==", "dev": true, "dependencies": { "diff-sequences": "^29.4.3", @@ -1266,38 +1225,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", - "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "node_modules/ably": { "version": "1.2.43", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", @@ -1696,9 +1623,9 @@ } }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", + "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -2064,9 +1991,9 @@ } }, "node_modules/esbuild": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz", - "integrity": "sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, "bin": { @@ -2076,28 +2003,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.15", - "@esbuild/android-arm64": "0.17.15", - "@esbuild/android-x64": "0.17.15", - "@esbuild/darwin-arm64": "0.17.15", - "@esbuild/darwin-x64": "0.17.15", - "@esbuild/freebsd-arm64": "0.17.15", - "@esbuild/freebsd-x64": "0.17.15", - "@esbuild/linux-arm": "0.17.15", - "@esbuild/linux-arm64": "0.17.15", - "@esbuild/linux-ia32": "0.17.15", - "@esbuild/linux-loong64": "0.17.15", - "@esbuild/linux-mips64el": "0.17.15", - "@esbuild/linux-ppc64": "0.17.15", - "@esbuild/linux-riscv64": "0.17.15", - "@esbuild/linux-s390x": "0.17.15", - "@esbuild/linux-x64": "0.17.15", - "@esbuild/netbsd-x64": "0.17.15", - "@esbuild/openbsd-x64": "0.17.15", - "@esbuild/sunos-x64": "0.17.15", - "@esbuild/win32-arm64": "0.17.15", - "@esbuild/win32-ia32": "0.17.15", - "@esbuild/win32-x64": "0.17.15" + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "node_modules/escalade": { @@ -3591,9 +3518,9 @@ } }, "node_modules/mlly": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.1.tgz", - "integrity": "sha512-SCDs78Q2o09jiZiE2WziwVBEqXQ02XkGdUy45cbJf+BpYRIjArXRJ1Wbowxkb+NaM9DWvS3UC9GiO/6eqvQ/pg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", "dev": true, "dependencies": { "acorn": "^8.10.0", @@ -3921,9 +3848,9 @@ } }, "node_modules/postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.4.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", + "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", "dev": true, "funding": [ { @@ -3990,6 +3917,32 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4050,6 +4003,12 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/regexp-tree": { "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", @@ -4586,6 +4545,15 @@ "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", "dev": true }, + "node_modules/tinyspy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", + "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4851,14 +4819,14 @@ } }, "node_modules/vite": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", - "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "dev": true, "dependencies": { - "esbuild": "^0.17.5", - "postcss": "^8.4.23", - "rollup": "^3.21.0" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" @@ -4866,12 +4834,16 @@ "engines": { "node": "^14.18.0 || >=16.0.0" }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", + "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", @@ -4884,6 +4856,9 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -4899,9 +4874,9 @@ } }, "node_modules/vite-node": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.3.tgz", - "integrity": "sha512-+0TzJf1g0tYXj6tR2vEyiA42OPq68QkRZCu/ERSo2PtsDJfBpDyEfuKbRvLmZqi/CgC7SCBtyC+WjTGNMRIaig==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.4.tgz", + "integrity": "sha512-ho8HtiLc+nsmbwZMw8SlghESEE3KxJNp04F/jPUCLVvaURwt0d+r9LxEqCX5hvrrOQ0GSyxbYr5ZfRYhQ0yVKQ==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -4922,19 +4897,19 @@ } }, "node_modules/vitest": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.3.tgz", - "integrity": "sha512-7+VA5Iw4S3USYk+qwPxHl8plCMhA5rtfwMjgoQXMT7rO5ldWcdsdo3U1QD289JgglGK4WeOzgoLTsGFu6VISyQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.4.tgz", + "integrity": "sha512-SE/laOsB6995QlbSE6BtkpXDeVNLJc1u2LHRG/OpnN4RsRzM3GQm4nm3PQCK5OBtrsUqnhzLdnT7se3aeNGdlw==", "dev": true, "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.34.3", - "@vitest/runner": "0.34.3", - "@vitest/snapshot": "0.34.3", - "@vitest/spy": "0.34.3", - "@vitest/utils": "0.34.3", + "@vitest/expect": "0.34.4", + "@vitest/runner": "0.34.4", + "@vitest/snapshot": "0.34.4", + "@vitest/spy": "0.34.4", + "@vitest/utils": "0.34.4", "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -4948,8 +4923,8 @@ "strip-literal": "^1.0.1", "tinybench": "^2.5.0", "tinypool": "^0.7.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.34.3", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.4", "why-is-node-running": "^2.2.2" }, "bin": { @@ -5233,156 +5208,156 @@ } }, "@esbuild/android-arm": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.15.tgz", - "integrity": "sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.15.tgz", - "integrity": "sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.15.tgz", - "integrity": "sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.15.tgz", - "integrity": "sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.15.tgz", - "integrity": "sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.15.tgz", - "integrity": "sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.15.tgz", - "integrity": "sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.15.tgz", - "integrity": "sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.15.tgz", - "integrity": "sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.15.tgz", - "integrity": "sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.15.tgz", - "integrity": "sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.15.tgz", - "integrity": "sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.15.tgz", - "integrity": "sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.15.tgz", - "integrity": "sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.15.tgz", - "integrity": "sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.15.tgz", - "integrity": "sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.15.tgz", - "integrity": "sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.15.tgz", - "integrity": "sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.15.tgz", - "integrity": "sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.15.tgz", - "integrity": "sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.15.tgz", - "integrity": "sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz", - "integrity": "sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "dev": true, "optional": true }, @@ -5866,114 +5841,56 @@ } }, "@vitest/expect": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.3.tgz", - "integrity": "sha512-F8MTXZUYRBVsYL1uoIft1HHWhwDbSzwAU9Zgh8S6WFC3YgVb4AnFV2GXO3P5Em8FjEYaZtTnQYoNwwBrlOMXgg==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.4.tgz", + "integrity": "sha512-XlMKX8HyYUqB8dsY8Xxrc64J2Qs9pKMt2Z8vFTL4mBWXJsg4yoALHzJfDWi8h5nkO4Zua4zjqtapQ/IluVkSnA==", "dev": true, "requires": { - "@vitest/spy": "0.34.3", - "@vitest/utils": "0.34.3", + "@vitest/spy": "0.34.4", + "@vitest/utils": "0.34.4", "chai": "^4.3.7" } }, "@vitest/runner": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.3.tgz", - "integrity": "sha512-lYNq7N3vR57VMKMPLVvmJoiN4bqwzZ1euTW+XXYH5kzr3W/+xQG3b41xJn9ChJ3AhYOSoweu974S1V3qDcFESA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.4.tgz", + "integrity": "sha512-hwwdB1StERqUls8oV8YcpmTIpVeJMe4WgYuDongVzixl5hlYLT2G8afhcdADeDeqCaAmZcSgLTLtqkjPQF7x+w==", "dev": true, "requires": { - "@vitest/utils": "0.34.3", + "@vitest/utils": "0.34.4", "p-limit": "^4.0.0", "pathe": "^1.1.1" } }, "@vitest/snapshot": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.3.tgz", - "integrity": "sha512-QyPaE15DQwbnIBp/yNJ8lbvXTZxS00kRly0kfFgAD5EYmCbYcA+1EEyRalc93M0gosL/xHeg3lKAClIXYpmUiQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.4.tgz", + "integrity": "sha512-GCsh4coc3YUSL/o+BPUo7lHQbzpdttTxL6f4q0jRx2qVGoYz/cyTRDJHbnwks6TILi6560bVWoBpYC10PuTLHw==", "dev": true, "requires": { "magic-string": "^0.30.1", "pathe": "^1.1.1", "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", - "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - } } }, "@vitest/spy": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.3.tgz", - "integrity": "sha512-N1V0RFQ6AI7CPgzBq9kzjRdPIgThC340DGjdKdPSE8r86aUSmeliTUgkTqLSgtEwWWsGfBQ+UetZWhK0BgJmkQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.4.tgz", + "integrity": "sha512-PNU+fd7DUPgA3Ya924b1qKuQkonAW6hL7YUjkON3wmBwSTIlhOSpy04SJ0NrRsEbrXgMMj6Morh04BMf8k+w0g==", "dev": true, "requires": { "tinyspy": "^2.1.1" - }, - "dependencies": { - "tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", - "dev": true - } } }, "@vitest/utils": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.3.tgz", - "integrity": "sha512-kiSnzLG6m/tiT0XEl4U2H8JDBjFtwVlaE8I3QfGiMFR0QvnRDfYfdP3YvTBWM/6iJDAyaPY6yVQiCTUc7ZzTHA==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.4.tgz", + "integrity": "sha512-yR2+5CHhp/K4ySY0Qtd+CAL9f5Yh1aXrKfAT42bq6CtlGPh92jIDDDSg7ydlRow1CP+dys4TrOrbELOyNInHSg==", "dev": true, "requires": { "diff-sequences": "^29.4.3", "loupe": "^2.3.6", "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", - "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - } } }, "ably": { @@ -6266,9 +6183,9 @@ "dev": true }, "chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", + "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", "dev": true, "requires": { "assertion-error": "^1.1.0", @@ -6553,33 +6470,33 @@ } }, "esbuild": { - "version": "0.17.15", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz", - "integrity": "sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.17.15", - "@esbuild/android-arm64": "0.17.15", - "@esbuild/android-x64": "0.17.15", - "@esbuild/darwin-arm64": "0.17.15", - "@esbuild/darwin-x64": "0.17.15", - "@esbuild/freebsd-arm64": "0.17.15", - "@esbuild/freebsd-x64": "0.17.15", - "@esbuild/linux-arm": "0.17.15", - "@esbuild/linux-arm64": "0.17.15", - "@esbuild/linux-ia32": "0.17.15", - "@esbuild/linux-loong64": "0.17.15", - "@esbuild/linux-mips64el": "0.17.15", - "@esbuild/linux-ppc64": "0.17.15", - "@esbuild/linux-riscv64": "0.17.15", - "@esbuild/linux-s390x": "0.17.15", - "@esbuild/linux-x64": "0.17.15", - "@esbuild/netbsd-x64": "0.17.15", - "@esbuild/openbsd-x64": "0.17.15", - "@esbuild/sunos-x64": "0.17.15", - "@esbuild/win32-arm64": "0.17.15", - "@esbuild/win32-ia32": "0.17.15", - "@esbuild/win32-x64": "0.17.15" + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "escalade": { @@ -7686,9 +7603,9 @@ "dev": true }, "mlly": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.1.tgz", - "integrity": "sha512-SCDs78Q2o09jiZiE2WziwVBEqXQ02XkGdUy45cbJf+BpYRIjArXRJ1Wbowxkb+NaM9DWvS3UC9GiO/6eqvQ/pg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", "dev": true, "requires": { "acorn": "^8.10.0", @@ -7922,9 +7839,9 @@ } }, "postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.4.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", + "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", "dev": true, "requires": { "nanoid": "^3.3.6", @@ -7952,6 +7869,25 @@ "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", "dev": true }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -7989,6 +7925,12 @@ "safe-buffer": "^5.1.0" } }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "regexp-tree": { "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", @@ -8369,6 +8311,12 @@ "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", "dev": true }, + "tinyspy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", + "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8559,21 +8507,21 @@ } }, "vite": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", - "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "dev": true, "requires": { - "esbuild": "^0.17.5", + "esbuild": "^0.18.10", "fsevents": "~2.3.2", - "postcss": "^8.4.23", - "rollup": "^3.21.0" + "postcss": "^8.4.27", + "rollup": "^3.27.1" } }, "vite-node": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.3.tgz", - "integrity": "sha512-+0TzJf1g0tYXj6tR2vEyiA42OPq68QkRZCu/ERSo2PtsDJfBpDyEfuKbRvLmZqi/CgC7SCBtyC+WjTGNMRIaig==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.4.tgz", + "integrity": "sha512-ho8HtiLc+nsmbwZMw8SlghESEE3KxJNp04F/jPUCLVvaURwt0d+r9LxEqCX5hvrrOQ0GSyxbYr5ZfRYhQ0yVKQ==", "dev": true, "requires": { "cac": "^6.7.14", @@ -8585,19 +8533,19 @@ } }, "vitest": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.3.tgz", - "integrity": "sha512-7+VA5Iw4S3USYk+qwPxHl8plCMhA5rtfwMjgoQXMT7rO5ldWcdsdo3U1QD289JgglGK4WeOzgoLTsGFu6VISyQ==", + "version": "0.34.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.4.tgz", + "integrity": "sha512-SE/laOsB6995QlbSE6BtkpXDeVNLJc1u2LHRG/OpnN4RsRzM3GQm4nm3PQCK5OBtrsUqnhzLdnT7se3aeNGdlw==", "dev": true, "requires": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.34.3", - "@vitest/runner": "0.34.3", - "@vitest/snapshot": "0.34.3", - "@vitest/spy": "0.34.3", - "@vitest/utils": "0.34.3", + "@vitest/expect": "0.34.4", + "@vitest/runner": "0.34.4", + "@vitest/snapshot": "0.34.4", + "@vitest/spy": "0.34.4", + "@vitest/utils": "0.34.4", "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -8611,8 +8559,8 @@ "strip-literal": "^1.0.1", "tinybench": "^2.5.0", "tinypool": "^0.7.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.34.3", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.4", "why-is-node-running": "^2.2.2" }, "dependencies": { From 897f1bc28c3157f8ed14d5df7f0d489e91c373cd Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 21 Sep 2023 09:38:41 +0100 Subject: [PATCH 067/191] Fix updateProfileData not passing current profileData as an argument --- src/Space.test.ts | 22 +++++++++++++++++-- src/Space.ts | 54 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/Space.test.ts b/src/Space.test.ts index 1f89cd66..9ad84c28 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -153,10 +153,28 @@ describe('Space', () => { presenceMap, space, }) => { - presenceMap.set('1', createPresenceMessage('enter')); + presenceMap.set( + '1', + createPresenceMessage('enter', { + data: { + profileUpdate: { + id: 1, + current: { color: 'black' }, + }, + locationUpdate: { + id: null, + current: null, + previous: null, + }, + }, + }), + ); const updateSpy = vi.spyOn(presence, 'update'); await space.updateProfileData((profileData) => ({ ...profileData, name: 'Betty' })); - expect(updateSpy).toHaveBeenNthCalledWith(1, createProfileUpdate({ current: { name: 'Betty' } })); + expect(updateSpy).toHaveBeenNthCalledWith( + 1, + createProfileUpdate({ current: { name: 'Betty', color: 'black' } }), + ); }); }); diff --git a/src/Space.ts b/src/Space.ts index ccf314f9..7ec96340 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -106,6 +106,35 @@ class Space extends EventEmitter<SpaceEventsMap> { this.emit('update', { members: await this.members.getAll() }); } + private async createProfileUpdate(update: ProfileData) { + const self = await this.members.getSelf(); + + const profileUpdate = { + id: nanoid(), + current: update, + }; + + if (!self) { + return { + profileUpdate, + locationUpdate: { + id: null, + current: null, + previous: null, + }, + }; + } else { + return { + profileUpdate, + locationUpdate: { + id: null, + current: self.location ?? null, + previous: null, + }, + }; + } + } + async enter(profileData: ProfileData = null): Promise<SpaceMember[]> { return new Promise((resolve) => { const presence = this.channel.presence; @@ -137,11 +166,7 @@ class Space extends EventEmitter<SpaceEventsMap> { }); } - async updateProfileData( - profileDataOrUpdateFn: - | Record<string, unknown> - | ((update: Record<string, unknown> | null) => Record<string, unknown>), - ): Promise<void> { + async updateProfileData(profileDataOrUpdateFn: ProfileData | ((update: ProfileData) => ProfileData)): Promise<void> { const self = await this.members.getSelf(); if (!isObject(profileDataOrUpdateFn) && !isFunction(profileDataOrUpdateFn)) { @@ -150,24 +175,19 @@ class Space extends EventEmitter<SpaceEventsMap> { ); } - const update = { - profileUpdate: { - id: nanoid(), - current: isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(null) : profileDataOrUpdateFn, - }, - locationUpdate: { - id: null, - current: self?.location ?? null, - previous: null, - }, - }; - if (!self) { + const update = await this.createProfileUpdate( + isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(null) : profileDataOrUpdateFn, + ); await this.presenceEnter(update); return; } + const update = await this.createProfileUpdate( + isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(self.profileData) : profileDataOrUpdateFn, + ); const extras = this.locks.getLockExtras(self.connectionId); + return this.presenceUpdate(update, extras); } From 97404657977f992596a638f234f44d545500bcf4 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 21 Sep 2023 10:25:03 +0100 Subject: [PATCH 068/191] Fix .leave resetting profileData for users --- src/Space.test.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++--- src/Space.ts | 38 ++++++++++++-------- 2 files changed, 108 insertions(+), 19 deletions(-) diff --git a/src/Space.test.ts b/src/Space.test.ts index 9ad84c28..eb0c62dd 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -192,13 +192,94 @@ describe('Space', () => { }); describe('leave', () => { - it<SpaceTestContext>('leaves a space successfully', async ({ presence, presenceMap, space }) => { - presenceMap.set('1', createPresenceMessage('enter')); + it<SpaceTestContext>('leaves a space successfully and does not nullify presence data', async ({ + presence, + presenceMap, + space, + }) => { + presenceMap.set( + '1', + createPresenceMessage('enter', { + data: { + profileUpdate: { id: 1, current: { name: 'Betty' } }, + locationUpdate: { id: null, current: { slide: 1 }, previous: null }, + }, + }), + ); - await space.enter(); const spy = vi.spyOn(presence, 'leave'); await space.leave(); - expect(spy).toHaveBeenCalledOnce(); + expect(spy).toHaveBeenNthCalledWith(1, { + profileUpdate: { + id: null, + current: { name: 'Betty' }, + }, + locationUpdate: { + id: null, + current: { slide: 1 }, + previous: null, + }, + }); + }); + + it<SpaceTestContext>('leaves a space successfully and nullifies presence data', async ({ + presence, + presenceMap, + space, + }) => { + presenceMap.set( + '1', + createPresenceMessage('enter', { + data: { + profileUpdate: { id: 1, current: { name: 'Betty' } }, + locationUpdate: { id: null, current: { slide: 1 }, previous: null }, + }, + }), + ); + + const spy = vi.spyOn(presence, 'leave'); + await space.leave(null); + expect(spy).toHaveBeenNthCalledWith(1, { + profileUpdate: { + id: 'NanoidID', + current: null, + }, + locationUpdate: { + id: null, + current: { slide: 1 }, + previous: null, + }, + }); + }); + + it<SpaceTestContext>('leaves a space successfully and updates presence data', async ({ + presence, + presenceMap, + space, + }) => { + presenceMap.set( + '1', + createPresenceMessage('enter', { + data: { + profileUpdate: { id: 1, current: { name: 'Betty' } }, + locationUpdate: { id: null, current: { slide: 1 }, previous: null }, + }, + }), + ); + + const spy = vi.spyOn(presence, 'leave'); + await space.leave({ colorWhenLeft: 'blue' }); + expect(spy).toHaveBeenNthCalledWith(1, { + profileUpdate: { + id: 'NanoidID', + current: { colorWhenLeft: 'blue' }, + }, + locationUpdate: { + id: null, + current: { slide: 1 }, + previous: null, + }, + }); }); }); diff --git a/src/Space.ts b/src/Space.ts index 7ec96340..7d84399e 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -106,9 +106,7 @@ class Space extends EventEmitter<SpaceEventsMap> { this.emit('update', { members: await this.members.getAll() }); } - private async createProfileUpdate(update: ProfileData) { - const self = await this.members.getSelf(); - + private createProfileUpdate(self, update: ProfileData) { const profileUpdate = { id: nanoid(), current: update, @@ -177,6 +175,7 @@ class Space extends EventEmitter<SpaceEventsMap> { if (!self) { const update = await this.createProfileUpdate( + self, isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(null) : profileDataOrUpdateFn, ); await this.presenceEnter(update); @@ -184,6 +183,7 @@ class Space extends EventEmitter<SpaceEventsMap> { } const update = await this.createProfileUpdate( + self, isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(self.profileData) : profileDataOrUpdateFn, ); const extras = this.locks.getLockExtras(self.connectionId); @@ -198,19 +198,27 @@ class Space extends EventEmitter<SpaceEventsMap> { throw ERR_NOT_ENTERED_SPACE(); } - const update = { - profileUpdate: { - id: profileData ? nanoid() : null, - current: profileData ?? null, - }, - locationUpdate: { - id: null, - current: self?.location ?? null, - previous: null, - }, - }; + let update; - await this.presenceLeave(update); + // Use arguments so it's possible to deliberately nullify profileData on leave + if (arguments.length > 0) { + update = this.createProfileUpdate(self, profileData); + } else { + update = { + profileUpdate: { + id: null, + current: self.profileData, + }, + locationUpdate: { + id: null, + current: self.location, + previous: null, + }, + }; + } + + const extras = this.locks.getLockExtras(self.connectionId); + await this.presenceLeave(update, extras); } async getState(): Promise<{ members: SpaceMember[] }> { From 0cbad07cececb8fc43667e9b0e9da7e1bc2cac89 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 21 Sep 2023 12:25:23 +0100 Subject: [PATCH 069/191] Add SpaceUpdate class This class is responsible for creating objects that will be passed to presence. It takes care of creating an update id and encapsulates the structure of the update. --- src/Locations.ts | 24 ++--------- src/Locks.ts | 23 +++-------- src/Space.ts | 89 +++++++++-------------------------------- src/SpaceUpdate.test.ts | 71 ++++++++++++++++++++++++++++++++ src/SpaceUpdate.ts | 73 +++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 106 deletions(-) create mode 100644 src/SpaceUpdate.test.ts create mode 100644 src/SpaceUpdate.ts diff --git a/src/Locations.ts b/src/Locations.ts index fc42d569..f2b57106 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -1,5 +1,3 @@ -import { nanoid } from 'nanoid'; - import EventEmitter, { InvalidArgumentError, inspect, @@ -11,6 +9,7 @@ import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; +import SpaceUpdate from './SpaceUpdate.js'; type LocationsEventMap = { update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown }; @@ -19,10 +18,7 @@ type LocationsEventMap = { export default class Locations extends EventEmitter<LocationsEventMap> { private lastLocationUpdate: Record<string, PresenceMember['data']['locationUpdate']['id']> = {}; - constructor( - private space: Space, - private presenceUpdate: (update: PresenceMember['data'], extras?: PresenceMember['extras']) => Promise<void>, - ) { + constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { super(); } @@ -61,20 +57,8 @@ export default class Locations extends EventEmitter<LocationsEventMap> { throw ERR_NOT_ENTERED_SPACE(); } - const update: PresenceMember['data'] = { - profileUpdate: { - id: null, - current: self.profileData, - }, - locationUpdate: { - id: nanoid(), - previous: self.location, - current: location, - }, - }; - - const extras = this.space.locks.getLockExtras(self.connectionId); - await this.presenceUpdate(update, extras); + const update = new SpaceUpdate({ self, extras: this.space.locks.getLockExtras(self.connectionId) }); + await this.presenceUpdate(update.updateLocation(location)); } subscribe<K extends EventKey<LocationsEventMap>>( diff --git a/src/Locks.ts b/src/Locks.ts index 282221a1..c0b94b6c 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -11,6 +11,8 @@ import EventEmitter, { type EventListener, } from './utilities/EventEmitter.js'; +import SpaceUpdate from './SpaceUpdate.js'; + export class LockAttributes extends Map<string, string> { toJSON() { return Object.fromEntries(this); @@ -34,10 +36,7 @@ export default class Locks extends EventEmitter<LockEventMap> { // have requested. private locks: Map<string, Map<string, Lock>>; - constructor( - private space: Space, - private presenceUpdate: (update: PresenceMember['data'], extras?: any) => Promise<void>, - ) { + constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { super(); this.locks = new Map(); } @@ -267,19 +266,9 @@ export default class Locks extends EventEmitter<LockEventMap> { pendingLock.reason = ERR_LOCK_IS_LOCKED(); } - updatePresence(member: SpaceMember) { - const update: PresenceMember['data'] = { - profileUpdate: { - id: null, - current: member.profileData, - }, - locationUpdate: { - id: null, - current: member?.location ?? null, - previous: null, - }, - }; - return this.presenceUpdate(update, this.getLockExtras(member.connectionId)); + updatePresence(self: SpaceMember) { + const update = new SpaceUpdate({ self, extras: this.getLockExtras(self.connectionId) }); + return this.presenceUpdate(update.noop()); } getLock(id: string, connectionId: string): Lock | undefined { diff --git a/src/Space.ts b/src/Space.ts index 7d84399e..50dec7d2 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -1,5 +1,4 @@ import Ably, { Types } from 'ably'; -import { nanoid } from 'nanoid'; import EventEmitter, { InvalidArgumentError, @@ -11,9 +10,9 @@ import Locations from './Locations.js'; import Cursors from './Cursors.js'; import Members from './Members.js'; import Locks from './Locks.js'; +import SpaceUpdate, { type SpacePresenceData } from './SpaceUpdate.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; - import { isFunction, isObject } from './utilities/is.js'; import type { SpaceOptions, SpaceMember, ProfileData } from './types.js'; @@ -63,21 +62,21 @@ class Space extends EventEmitter<SpaceEventsMap> { this.locks = new Locks(this, this.presenceUpdate); } - private presenceUpdate = (data: PresenceMember['data'], extras?: PresenceMember['extras']) => { + private presenceUpdate = ({ data, extras }: SpacePresenceData) => { if (!extras) { return this.channel.presence.update(data); } return this.channel.presence.update(Ably.Realtime.PresenceMessage.fromValues({ data, extras })); }; - private presenceEnter = (data: PresenceMember['data'], extras?: PresenceMember['extras']) => { + private presenceEnter = ({ data, extras }: SpacePresenceData) => { if (!extras) { return this.channel.presence.enter(data); } return this.channel.presence.enter(Ably.Realtime.PresenceMessage.fromValues({ data, extras })); }; - private presenceLeave = (data: PresenceMember['data'], extras?: PresenceMember['extras']) => { + private presenceLeave = ({ data, extras }: SpacePresenceData) => { if (!extras) { return this.channel.presence.leave(data); } @@ -106,33 +105,6 @@ class Space extends EventEmitter<SpaceEventsMap> { this.emit('update', { members: await this.members.getAll() }); } - private createProfileUpdate(self, update: ProfileData) { - const profileUpdate = { - id: nanoid(), - current: update, - }; - - if (!self) { - return { - profileUpdate, - locationUpdate: { - id: null, - current: null, - previous: null, - }, - }; - } else { - return { - profileUpdate, - locationUpdate: { - id: null, - current: self.location ?? null, - previous: null, - }, - }; - } - } - async enter(profileData: ProfileData = null): Promise<SpaceMember[]> { return new Promise((resolve) => { const presence = this.channel.presence; @@ -150,17 +122,8 @@ class Space extends EventEmitter<SpaceEventsMap> { resolve(members); }); - this.presenceEnter({ - profileUpdate: { - id: nanoid(), - current: profileData, - }, - locationUpdate: { - id: null, - current: null, - previous: null, - }, - }); + const update = new SpaceUpdate({ self: null, extras: null }); + this.presenceEnter(update.updateProfileData(profileData)); }); } @@ -173,22 +136,20 @@ class Space extends EventEmitter<SpaceEventsMap> { ); } + let update = new SpaceUpdate({ self, extras: self ? this.locks.getLockExtras(self.connectionId) : null }); + if (!self) { - const update = await this.createProfileUpdate( - self, + const data = update.updateProfileData( isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(null) : profileDataOrUpdateFn, ); - await this.presenceEnter(update); + await this.presenceEnter(data); return; + } else { + const data = update.updateProfileData( + isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(self.profileData) : profileDataOrUpdateFn, + ); + return this.presenceUpdate(data); } - - const update = await this.createProfileUpdate( - self, - isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(self.profileData) : profileDataOrUpdateFn, - ); - const extras = this.locks.getLockExtras(self.connectionId); - - return this.presenceUpdate(update, extras); } async leave(profileData: ProfileData = null) { @@ -198,27 +159,17 @@ class Space extends EventEmitter<SpaceEventsMap> { throw ERR_NOT_ENTERED_SPACE(); } - let update; + const update = new SpaceUpdate({ self, extras: this.locks.getLockExtras(self.connectionId) }); + let data; // Use arguments so it's possible to deliberately nullify profileData on leave if (arguments.length > 0) { - update = this.createProfileUpdate(self, profileData); + data = update.updateProfileData(profileData); } else { - update = { - profileUpdate: { - id: null, - current: self.profileData, - }, - locationUpdate: { - id: null, - current: self.location, - previous: null, - }, - }; + data = update.noop(); } - const extras = this.locks.getLockExtras(self.connectionId); - await this.presenceLeave(update, extras); + await this.presenceLeave(data); } async getState(): Promise<{ members: SpaceMember[] }> { diff --git a/src/SpaceUpdate.test.ts b/src/SpaceUpdate.test.ts new file mode 100644 index 00000000..aa49f926 --- /dev/null +++ b/src/SpaceUpdate.test.ts @@ -0,0 +1,71 @@ +import { describe, it, vi, expect } from 'vitest'; + +import SpaceUpdate from './SpaceUpdate.js'; +import { createSpaceMember } from './utilities/test/fakes.js'; + +vi.mock('nanoid'); + +describe('SpaceUpdate', () => { + it('creates a profileUpdate', () => { + const self = createSpaceMember({ profileData: { name: 'Berry' } }); + const update = new SpaceUpdate({ self }); + expect(update.updateProfileData({ name: 'Barry' })).toEqual({ + data: { + locationUpdate: { + current: null, + id: null, + previous: null, + }, + profileUpdate: { + current: { + name: 'Barry', + }, + id: 'NanoidID', + }, + }, + extras: undefined, + }); + }); + + it('creates a locationUpdate', () => { + const self = createSpaceMember({ location: { slide: 3 }, profileData: { name: 'Berry' } }); + const update = new SpaceUpdate({ self }); + expect(update.updateLocation({ slide: 1 }, null)).toEqual({ + data: { + locationUpdate: { + current: { slide: 1 }, + id: 'NanoidID', + previous: { slide: 3 }, + }, + profileUpdate: { + current: { + name: 'Berry', + }, + id: null, + }, + }, + extras: undefined, + }); + }); + + it('creates an object with no updates to current data', () => { + const self = createSpaceMember({ location: { slide: 3 }, profileData: { name: 'Berry' } }); + const update = new SpaceUpdate({ self }); + expect(update.noop()).toEqual({ + data: { + locationUpdate: { + current: { slide: 3 }, + id: null, + previous: null, + }, + profileUpdate: { + current: { + name: 'Berry', + }, + id: null, + }, + }, + extras: undefined, + }); + }); +}); diff --git a/src/SpaceUpdate.ts b/src/SpaceUpdate.ts new file mode 100644 index 00000000..11cebaec --- /dev/null +++ b/src/SpaceUpdate.ts @@ -0,0 +1,73 @@ +import { nanoid } from 'nanoid'; +import { Types } from 'ably'; + +import type { SpaceMember, ProfileData } from './types.js'; +import type { PresenceMember } from './utilities/types.js'; + +export interface SpacePresenceData { + data: PresenceMember['data']; + extras: PresenceMember['extras']; +} + +class SpaceUpdate { + private self: SpaceMember | null; + private extras: Types.PresenceMessage['extras']; + + constructor({ self, extras }: { self: SpaceMember | null; extras?: Types.PresenceMessage['extras'] }) { + this.self = self; + this.extras = extras; + } + + private profileUpdate(id: string | null, current: ProfileData) { + return { id, current }; + } + + private profileNoChange() { + return this.profileUpdate(null, this.self ? this.self.profileData : null); + } + + private locationUpdate(id: string | null, current: SpaceMember['location'], previous: SpaceMember['location']) { + return { id, current, previous }; + } + + private locationNoChange() { + const location = this.self ? this.self.location : null; + return this.locationUpdate(null, location, null); + } + + updateProfileData(current: ProfileData): SpacePresenceData { + return { + data: { + profileUpdate: this.profileUpdate(nanoid(), current), + locationUpdate: this.locationNoChange(), + }, + extras: this.extras, + }; + } + + updateLocation(location: SpaceMember['location'], previousLocation?: SpaceMember['location']): SpacePresenceData { + return { + data: { + profileUpdate: this.profileNoChange(), + locationUpdate: this.locationUpdate( + nanoid(), + location, + previousLocation ? previousLocation : this.self?.location, + ), + }, + extras: this.extras, + }; + } + + noop(): SpacePresenceData { + return { + data: { + profileUpdate: this.profileNoChange(), + locationUpdate: this.locationNoChange(), + }, + extras: this.extras, + }; + } +} + +export default SpaceUpdate; From 521931c4a5bb83a925c0f050d05019f013dfa2f4 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 22 Sep 2023 16:49:15 +0100 Subject: [PATCH 070/191] Update CDN versions to latest --- README.md | 2 +- docs/usage.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c3322a94..fac5df4e 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): ```html <script src="https://cdn.ably.com/lib/ably.min-1.js"></script> -<script src="https://cdn.ably.com/spaces/0.0.13/iife/index.bundle.js"></script> +<script src="https://cdn.ably.com/spaces/0.1.2/iife/index.bundle.js"></script> ``` After this, instantiate the SDK in the same way as in the NPM option above. diff --git a/docs/usage.md b/docs/usage.md index fd2c64b5..16a6fe66 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -34,7 +34,7 @@ You can also use Spaces with a CDN like [unpkg](https://www.unpkg.com/): ```html <script src="https://cdn.ably.com/lib/ably.min-1.js"></script> -<script src="https://unpkg.com/@ably/spaces@0.0.10/dist/iife/index.bundle.js"></script> +<script src="https://unpkg.com/@ably/spaces@0.1.2/dist/iife/index.bundle.js"></script> ``` ## Authentication and instantiation From b882b4a44191de91d5e424ede7ccd9d7f3a3af63 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 22 Sep 2023 16:47:20 +0100 Subject: [PATCH 071/191] Remove unused export --- src/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index f51aacdb..8c7cf938 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,11 @@ import Spaces from './Spaces.js'; export type Space = Awaited<ReturnType<Spaces['get']>>; -// Can be changed to * when we update to TS5 - +// Note: this should be the only non-type export from this file, +// otherwise the rollup IIFE build will export an object instead of a constructor export default Spaces; +// Can be changed to * when we update to TS5 export type { CursorsOptions, CursorPosition, @@ -17,5 +18,3 @@ export type { Lock, LockStatus, } from './types.js'; - -export { LockAttributes } from './Locks.js'; From 916404e0a9b3f8328e91fa46e8bdcb76cc5f4020 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 21 Sep 2023 13:36:34 +0100 Subject: [PATCH 072/191] Silence Vite warnings about node packages loaded by postcss See https://github.com/vitejs/vite/issues/9200 --- demo/noop.js | 2 ++ demo/vite.config.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) create mode 100644 demo/noop.js diff --git a/demo/noop.js b/demo/noop.js new file mode 100644 index 00000000..834ecd2d --- /dev/null +++ b/demo/noop.js @@ -0,0 +1,2 @@ +// See https://github.com/vitejs/vite/issues/9200 +module.exports = {}; diff --git a/demo/vite.config.ts b/demo/vite.config.ts index 33d7e5b2..e5ac0646 100644 --- a/demo/vite.config.ts +++ b/demo/vite.config.ts @@ -3,6 +3,16 @@ import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ + resolve: { + alias: { + // Silence Vite warnings about node packages loaded by postcss + // See https://github.com/vitejs/vite/issues/9200 + path: './noop.js', + fs: './noop.js', + url: './noop.js', + 'source-map-js': './noop.js', + }, + }, plugins: [react()], server: { port: 8080, From fab49b8c3a4260425e440a6a024a745a48423b69 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 25 Sep 2023 11:45:09 -0300 Subject: [PATCH 073/191] Make Locks.prototype.getLocksForConnectionId private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Whilst working on MMB-156 I asked whether this was meant to be part of the public API and was told no, and that its usage should be replaced by Locks.prototype.getSelf. I have no idea whether the `delay(60)` preceding the call to releaseMyLocks in the useElementSelect hook is still necessary, since I don’t know what the "spaces API" that it refers to is. --- demo/src/hooks/useElementSelect.ts | 2 +- demo/src/utils/locking.ts | 7 ++----- src/Locks.ts | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/demo/src/hooks/useElementSelect.ts b/demo/src/hooks/useElementSelect.ts index be873b66..d29adbf3 100644 --- a/demo/src/hooks/useElementSelect.ts +++ b/demo/src/hooks/useElementSelect.ts @@ -42,7 +42,7 @@ export const useClickOutside = (ref: MutableRefObject<HTMLElement | null>, self? await space.locations.set({ slide: self.location?.slide, element: undefined }); // TODO delete this workaround when spaces API is ready await delay(60); - await releaseMyLocks(space, self); + await releaseMyLocks(space); } }; diff --git a/demo/src/utils/locking.ts b/demo/src/utils/locking.ts index b1fbe331..5615b9dc 100644 --- a/demo/src/utils/locking.ts +++ b/demo/src/utils/locking.ts @@ -1,10 +1,7 @@ import { Space } from '@ably/spaces'; -import { Member } from './types'; -export const releaseMyLocks = async (space: Space, self: Member) => { - await Promise.all([ - ...space.locks.getLocksForConnectionId(self.connectionId).map((lock) => space.locks.release(lock.id)), - ]); +export const releaseMyLocks = async (space: Space) => { + await Promise.all([...space.locks.getSelf().map((lock) => space.locks.release(lock.id))]); }; export const buildLockId = (slide: string | undefined, element: string | undefined) => diff --git a/src/Locks.ts b/src/Locks.ts index c0b94b6c..8e6eba2f 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -292,7 +292,7 @@ export default class Locks extends EventEmitter<LockEventMap> { return locks.delete(connectionId); } - getLocksForConnectionId(connectionId: string): Lock[] { + private getLocksForConnectionId(connectionId: string): Lock[] { const requests: Lock[] = []; for (const locks of this.locks.values()) { From 89808207ac496f0eaf4cd10c760fa8a2360f18a8 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Tue, 26 Sep 2023 16:53:47 +0100 Subject: [PATCH 074/191] Revert "Remove unused export" This reverts commit b882b4a44191de91d5e424ede7ccd9d7f3a3af63. --- src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8c7cf938..f51aacdb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,11 +2,10 @@ import Spaces from './Spaces.js'; export type Space = Awaited<ReturnType<Spaces['get']>>; -// Note: this should be the only non-type export from this file, -// otherwise the rollup IIFE build will export an object instead of a constructor +// Can be changed to * when we update to TS5 + export default Spaces; -// Can be changed to * when we update to TS5 export type { CursorsOptions, CursorPosition, @@ -18,3 +17,5 @@ export type { Lock, LockStatus, } from './types.js'; + +export { LockAttributes } from './Locks.js'; From b5161f486c5a5646c7e90fa4734b958672b59abf Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 21 Sep 2023 12:33:24 +0100 Subject: [PATCH 075/191] Fix cursors set causing error if moving before entering --- demo/src/components/CurrentSlide.tsx | 2 +- demo/src/hooks/useTrackCursor.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/demo/src/components/CurrentSlide.tsx b/demo/src/components/CurrentSlide.tsx index 97486756..ecda595b 100644 --- a/demo/src/components/CurrentSlide.tsx +++ b/demo/src/components/CurrentSlide.tsx @@ -12,7 +12,7 @@ export const CurrentSlide = ({ slides }: Props) => { const { self } = useMembers(); const slide = parseInt(self?.location?.slide || '', 10) || 0; - useTrackCursor(containerRef); + useTrackCursor(containerRef, self?.connectionId); return ( <section diff --git a/demo/src/hooks/useTrackCursor.ts b/demo/src/hooks/useTrackCursor.ts index 40ecab67..01b277ba 100644 --- a/demo/src/hooks/useTrackCursor.ts +++ b/demo/src/hooks/useTrackCursor.ts @@ -5,7 +5,7 @@ export const CURSOR_MOVE = 'move'; export const CURSOR_ENTER = 'enter'; export const CURSOR_LEAVE = 'leave'; -export const useTrackCursor = (containerRef: RefObject<HTMLDivElement>) => { +export const useTrackCursor = (containerRef: RefObject<HTMLDivElement>, selfConnectionId?: string) => { const space = useContext(SpacesContext); useEffect(() => { @@ -15,6 +15,7 @@ export const useTrackCursor = (containerRef: RefObject<HTMLDivElement>) => { const cursorHandlers = { enter: (event: MouseEvent) => { + if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); space.cursors.set({ position: { x: event.clientX - left, y: event.clientY - top }, @@ -22,6 +23,7 @@ export const useTrackCursor = (containerRef: RefObject<HTMLDivElement>) => { }); }, move: (event: MouseEvent) => { + if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); space.cursors.set({ position: { x: event.clientX - left, y: event.clientY - top }, @@ -29,6 +31,7 @@ export const useTrackCursor = (containerRef: RefObject<HTMLDivElement>) => { }); }, leave: (event: MouseEvent) => { + if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); space.cursors.set({ position: { x: event.clientX - left, y: event.clientY - top }, @@ -47,5 +50,5 @@ export const useTrackCursor = (containerRef: RefObject<HTMLDivElement>) => { cursorContainer.removeEventListener('mousemove', cursorHandlers.move); cursorContainer.removeEventListener('mouseleave', cursorHandlers.leave); }; - }, [space, containerRef]); + }, [space, containerRef, selfConnectionId]); }; From 99d8200a34fa0edc626b6d1328f412a29eaf3ddd Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 21 Sep 2023 13:13:01 +0100 Subject: [PATCH 076/191] Refactor cursors for demo This simplifies the cursors component and fixes the issue where cursor icons would stay even after a member has left. --- demo/package-lock.json | 102 --------------------- demo/package.json | 6 -- demo/src/components/Cursors.tsx | 154 +++++++++++++------------------- demo/src/hooks/useMembers.ts | 2 +- 4 files changed, 61 insertions(+), 203 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index 4d63d346..c627159a 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -13,9 +13,6 @@ "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", - "lodash.assign": "^4.2.0", - "lodash.find": "^4.6.0", - "lodash.omit": "^4.5.0", "nanoid": "^4.0.2", "random-words": "^2.0.0", "react": "^18.2.0", @@ -24,9 +21,6 @@ "sanitize-html": "^2.11.0" }, "devDependencies": { - "@types/lodash.assign": "^4.2.7", - "@types/lodash.find": "^4.6.7", - "@types/lodash.omit": "^4.5.7", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-helmet": "^6.1.6", @@ -1055,39 +1049,6 @@ "@types/node": "*" } }, - "node_modules/@types/lodash": { - "version": "4.14.196", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz", - "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==", - "dev": true - }, - "node_modules/@types/lodash.assign": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@types/lodash.assign/-/lodash.assign-4.2.7.tgz", - "integrity": "sha512-FABtilcXqbXBj9jnSqgTbmr0Wy3lhDgMkGtNfcE+wipuKi2/fv7wB9rCeJfLi1H/qt1YxVAENtQkdKisYIwFKg==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/lodash.find": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/@types/lodash.find/-/lodash.find-4.6.7.tgz", - "integrity": "sha512-ESwK5k3nf9ufS+M2niH06/gjrREihHobyWQLs5c5+O93uar3N9s0rNx8bBfG9zLYVaPBJEsEIk06hm5ccstF5w==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/lodash.omit": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.7.tgz", - "integrity": "sha512-6q6cNg0tQ6oTWjSM+BcYMBhan54P/gLqBldG4AuXd3nKr0oeVekWNS4VrNEu3BhCSDXtGapi7zjhnna0s03KpA==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/node": { "version": "20.4.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.4.tgz", @@ -2862,27 +2823,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" - }, - "node_modules/lodash.find": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -21082,39 +21028,6 @@ "@types/node": "*" } }, - "@types/lodash": { - "version": "4.14.196", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz", - "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==", - "dev": true - }, - "@types/lodash.assign": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@types/lodash.assign/-/lodash.assign-4.2.7.tgz", - "integrity": "sha512-FABtilcXqbXBj9jnSqgTbmr0Wy3lhDgMkGtNfcE+wipuKi2/fv7wB9rCeJfLi1H/qt1YxVAENtQkdKisYIwFKg==", - "dev": true, - "requires": { - "@types/lodash": "*" - } - }, - "@types/lodash.find": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/@types/lodash.find/-/lodash.find-4.6.7.tgz", - "integrity": "sha512-ESwK5k3nf9ufS+M2niH06/gjrREihHobyWQLs5c5+O93uar3N9s0rNx8bBfG9zLYVaPBJEsEIk06hm5ccstF5w==", - "dev": true, - "requires": { - "@types/lodash": "*" - } - }, - "@types/lodash.omit": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.7.tgz", - "integrity": "sha512-6q6cNg0tQ6oTWjSM+BcYMBhan54P/gLqBldG4AuXd3nKr0oeVekWNS4VrNEu3BhCSDXtGapi7zjhnna0s03KpA==", - "dev": true, - "requires": { - "@types/lodash": "*" - } - }, "@types/node": { "version": "20.4.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.4.tgz", @@ -22374,27 +22287,12 @@ "p-locate": "^5.0.0" } }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" - }, - "lodash.find": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==" - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" - }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", diff --git a/demo/package.json b/demo/package.json index 95676f83..55a23629 100644 --- a/demo/package.json +++ b/demo/package.json @@ -17,9 +17,6 @@ "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", - "lodash.assign": "^4.2.0", - "lodash.find": "^4.6.0", - "lodash.omit": "^4.5.0", "nanoid": "^4.0.2", "random-words": "^2.0.0", "react": "^18.2.0", @@ -28,9 +25,6 @@ "sanitize-html": "^2.11.0" }, "devDependencies": { - "@types/lodash.assign": "^4.2.7", - "@types/lodash.find": "^4.6.7", - "@types/lodash.omit": "^4.5.7", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-helmet": "^6.1.6", diff --git a/demo/src/components/Cursors.tsx b/demo/src/components/Cursors.tsx index b7248948..0f726c4a 100644 --- a/demo/src/components/Cursors.tsx +++ b/demo/src/components/Cursors.tsx @@ -1,127 +1,93 @@ -import { useContext, useEffect, useReducer } from 'react'; -import assign from 'lodash.assign'; +import { useContext, useEffect, useState } from 'react'; +import type { CursorUpdate as _CursorUpdate } from '@ably/spaces'; + import cn from 'classnames'; -import find from 'lodash.find'; -import omit from 'lodash.omit'; import { CursorSvg, SpacesContext } from '.'; import { useMembers, CURSOR_ENTER, CURSOR_LEAVE, CURSOR_MOVE } from '../hooks'; -import { type Member } from '../utils/types'; - -type ActionType = 'move' | 'enter' | 'leave'; - -interface Action { - type: ActionType; - data: { - connectionId: string; - members?: Member[]; - position?: { - x: number; - y: number; - }; - }; -} -interface State { - [connectionId: string]: Member & Action['data']; -} - -const reducer = (state: State, action: Action): State => { - const { type, data } = action; - const { connectionId, members, position } = data; - switch (type) { - case CURSOR_ENTER: - return { - ...state, - [connectionId]: { - ...assign(find(members, { connectionId }), { connectionId, position }), - }, - }; - case CURSOR_LEAVE: - return { - ...omit(state, connectionId), - }; - case CURSOR_MOVE: - return { - ...state, - [connectionId]: { - ...assign(find(members, { connectionId }), { connectionId, position }), - }, - }; - default: - throw new Error('Unknown dispatch type'); - } -}; +type state = typeof CURSOR_ENTER | typeof CURSOR_LEAVE | typeof CURSOR_MOVE; +type CursorUpdate = Omit<_CursorUpdate, 'data'> & { data: { state: state } }; export const Cursors = () => { const space = useContext(SpacesContext); - const { self, members } = useMembers(); - const [activeCursors, dispatch] = useReducer(reducer, {}); + const { self, others } = useMembers(); + const [cursors, setCursors] = useState<{ + [connectionId: string]: { position: CursorUpdate['position']; state: CursorUpdate['data']['state'] }; + }>({}); useEffect(() => { - if (!space || !members) return; + if (!space || !others) return; space.cursors.subscribe('update', (cursorUpdate) => { - const { connectionId } = cursorUpdate; - const member = find<Member>(members, { connectionId }); + const { connectionId, position, data } = cursorUpdate as CursorUpdate; - if ( - connectionId !== self?.connectionId && - member?.location?.slide === self?.location?.slide && - cursorUpdate.data - ) { - dispatch({ - type: cursorUpdate.data.state as ActionType, - data: { - connectionId, - members, - position: cursorUpdate.position, - }, - }); - } else { - dispatch({ - type: CURSOR_LEAVE, - data: { - connectionId, - members, - }, - }); - } + if (cursorUpdate.connectionId === self?.connectionId) return; + + setCursors((currentCursors) => ({ + ...currentCursors, + [connectionId]: { position, state: data.state }, + })); }); return () => { space.cursors.unsubscribe('update'); }; - }, [space, members]); + }, [space, others, self?.connectionId]); + + useEffect(() => { + const handler = async (member: { connectionId: string }) => { + setCursors((currentCursors) => ({ + ...currentCursors, + [member.connectionId]: { position: { x: 0, y: 0 }, state: CURSOR_LEAVE }, + })); + }; + + space?.members.subscribe('leave', handler); + + return () => { + space?.members.unsubscribe('leave', handler); + }; + }, [space]); + + const activeCursors = others + .filter( + (member) => + member.isConnected && cursors[member.connectionId] && cursors[member.connectionId].state !== CURSOR_LEAVE, + ) + .map((member) => ({ + connectionId: member.connectionId, + profileData: member.profileData, + position: cursors[member.connectionId].position, + })); return ( <div className="h-full w-full z-10 pointer-events-none top-0 left-0 absolute"> - {Object.keys(activeCursors).map((cursor) => { - const { connectionId, profileData } = activeCursors[cursor]; + {activeCursors.map((cursor) => { + const { connectionId, profileData } = cursor; + return ( <div key={connectionId} style={{ position: 'absolute', - top: `${activeCursors[cursor].position?.y}px`, - left: `${activeCursors[cursor].position?.x}px`, + top: `${cursor.position.y}px`, + left: `${cursor.position.x}px`, }} > <CursorSvg - startColor={profileData?.color?.gradientStart?.hex} - endColor={profileData?.color?.gradientEnd?.hex} + startColor={profileData.color.gradientStart.hex} + endColor={profileData.color.gradientEnd.hex} id={connectionId} /> - {profileData?.name ? ( - <p - className={cn( - profileData.color.gradientStart.tw, - profileData.color.gradientEnd.tw, - 'py-2 px-4 bg-gradient-to-b rounded-full absolute text-white text-base truncate transition-all max-w-[120px]', - )} - > - {profileData.name.split(' ')[0]} - </p> - ) : null} + <p + className={cn( + profileData.color.gradientStart.tw, + profileData.color.gradientEnd.tw, + 'py-2 px-4 bg-gradient-to-b rounded-full absolute text-white text-base truncate transition-all max-w-[120px]', + )} + > + {profileData.name.split(' ')[0]} + </p> </div> ); })} diff --git a/demo/src/hooks/useMembers.ts b/demo/src/hooks/useMembers.ts index a218b347..0a437728 100644 --- a/demo/src/hooks/useMembers.ts +++ b/demo/src/hooks/useMembers.ts @@ -15,7 +15,7 @@ const areMembers = (arr: unknown): arr is Member[] => { const membersToOthers = (members: Member[] = [], self: SpaceMember | null): Member[] => members.filter((m) => m.connectionId !== self?.connectionId); -export const useMembers: () => Partial<{ self?: Member; others: Member[]; members: Member[] }> = () => { +export const useMembers: () => { self?: Member; others: Member[]; members: Member[] } = () => { const space = useContext(SpacesContext); const [members, setMembers] = useState<Member[]>([]); const [others, setOthers] = useState<Member[]>([]); From fde3915da4dfbeb8c3b972b354a843ff82a9c979 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Wed, 27 Sep 2023 10:55:16 +0100 Subject: [PATCH 077/191] Bump to 0.1.3 --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/Spaces.ts | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b75833a5..b5257ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # CHANGELOG +## v0.1.3 + +Breaking changes in this release: +* `space.locks.getLocksForConnectionId` is now private by @lawrence-forooghian https://github.com/ably/spaces/pull/188 + +Other notable changes: +* Refactor space updates by @dpiatek in https://github.com/ably/spaces/pull/180 + * Fixes `space.updateProfileData` not passing the existing `ProfileData` if a function was passed in as an argument + * Fixes `space.leave` resetting `ProfileData` when no arguments are passed in + +**Full Changelog**: https://github.com/ably/spaces/compare/0.1.2...0.1.3 + ## v0.1.2 No breaking changes were introduced in this release. diff --git a/package-lock.json b/package-lock.json index 8a905d07..9850a22d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ably/spaces", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ably/spaces", - "version": "0.1.2", + "version": "0.1.3", "license": "ISC", "dependencies": { "nanoid": "^4.0.2" diff --git a/package.json b/package.json index 0b00dc90..b06ddb0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/spaces", - "version": "0.1.2", + "version": "0.1.3", "description": "", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", diff --git a/src/Spaces.ts b/src/Spaces.ts index 86780063..5f252819 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -17,7 +17,7 @@ class Spaces { client: Types.RealtimePromise; connection: Types.ConnectionPromise; - readonly version = '0.1.2'; + readonly version = '0.1.3'; constructor(client: Types.RealtimePromise) { this.client = client; From 64f673652ef58dc9e9113a15905b554a97d6cbaf Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 28 Sep 2023 12:20:40 +0100 Subject: [PATCH 078/191] Update mode to use 0.1.3 --- demo/package-lock.json | 14 +++++++------- demo/package.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index c627159a..200e857e 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@ably-labs/react-hooks": "^3.0.0-canary.1", - "@ably/spaces": "^0.1.0", + "@ably/spaces": "0.1.3", "ably": "^1.2.43", "classnames": "^2.3.2", "dayjs": "^1.11.9", @@ -70,9 +70,9 @@ } }, "node_modules/@ably/spaces": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.0.tgz", - "integrity": "sha512-NryakCmQW1nsGtlkJzuKirC2xhfb3uaB6RheImjo9vC0cwAmwK+yYzoCBWtfUVbBKdYeOae1YE6DKC9zumnqeA==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.3.tgz", + "integrity": "sha512-8egeUAvl+L6wrBuIIVx17BdQH+bl9rP0VRqEuYpx8+lRt8pQS+t/gyfjE3XQbg5OIaILbeiDbhgkNNDD7OOlRw==", "dependencies": { "nanoid": "^4.0.2" }, @@ -20409,9 +20409,9 @@ } }, "@ably/spaces": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.0.tgz", - "integrity": "sha512-NryakCmQW1nsGtlkJzuKirC2xhfb3uaB6RheImjo9vC0cwAmwK+yYzoCBWtfUVbBKdYeOae1YE6DKC9zumnqeA==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.3.tgz", + "integrity": "sha512-8egeUAvl+L6wrBuIIVx17BdQH+bl9rP0VRqEuYpx8+lRt8pQS+t/gyfjE3XQbg5OIaILbeiDbhgkNNDD7OOlRw==", "requires": { "nanoid": "^4.0.2" } diff --git a/demo/package.json b/demo/package.json index 55a23629..c18baf69 100644 --- a/demo/package.json +++ b/demo/package.json @@ -13,8 +13,8 @@ }, "dependencies": { "@ably-labs/react-hooks": "^3.0.0-canary.1", - "@ably/spaces": "^0.1.0", - "ably": "^1.2.43", + "@ably/spaces": "0.1.3", + "ably": "^1.2.44", "classnames": "^2.3.2", "dayjs": "^1.11.9", "nanoid": "^4.0.2", From 5f724b181dd320934251322968efdd1cabc0cf93 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Thu, 28 Sep 2023 12:21:07 +0100 Subject: [PATCH 079/191] Fix locks release method --- demo/src/utils/locking.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/demo/src/utils/locking.ts b/demo/src/utils/locking.ts index 5615b9dc..975f81c0 100644 --- a/demo/src/utils/locking.ts +++ b/demo/src/utils/locking.ts @@ -1,7 +1,11 @@ import { Space } from '@ably/spaces'; export const releaseMyLocks = async (space: Space) => { - await Promise.all([...space.locks.getSelf().map((lock) => space.locks.release(lock.id))]); + const locks = await space.locks.getSelf(); + + if (locks.length > 0) { + locks.forEach((lock) => space.locks.release(lock.id)); + } }; export const buildLockId = (slide: string | undefined, element: string | undefined) => From c290a250cba71ac1e85ec1da9f227469dca06bd2 Mon Sep 17 00:00:00 2001 From: Nikita Kakuev <n.kakuev@blexr.com> Date: Wed, 20 Sep 2023 23:25:25 +0100 Subject: [PATCH 080/191] Add edit name modal --- demo/src/App.tsx | 11 +++++-- demo/src/components/Avatar.tsx | 2 +- demo/src/components/Modal.tsx | 55 ++++++++++++++++++++++++++++++++++ demo/src/components/index.ts | 1 + demo/src/hooks/useMembers.ts | 1 - 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 demo/src/components/Modal.tsx diff --git a/demo/src/App.tsx b/demo/src/App.tsx index d824ad99..9c79e7b8 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,6 +1,6 @@ -import { useContext, useEffect } from 'react'; +import { useContext, useEffect, useState } from 'react'; -import { Header, SlideMenu, SpacesContext, CurrentSlide, AblySvg, slides } from './components'; +import { Header, SlideMenu, SpacesContext, CurrentSlide, AblySvg, slides, Modal } from './components'; import { getRandomName, getRandomColor } from './utils'; import { useMembers } from './hooks'; import { PreviewProvider } from './components/PreviewContext.tsx'; @@ -8,6 +8,7 @@ import { PreviewProvider } from './components/PreviewContext.tsx'; const App = () => { const space = useContext(SpacesContext); const { self, others } = useMembers(); + const [isModalVisible, setModalIsVisible] = useState(false); useEffect(() => { if (!space || self?.profileData.name) return; @@ -16,6 +17,7 @@ const App = () => { const name = getRandomName(); await space.enter({ name, color: getRandomColor() }); await space.locations.set({ slide: `${0}`, element: null }); + setModalIsVisible(true); }; enter(); @@ -47,6 +49,11 @@ const App = () => { <AblySvg className="ml-2" /> </a> </div> + <Modal + self={self} + isVisible={isModalVisible} + setIsVisible={setModalIsVisible} + /> </div> ); }; diff --git a/demo/src/components/Avatar.tsx b/demo/src/components/Avatar.tsx index d079b157..cb4a0252 100644 --- a/demo/src/components/Avatar.tsx +++ b/demo/src/components/Avatar.tsx @@ -20,7 +20,7 @@ export const Avatar = ({ }: AvatarProps) => { const initials = profileData.name .split(' ') - .map((n: string) => n[0]) + .map((n: string) => n[0].toUpperCase()) .join(''); return ( diff --git a/demo/src/components/Modal.tsx b/demo/src/components/Modal.tsx new file mode 100644 index 00000000..f6a56c3f --- /dev/null +++ b/demo/src/components/Modal.tsx @@ -0,0 +1,55 @@ +import { FormEvent, useContext, useRef } from 'react'; +import cn from 'classnames'; + +import { SpacesContext } from '.'; +import { Member } from '../utils/types'; + +interface Props { + self?: Member; + isVisible?: boolean; + setIsVisible?: (isVisible: boolean) => void; +} + +export const Modal = ({ isVisible = false, setIsVisible, self }: Props) => { + const space = useContext(SpacesContext); + const inputRef = useRef<HTMLInputElement>(null); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + if (!space || !setIsVisible) return; + + space.updateProfileData((profileData) => ({ ...profileData, name: inputRef.current?.value })); + setIsVisible(false); + }; + + return ( + <div + className={cn( + 'backdrop-blur-md bg-black/30 fixed top-0 left-0 w-full h-full flex items-center justify-center transition-all duration-300', + { + 'opacity-0 pointer-events-none': !isVisible, + 'opacity-100': isVisible, + }, + )} + > + <form + onSubmit={handleSubmit} + className="bg-white p-8 shadow-lg rounded-[20px]" + > + <h3 className="font-semibold text-xl text-center mb-8">Enter your name</h3> + <input + ref={inputRef} + className="border border-gray-300 rounded-md p-2 w-full" + defaultValue={self?.profileData?.name} + /> + <button + type="submit" + className="bg-ably-black text-white rounded-md p-2 w-full mt-4" + > + Set name + </button> + </form> + </div> + ); +}; diff --git a/demo/src/components/index.ts b/demo/src/components/index.ts index e4957dd2..28b16ad4 100644 --- a/demo/src/components/index.ts +++ b/demo/src/components/index.ts @@ -5,6 +5,7 @@ export * from './CurrentSlide'; export * from './Cursors'; export * from './Header'; export * from './Image'; +export * from './Modal'; export * from './Paragraph'; export * from './SlideMenu'; export * from './SlidePreview'; diff --git a/demo/src/hooks/useMembers.ts b/demo/src/hooks/useMembers.ts index 0a437728..47905d24 100644 --- a/demo/src/hooks/useMembers.ts +++ b/demo/src/hooks/useMembers.ts @@ -50,7 +50,6 @@ export const useMembers: () => { self?: Member; others: Member[]; members: Membe setMembers(initMembers); setOthers(membersToOthers(initMembers, initSelf)); } - space.subscribe('update', handler); }; From 50ce0a859270d331c1f9d2855f1dcbf666645ff8 Mon Sep 17 00:00:00 2001 From: Nikita Kakuev <n.kakuev@blexr.com> Date: Thu, 21 Sep 2023 22:54:18 +0100 Subject: [PATCH 081/191] Add team url param --- demo/src/components/Header.tsx | 9 ++++++--- demo/src/components/SpacesContext.tsx | 6 +++--- demo/src/hooks/useTextComponentLock.ts | 4 ++-- demo/src/utils/url.ts | 10 +++++----- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/demo/src/components/Header.tsx b/demo/src/components/Header.tsx index 2e50f4ce..052836d0 100644 --- a/demo/src/components/Header.tsx +++ b/demo/src/components/Header.tsx @@ -2,6 +2,7 @@ import { Avatar } from './Avatar'; import { AvatarStack } from './AvatarStack'; import { ExternalLinkSvg, InfoSvg } from './svg'; import { type Member } from '../utils/types'; +import { getParamNameFromUrl } from '../utils'; interface Props { self?: Member; @@ -9,6 +10,8 @@ interface Props { } export const Header = ({ self, others }: Props) => { + const teamName = getParamNameFromUrl('team', 1, '') + return ( <header id="main-header" @@ -16,7 +19,7 @@ export const Header = ({ self, others }: Props) => { > <div className="mx-auto justify-between grid grid-rows-2 grid-cols-2 max-w-screen-2xl md:px-8 lg:px-16 md:flex md:items-center"> <section className="py-4 shrink-0 mr-4"> - <p className="font-semibold pl-8 md:text-2xl">Team Argo</p> + <p className="font-semibold pl-8 md:text-2xl capitalize">Team {teamName ?? 'Argo'}</p> <p className="leading-5 pl-8">Pitch deck</p> </section> @@ -47,7 +50,7 @@ export const Header = ({ self, others }: Props) => { <a href="https://github.com/ably-labs/spaces" target="_blank" - className="flex items-center px-5 py-[14px] justify-start shrink-0 block" + className="flex items-center px-5 py-[14px] justify-start shrink-0" rel="noreferrer" > <p className="font-medium text-base">Space API</p> @@ -56,7 +59,7 @@ export const Header = ({ self, others }: Props) => { <a href="https://docs.google.com/forms/d/e/1FAIpQLSer2ujLNw0rlrf2FvfIhLxyiWuuvTwYkDDqHmv30F8Cs00YWg/viewform" - className="block w-[100px] text-white bg-ably-black rounded-md py-[11px] px-5 leading-[1.125] md:text-xs lg:text-base lg:ml-[24px] shrink-0 hidden lg:block" + className="w-[100px] text-white bg-ably-black rounded-md py-[11px] px-5 leading-[1.125] md:text-xs lg:text-base lg:ml-[24px] shrink-0 hidden lg:block" > Sign Up </a> diff --git a/demo/src/components/SpacesContext.tsx b/demo/src/components/SpacesContext.tsx index 24abb9d5..834eba7c 100644 --- a/demo/src/components/SpacesContext.tsx +++ b/demo/src/components/SpacesContext.tsx @@ -5,13 +5,13 @@ import Spaces, { type Space } from '@ably/spaces'; import { Realtime } from 'ably'; import { nanoid } from 'nanoid'; -import { getSpaceNameFromUrl } from '../utils'; +import { getParamNameFromUrl } from '../utils'; export const SpacesContext = React.createContext<Space | undefined>(undefined); const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [space, setSpace] = React.useState<Space | undefined>(undefined); - const spaceName = getSpaceNameFromUrl(); + const spaceName = getParamNameFromUrl(); const [spaces, ably] = React.useMemo(() => { const clientId = nanoid(); @@ -28,7 +28,7 @@ const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ childre let ignore = false; const init = async () => { - const spaceInstance = await spaces.get(getSpaceNameFromUrl(), { + const spaceInstance = await spaces.get(getParamNameFromUrl(), { offlineTimeout: 10_000, }); diff --git a/demo/src/hooks/useTextComponentLock.ts b/demo/src/hooks/useTextComponentLock.ts index b95a0e34..57aebc75 100644 --- a/demo/src/hooks/useTextComponentLock.ts +++ b/demo/src/hooks/useTextComponentLock.ts @@ -1,6 +1,6 @@ import { MutableRefObject, useCallback } from 'react'; import { useChannel } from '@ably-labs/react-hooks'; -import { findActiveMember, getSpaceNameFromUrl } from '../utils'; +import { findActiveMember, getParamNameFromUrl } from '../utils'; import { buildLockId } from '../utils/locking.ts'; import { usePreview } from '../components/PreviewContext.tsx'; import { useMembers } from './useMembers.ts'; @@ -17,7 +17,7 @@ interface UseTextComponentLockArgs { } export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: UseTextComponentLockArgs) => { - const spaceName = getSpaceNameFromUrl(); + const spaceName = getParamNameFromUrl(); const { members, self } = useMembers(); const activeMember = findActiveMember(id, slide, members); const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId); diff --git a/demo/src/utils/url.ts b/demo/src/utils/url.ts index 4e8858b4..d88b401f 100644 --- a/demo/src/utils/url.ts +++ b/demo/src/utils/url.ts @@ -1,17 +1,17 @@ import { generate } from 'random-words'; -const getSpaceNameFromUrl = () => { +const getParamNameFromUrl = (paramName: string = 'space', length = 3, join = '-' ) => { const url = new URL(window.location.href); - const spaceNameInParams = url.searchParams.get('space'); + const spaceNameInParams = url.searchParams.get(paramName); if (spaceNameInParams) { return spaceNameInParams; } else { - const generatedName = generate({ exactly: 3, join: '-' }); - url.searchParams.set('space', generatedName); + const generatedName = generate({ exactly: length, join }); + url.searchParams.set(paramName, generatedName); window.history.replaceState({}, '', `?${url.searchParams.toString()}`); return generatedName; } }; -export { getSpaceNameFromUrl }; +export { getParamNameFromUrl }; From 422a2dd7b4d2cdb0b36d4f3a93258ac9dc966e6f Mon Sep 17 00:00:00 2001 From: Nikita Kakuev <n.kakuev@blexr.com> Date: Thu, 21 Sep 2023 22:56:03 +0100 Subject: [PATCH 082/191] Small refactor --- demo/src/utils/url.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/src/utils/url.ts b/demo/src/utils/url.ts index d88b401f..751d336c 100644 --- a/demo/src/utils/url.ts +++ b/demo/src/utils/url.ts @@ -1,13 +1,13 @@ import { generate } from 'random-words'; -const getParamNameFromUrl = (paramName: string = 'space', length = 3, join = '-' ) => { +const getParamNameFromUrl = (paramName = 'space', exactly = 3, join = '-' ) => { const url = new URL(window.location.href); const spaceNameInParams = url.searchParams.get(paramName); if (spaceNameInParams) { return spaceNameInParams; } else { - const generatedName = generate({ exactly: length, join }); + const generatedName = generate({ exactly, join }); url.searchParams.set(paramName, generatedName); window.history.replaceState({}, '', `?${url.searchParams.toString()}`); return generatedName; From eeda0d7c1a2fbc27067e43e7ee7e64d44ff255a1 Mon Sep 17 00:00:00 2001 From: Nikita Kakuev <n.kakuev@blexr.com> Date: Mon, 25 Sep 2023 21:55:20 +0100 Subject: [PATCH 083/191] Fix linting issues --- demo/src/components/Header.tsx | 4 ++-- demo/src/utils/url.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/src/components/Header.tsx b/demo/src/components/Header.tsx index 052836d0..7a6ff140 100644 --- a/demo/src/components/Header.tsx +++ b/demo/src/components/Header.tsx @@ -10,8 +10,8 @@ interface Props { } export const Header = ({ self, others }: Props) => { - const teamName = getParamNameFromUrl('team', 1, '') - + const teamName = getParamNameFromUrl('team', 1, ''); + return ( <header id="main-header" diff --git a/demo/src/utils/url.ts b/demo/src/utils/url.ts index 751d336c..968950b8 100644 --- a/demo/src/utils/url.ts +++ b/demo/src/utils/url.ts @@ -1,6 +1,6 @@ import { generate } from 'random-words'; -const getParamNameFromUrl = (paramName = 'space', exactly = 3, join = '-' ) => { +const getParamNameFromUrl = (paramName = 'space', exactly = 3, join = '-') => { const url = new URL(window.location.href); const spaceNameInParams = url.searchParams.get(paramName); From c6f2fc55619c10bc1ab6ec56a7a816835e4522cd Mon Sep 17 00:00:00 2001 From: Nikita Kakuev <n.kakuev@blexr.com> Date: Mon, 25 Sep 2023 22:49:26 +0100 Subject: [PATCH 084/191] Add dash separator --- demo/src/components/Header.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/src/components/Header.tsx b/demo/src/components/Header.tsx index 7a6ff140..58537649 100644 --- a/demo/src/components/Header.tsx +++ b/demo/src/components/Header.tsx @@ -11,6 +11,7 @@ interface Props { export const Header = ({ self, others }: Props) => { const teamName = getParamNameFromUrl('team', 1, ''); + const formattedTeamName = teamName?.replace(/-/g, ' '); return ( <header @@ -19,7 +20,7 @@ export const Header = ({ self, others }: Props) => { > <div className="mx-auto justify-between grid grid-rows-2 grid-cols-2 max-w-screen-2xl md:px-8 lg:px-16 md:flex md:items-center"> <section className="py-4 shrink-0 mr-4"> - <p className="font-semibold pl-8 md:text-2xl capitalize">Team {teamName ?? 'Argo'}</p> + <p className="font-semibold pl-8 md:text-2xl capitalize">Team {formattedTeamName ?? 'Argo'}</p> <p className="leading-5 pl-8">Pitch deck</p> </section> From 074ff0b1495abeb54c50fee25d8bc0fc87ee0a45 Mon Sep 17 00:00:00 2001 From: Nikita Kakuev <n.kakuev@blexr.com> Date: Tue, 26 Sep 2023 22:16:28 +0100 Subject: [PATCH 085/191] Feedback --- demo/src/components/Header.tsx | 6 +++--- demo/src/components/SpacesContext.tsx | 6 +++--- demo/src/hooks/useTextComponentLock.ts | 4 ++-- demo/src/utils/url.ts | 27 +++++++++++++++++--------- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/demo/src/components/Header.tsx b/demo/src/components/Header.tsx index 58537649..f52bbee7 100644 --- a/demo/src/components/Header.tsx +++ b/demo/src/components/Header.tsx @@ -2,7 +2,7 @@ import { Avatar } from './Avatar'; import { AvatarStack } from './AvatarStack'; import { ExternalLinkSvg, InfoSvg } from './svg'; import { type Member } from '../utils/types'; -import { getParamNameFromUrl } from '../utils'; +import { getParamValueFromUrl, generateTeamName } from '../utils'; interface Props { self?: Member; @@ -10,7 +10,7 @@ interface Props { } export const Header = ({ self, others }: Props) => { - const teamName = getParamNameFromUrl('team', 1, ''); + const teamName = getParamValueFromUrl('team', generateTeamName); const formattedTeamName = teamName?.replace(/-/g, ' '); return ( @@ -20,7 +20,7 @@ export const Header = ({ self, others }: Props) => { > <div className="mx-auto justify-between grid grid-rows-2 grid-cols-2 max-w-screen-2xl md:px-8 lg:px-16 md:flex md:items-center"> <section className="py-4 shrink-0 mr-4"> - <p className="font-semibold pl-8 md:text-2xl capitalize">Team {formattedTeamName ?? 'Argo'}</p> + <p className="font-semibold pl-8 md:text-2xl capitalize">Team {formattedTeamName}</p> <p className="leading-5 pl-8">Pitch deck</p> </section> diff --git a/demo/src/components/SpacesContext.tsx b/demo/src/components/SpacesContext.tsx index 834eba7c..1ec91831 100644 --- a/demo/src/components/SpacesContext.tsx +++ b/demo/src/components/SpacesContext.tsx @@ -5,13 +5,13 @@ import Spaces, { type Space } from '@ably/spaces'; import { Realtime } from 'ably'; import { nanoid } from 'nanoid'; -import { getParamNameFromUrl } from '../utils'; +import { getParamValueFromUrl, generateSpaceName } from '../utils'; export const SpacesContext = React.createContext<Space | undefined>(undefined); const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [space, setSpace] = React.useState<Space | undefined>(undefined); - const spaceName = getParamNameFromUrl(); + const spaceName = getParamValueFromUrl('space', generateSpaceName); const [spaces, ably] = React.useMemo(() => { const clientId = nanoid(); @@ -28,7 +28,7 @@ const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ childre let ignore = false; const init = async () => { - const spaceInstance = await spaces.get(getParamNameFromUrl(), { + const spaceInstance = await spaces.get(getParamValueFromUrl('space', generateSpaceName), { offlineTimeout: 10_000, }); diff --git a/demo/src/hooks/useTextComponentLock.ts b/demo/src/hooks/useTextComponentLock.ts index 57aebc75..cd837dff 100644 --- a/demo/src/hooks/useTextComponentLock.ts +++ b/demo/src/hooks/useTextComponentLock.ts @@ -1,6 +1,6 @@ import { MutableRefObject, useCallback } from 'react'; import { useChannel } from '@ably-labs/react-hooks'; -import { findActiveMember, getParamNameFromUrl } from '../utils'; +import { findActiveMember, generateSpaceName, getParamValueFromUrl } from '../utils'; import { buildLockId } from '../utils/locking.ts'; import { usePreview } from '../components/PreviewContext.tsx'; import { useMembers } from './useMembers.ts'; @@ -17,7 +17,7 @@ interface UseTextComponentLockArgs { } export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: UseTextComponentLockArgs) => { - const spaceName = getParamNameFromUrl(); + const spaceName = getParamValueFromUrl('space', generateSpaceName); const { members, self } = useMembers(); const activeMember = findActiveMember(id, slide, members); const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId); diff --git a/demo/src/utils/url.ts b/demo/src/utils/url.ts index 968950b8..d29d9923 100644 --- a/demo/src/utils/url.ts +++ b/demo/src/utils/url.ts @@ -1,17 +1,26 @@ import { generate } from 'random-words'; -const getParamNameFromUrl = (paramName = 'space', exactly = 3, join = '-') => { +const getParamValueFromUrl = (param: string, generateDefault: () => string): string => { const url = new URL(window.location.href); - const spaceNameInParams = url.searchParams.get(paramName); + const value = url.searchParams.get(param); - if (spaceNameInParams) { - return spaceNameInParams; - } else { - const generatedName = generate({ exactly, join }); - url.searchParams.set(paramName, generatedName); + if (!value) { + const generatedValue = generateDefault(); + url.searchParams.set(param, generatedValue); window.history.replaceState({}, '', `?${url.searchParams.toString()}`); - return generatedName; + + return generatedValue; } + + return value; +}; + +const generateSpaceName = () => { + return generate({ exactly: 3, join: '-' }); +}; + +const generateTeamName = () => { + return generate({ exactly: 1, join: '' }); }; -export { getParamNameFromUrl }; +export { getParamValueFromUrl, generateSpaceName, generateTeamName }; From 21528ac220d61b8303dea20e74a81592acaac6eb Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 2 Oct 2023 09:31:34 -0300 Subject: [PATCH 086/191] Remove Husky MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Whilst I am a strong believer in making sure that each commit conforms to formatting rules and that its tests pass, I have found that the downsides of using Husky to enforce this outweigh the benefits. In particular, I have my own set of personal Git hooks that I use as part of my development workflow, and have configured my global core.hooksPath so that they are applied across all repositories that I work with. However, when I run `npm install` inside this repo, Husky then comes along and — without even giving me a warning that it’s going to do so — sets the repo’s local core.hooksPath to its own value, silently disabling my personal hooks. As far as I can tell, there is no way for Husky to nicely co-exist with personal Git hooks. With this in mind, I would prefer we remove Husky and leave it up to developers to find their own way of checking their commits. (I would also be happy to, alternatively, keep the Husky config but remove it from the `prepare` NPM script, leaving developers with the option of running `husky install` if they wish.) --- .husky/pre-commit | 6 ------ package-lock.json | 22 ---------------------- package.json | 2 -- 3 files changed, 30 deletions(-) delete mode 100755 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 26a35fcc..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npm run format:check - -npm test diff --git a/package-lock.json b/package-lock.json index 9850a22d..1e2bdd1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", - "husky": "^8.0.0", "prettier": "^2.8.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", @@ -2891,21 +2890,6 @@ "node": ">=10.19.0" } }, - "node_modules/husky": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", - "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", - "dev": true, - "bin": { - "husky": "lib/bin.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -7151,12 +7135,6 @@ "resolve-alpn": "^1.0.0" } }, - "husky": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", - "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", - "dev": true - }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", diff --git a/package.json b/package.json index b06ddb0a..1b7d4d3f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "build:mjs": "npx tsc --project tsconfig.mjs.json && cp res/package.mjs.json dist/mjs/package.json", "build:cjs": "npx tsc --project tsconfig.cjs.json && cp res/package.cjs.json dist/cjs/package.json", "build:iife": "rm -rf dist/iife && npx tsc --project tsconfig.iife.json && rollup -c", - "prepare": "husky install", "examples:locks": "ts-node ./examples/locks.ts" }, "exports": { @@ -49,7 +48,6 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", - "husky": "^8.0.0", "prettier": "^2.8.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", From 58b0409858c6bed21417844001791cea2ce044c9 Mon Sep 17 00:00:00 2001 From: Evgeny Khokhlov <khokhlov.e.n@gmail.com> Date: Tue, 3 Oct 2023 09:09:20 +0100 Subject: [PATCH 087/191] chore: bump ably-js to `1.2.45` (#200) Updated ably-js to 1.2.45 Also replaced `ErrorInfo` implementation --- package-lock.json | 26 +++++++++++++++------ package.json | 2 +- src/Errors.ts | 57 +++++------------------------------------------ 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e2bdd1b..37b8b771 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "vitest": "^0.34.3" }, "peerDependencies": { - "ably": "^1.2.43" + "ably": "^1.2.45" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1225,9 +1225,9 @@ } }, "node_modules/ably": { - "version": "1.2.43", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", - "integrity": "sha512-HZ99Nd98KzYToNUD4+ysHp4+vMp1NmYTi59yqGpejHo/VffTgg0pereoib0nRRAHYUhGUGys5HGwR5yHYESWDA==", + "version": "1.2.45", + "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", + "integrity": "sha512-8Jk8XT0dSPcLcZ8zSAF+mRtwMzaQR5dWHqGdx9Y859ZLhXiqf4gQb7P+4Elqy1l1IIRoDWgyt0fkyfo7ngUdMA==", "peer": true, "dependencies": { "@ably/msgpack-js": "^0.4.0", @@ -1236,6 +1236,18 @@ }, "engines": { "node": ">=5.10.x" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, "node_modules/acorn": { @@ -5878,9 +5890,9 @@ } }, "ably": { - "version": "1.2.43", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", - "integrity": "sha512-HZ99Nd98KzYToNUD4+ysHp4+vMp1NmYTi59yqGpejHo/VffTgg0pereoib0nRRAHYUhGUGys5HGwR5yHYESWDA==", + "version": "1.2.45", + "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", + "integrity": "sha512-8Jk8XT0dSPcLcZ8zSAF+mRtwMzaQR5dWHqGdx9Y859ZLhXiqf4gQb7P+4Elqy1l1IIRoDWgyt0fkyfo7ngUdMA==", "peer": true, "requires": { "@ably/msgpack-js": "^0.4.0", diff --git a/package.json b/package.json index 1b7d4d3f..ae64695d 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,6 @@ "nanoid": "^4.0.2" }, "peerDependencies": { - "ably": "^1.2.43" + "ably": "^1.2.45" } } diff --git a/src/Errors.ts b/src/Errors.ts index 5d0e6278..5572191b 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -1,57 +1,12 @@ -interface ErrorInfoValues { - message: string; - code: number; - statusCode: number; -} +import { ErrorInfo } from 'ably'; -// TODO: export ErrorInfo from ably-js and use that instead. -export class ErrorInfo extends Error { - code: number; - statusCode: number; +export const ERR_SPACE_NAME_MISSING = () => new ErrorInfo('must have a non-empty name for the space', 101000, 400); - constructor({ message, code, statusCode }: ErrorInfoValues) { - super(message); +export const ERR_NOT_ENTERED_SPACE = () => new ErrorInfo('must enter a space to perform this operation', 101001, 400); - if (typeof Object.setPrototypeOf !== 'undefined') { - Object.setPrototypeOf(this, ErrorInfo.prototype); - } +export const ERR_LOCK_REQUEST_EXISTS = () => new ErrorInfo('lock request already exists', 101002, 400); - this.code = code; - this.statusCode = statusCode; - } -} - -export const ERR_SPACE_NAME_MISSING = () => - new ErrorInfo({ - message: 'must have a non-empty name for the space', - code: 101000, - statusCode: 400, - }); - -export const ERR_NOT_ENTERED_SPACE = () => - new ErrorInfo({ - message: 'must enter a space to perform this operation', - code: 101001, - statusCode: 400, - }); - -export const ERR_LOCK_REQUEST_EXISTS = () => - new ErrorInfo({ - message: 'lock request already exists', - code: 101002, - statusCode: 400, - }); - -export const ERR_LOCK_IS_LOCKED = () => - new ErrorInfo({ - message: 'lock is currently locked', - code: 101003, - statusCode: 400, - }); +export const ERR_LOCK_IS_LOCKED = () => new ErrorInfo('lock is currently locked', 101003, 400); export const ERR_LOCK_INVALIDATED = () => - new ErrorInfo({ - message: 'lock was invalidated by a concurrent lock request which now holds the lock', - code: 101004, - statusCode: 400, - }); + new ErrorInfo('lock was invalidated by a concurrent lock request which now holds the lock', 101004, 400); From ddae7c56e3d0018f31a596e87597092b45aa3e8a Mon Sep 17 00:00:00 2001 From: Laura Martin <laura.martin@ably.com> Date: Thu, 5 Oct 2023 12:22:37 +0100 Subject: [PATCH 088/191] Publish to new CDN bucket only We've finally migrated to the new bucket, so we can stop publishing to the old bucket, which I intend to delete. --- .github/workflows/cdn.yml | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/.github/workflows/cdn.yml b/.github/workflows/cdn.yml index 7caa2e20..cfc9613e 100644 --- a/.github/workflows/cdn.yml +++ b/.github/workflows/cdn.yml @@ -47,30 +47,3 @@ jobs: npm run build - run: | aws s3 cp ./dist/iife/index.bundle.js s3://${{ github.event.inputs.bucket }}/spaces/${{ github.event.inputs.version }}/iife/index.bundle.js - publish_legacy: - if: ${{ github.event.inputs.bucket == 'prod-cdn.ably.com' }} # Only run for the prod build - runs-on: ubuntu-latest - # These permissions are necessary to run the configure-aws-credentials action - permissions: - id-token: write - contents: read - steps: - - uses: actions/checkout@v2 - with: - ref: ${{ github.event.inputs.version }} - - uses: aws-actions/configure-aws-credentials@v2 - with: - role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_PRODUCTION }}:role/github-actions-sdk - aws-region: us-east-1 - - name: Use Node.js 14.x - uses: actions/setup-node@v3 - with: - node-version: 14.x - - name: Install dependencies and build - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - npm ci - npm run build - - run: | - aws s3 cp ./dist/iife/index.bundle.js s3://cdn.ably.io/spaces/${{ github.event.inputs.version }}/iife/index.bundle.js From d5c912c3a917a18e54ba9a29c5d40734bf522067 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:12:35 +0000 Subject: [PATCH 089/191] build(deps-dev): bump postcss from 8.4.29 to 8.4.31 Bumps [postcss](https://github.com/postcss/postcss) from 8.4.29 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.29...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37b8b771..bc706d7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3844,9 +3844,9 @@ } }, "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -7829,9 +7829,9 @@ } }, "postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", From deb6e766f7bc7c3ed83a036f19a1d240ba9a97f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:56:17 +0000 Subject: [PATCH 090/191] build(deps-dev): bump postcss from 8.4.27 to 8.4.31 in /demo Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> --- demo/package-lock.json | 40 ++++++++++++++++++++++++++-------------- demo/package.json | 2 +- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index 200e857e..59db66d1 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@ably-labs/react-hooks": "^3.0.0-canary.1", "@ably/spaces": "0.1.3", - "ably": "^1.2.43", + "ably": "^1.2.44", "classnames": "^2.3.2", "dayjs": "^1.11.9", "nanoid": "^4.0.2", @@ -34,7 +34,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "netlify-cli": "^15.9.1", - "postcss": "^8.4.27", + "postcss": "^8.4.31", "tailwindcss": "^3.3.3", "typescript": "^5.0.2", "vite": "^4.4.5" @@ -1327,9 +1327,9 @@ } }, "node_modules/ably": { - "version": "1.2.43", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", - "integrity": "sha512-HZ99Nd98KzYToNUD4+ysHp4+vMp1NmYTi59yqGpejHo/VffTgg0pereoib0nRRAHYUhGUGys5HGwR5yHYESWDA==", + "version": "1.2.45", + "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", + "integrity": "sha512-8Jk8XT0dSPcLcZ8zSAF+mRtwMzaQR5dWHqGdx9Y859ZLhXiqf4gQb7P+4Elqy1l1IIRoDWgyt0fkyfo7ngUdMA==", "dependencies": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", @@ -1337,6 +1337,18 @@ }, "engines": { "node": ">=5.10.x" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, "node_modules/acorn": { @@ -19099,9 +19111,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -21211,9 +21223,9 @@ } }, "ably": { - "version": "1.2.43", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.43.tgz", - "integrity": "sha512-HZ99Nd98KzYToNUD4+ysHp4+vMp1NmYTi59yqGpejHo/VffTgg0pereoib0nRRAHYUhGUGys5HGwR5yHYESWDA==", + "version": "1.2.45", + "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", + "integrity": "sha512-8Jk8XT0dSPcLcZ8zSAF+mRtwMzaQR5dWHqGdx9Y859ZLhXiqf4gQb7P+4Elqy1l1IIRoDWgyt0fkyfo7ngUdMA==", "requires": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", @@ -34397,9 +34409,9 @@ "dev": true }, "postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", diff --git a/demo/package.json b/demo/package.json index c18baf69..98bae423 100644 --- a/demo/package.json +++ b/demo/package.json @@ -38,7 +38,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "netlify-cli": "^15.9.1", - "postcss": "^8.4.27", + "postcss": "^8.4.31", "tailwindcss": "^3.3.3", "typescript": "^5.0.2", "vite": "^4.4.5" From bb12d11e1f4a5bead79dcb43befbf1cb65b63621 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 28 Sep 2023 15:50:38 -0300 Subject: [PATCH 091/191] Remove ability to pass array of event names to EventEmitter.prototype.once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This copies across the change from commit cd4f7b3 in ably-js: > This functionality is implemented by wrapping the listener argument in > another listener. This means that the event emitter does not hold a > reference to the listener argument (other than that held indirectly > through the wrapper) and so it is not possible to remove this listener > using `off(..., listener)`. > > The client library specification does not specify a version of `once` > which accepts an array of event names, and we do not advertise it as > part of the public API. So, I think the simplest thing is to remove this > functionality. The removed code in the current commit also had a bug — the second argument in the removed code `self.off(eventName, this)` was incorrectly populated and this made the removed code equivalent to `self.off(eventName)`; that is, it removed _all_ listeners for the given event name. I believe that the removal of this code accounts for the increased expected number of calls to context.spy in one of the tests here. I’m not sure what reasoning led to the previous expected count of 3 (perhaps the expectation was written just based on the number of calls observed when running the code), but here’s my reasoning for the expectation of 4 calls: Before `context.eventEmitter['off']('myEvent', context.spy)`, the following calls are relevant to context.spy: - eventEmitter['on'](context.spy); - eventEmitter['on']('myEvent', context.spy); - eventEmitter['on'](['myEvent', 'myOtherEvent', 'myThirdEvent'], context.spy); - eventEmitter['once'](context.spy); - eventEmitter['once']('myEvent', context.spy); After `context.eventEmitter['off']('myEvent', context.spy)`, the following calls are relevant to context.spy: - eventEmitter['on'](context.spy); - eventEmitter['on'](['myEvent' /* no longer applies */, 'myOtherEvent', 'myThirdEvent'], context.spy); - eventEmitter['once'](context.spy); After `context.eventEmitter['emit']('myEvent', '')`, the following calls are relevant to context.spy: - eventEmitter['on'](context.spy); - eventEmitter['on'](['myEvent' /* no longer applies *\/, 'myOtherEvent', 'myThirdEvent'], context.spy); And hence calling `context.eventEmitter['emit']('myOtherEvent', '')` calls context.spy a further two times. --- src/utilities/EventEmitter.test.ts | 15 +------------- src/utilities/EventEmitter.ts | 33 +++++++----------------------- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/src/utilities/EventEmitter.test.ts b/src/utilities/EventEmitter.test.ts index aa4c87f6..330102ea 100644 --- a/src/utilities/EventEmitter.test.ts +++ b/src/utilities/EventEmitter.test.ts @@ -138,7 +138,6 @@ describe('EventEmitter', () => { eventEmitter['once'](altListener); eventEmitter['once']('myEvent', context.spy); eventEmitter['once']('myEvent', altListener); - eventEmitter['once'](['myEvent', 'myOtherEvent', 'myThirdEvent'], altListener); context.eventEmitter = eventEmitter; }); @@ -171,7 +170,7 @@ describe('EventEmitter', () => { context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledTimes(2); context.eventEmitter['emit']('myOtherEvent', ''); - expect(context.spy).toHaveBeenCalledTimes(3); + expect(context.spy).toHaveBeenCalledTimes(4); }); it('removes a specific listener from multiple events', () => { @@ -254,18 +253,6 @@ describe('EventEmitter', () => { context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledOnce(); }); - - it('adds a listener to multiple eventOnce fields on calling `once` with a listener and event name; and after emitting any of the events, all are removed', (context) => { - context.eventEmitter['once'](['myEvent', 'myOtherEvent', 'myThirdEvent'], context.spy); - expect(context.eventEmitter['eventsOnce']['myEvent']).toHaveLength(1); - expect(context.eventEmitter['eventsOnce']['myOtherEvent']).toHaveLength(1); - expect(context.eventEmitter['eventsOnce']['myThirdEvent']).toHaveLength(1); - expect(context.eventEmitter['emit']('myEvent', '')); - expect(context.eventEmitter['eventsOnce']['myEvent']).toBe(undefined); - expect(context.eventEmitter['eventsOnce']['myOtherEvent']).toBe(undefined); - expect(context.eventEmitter['eventsOnce']['myThirdEvent']).toBe(undefined); - expect(context.spy).toHaveBeenCalledOnce(); - }); }); describe('calling the emit method', () => { diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 247645a2..6c2d1101 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -223,46 +223,27 @@ export default class EventEmitter<T extends EventMap> { /** * Listen for a single occurrence of an event - * @param listenerOrEvents (optional) the name of the event to listen to + * @param listenerOrEvent (optional) the name of the event to listen to * @param listener (optional) the listener to be called */ once<K extends EventKey<T>>( - listenerOrEvents: K | K[] | EventListener<T[K]>, + listenerOrEvent: K | EventListener<T[K]>, listener?: EventListener<T[K]>, ): void | Promise<any> { // .once("eventName", () => {}) - if (isString(listenerOrEvents) && isFunction(listener)) { - const listeners = this.eventsOnce[listenerOrEvents] || (this.eventsOnce[listenerOrEvents] = []); + if (isString(listenerOrEvent) && isFunction(listener)) { + const listeners = this.eventsOnce[listenerOrEvent] || (this.eventsOnce[listenerOrEvent] = []); listeners.push(listener); return; } - // .once(["eventName"], () => {}) - if (isArray(listenerOrEvents) && isFunction(listener)) { - const self = this; - - listenerOrEvents.forEach(function (eventName) { - const listenerWrapper: EventListener<T[K]> = function (this: EventListener<T[K]>, listenerThis) { - const innerArgs = Array.prototype.slice.call(arguments) as [params: T[K]]; - listenerOrEvents.forEach((eventName) => { - self.off(eventName, this); - }); - - listener.apply(listenerThis, innerArgs); - }; - self.once(eventName, listenerWrapper); - }); - - return; - } - // .once(() => {}) - if (isFunction(listenerOrEvents)) { - this.anyOnce.push(listenerOrEvents); + if (isFunction(listenerOrEvent)) { + this.anyOnce.push(listenerOrEvent); return; } - throw new InvalidArgumentError('EventEmitter.once(): invalid arguments:' + inspect([listenerOrEvents, listener])); + throw new InvalidArgumentError('EventEmitter.once(): invalid arguments:' + inspect([listenerOrEvent, listener])); } /** From be9b08f2ea02bf67911e13d448bb6ff9d6ad265d Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Fri, 6 Oct 2023 10:31:52 +0100 Subject: [PATCH 092/191] Add missing package for code coverage --- package-lock.json | 70 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 71 insertions(+) diff --git a/package-lock.json b/package-lock.json index bc706d7c..2ebf0ce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", + "@vitest/coverage-v8": "^0.34.6", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", @@ -1156,6 +1157,31 @@ "vitest": ">=0.30.0 <1" } }, + "node_modules/@vitest/coverage-v8": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.34.6.tgz", + "integrity": "sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "magic-string": "^0.30.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.32.0 <1" + } + }, "node_modules/@vitest/expect": { "version": "0.34.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.4.tgz", @@ -3267,6 +3293,20 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-reports": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", @@ -5836,6 +5876,25 @@ "std-env": "^3.3.3" } }, + "@vitest/coverage-v8": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.34.6.tgz", + "integrity": "sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "magic-string": "^0.30.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" + } + }, "@vitest/expect": { "version": "0.34.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.4.tgz", @@ -7401,6 +7460,17 @@ "supports-color": "^7.1.0" } }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, "istanbul-reports": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", diff --git a/package.json b/package.json index ae64695d..89a38036 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", + "@vitest/coverage-v8": "^0.34.6", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", From e0f7dde430e1135f0a27ad378070471c07b15fc1 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 9 Oct 2023 15:37:29 -0300 Subject: [PATCH 093/191] Remove the LockAttributes type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves MMB-317: > During the locking work we added an export for LockAttributes. This broke the CDN build because rollup will only create a Spaces global constructor if there is a single export from it’s entry file. Instead it exports a fake ES6 module: > > Spaces global === { default: Spaces constructor, LockAttributes } > > instead of: > > Spaces global === Spaces constructor and resolves MMB-318: > In docs, we show a way to get values from LockAttributes > https://ably.com/docs/spaces/locking#subscribe in the subscriber. This > is however incorrect, because attributes on the receiving side is never > a Map, but an object. I have chosen to simply remove the LockAttributes class and replace it with Record<string, unknown>, which is what we use elsewhere for representing arbitrary key-value pairs in the public API of the SDK. --- docs/class-definitions.md | 2 +- src/Locks.test.ts | 5 +---- src/Locks.ts | 7 ++----- src/index.ts | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index dc9069ff..c117ce93 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -706,5 +706,5 @@ type LockStatus = 'pending' | 'locked' | 'unlocked'; Additional attributes that can be set when acquiring a lock. ```ts -type LockAttributes = Map<string, string>; +type LockAttributes = Record<string, unknown>; ``` \ No newline at end of file diff --git a/src/Locks.test.ts b/src/Locks.test.ts index a96d6a93..2f1a6216 100644 --- a/src/Locks.test.ts +++ b/src/Locks.test.ts @@ -3,7 +3,6 @@ import { Realtime, Types } from 'ably/promises'; import Space from './Space.js'; import type { SpaceMember, LockStatus } from './types.js'; -import { LockAttributes } from './Locks.js'; import { createPresenceMessage } from './utilities/test/fakes.js'; interface SpaceTestContext { @@ -77,9 +76,7 @@ describe('Locks', () => { const presenceUpdate = vi.spyOn(presence, 'update'); const lockID = 'test'; - const attributes = new LockAttributes(); - attributes.set('key1', 'foo'); - attributes.set('key2', 'bar'); + const attributes = { key1: 'foo', key2: 'bar' }; const req = await space.locks.acquire(lockID, { attributes }); expect(req.attributes).toBe(attributes); diff --git a/src/Locks.ts b/src/Locks.ts index 8e6eba2f..c8f9e20e 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -13,11 +13,7 @@ import EventEmitter, { import SpaceUpdate from './SpaceUpdate.js'; -export class LockAttributes extends Map<string, string> { - toJSON() { - return Object.fromEntries(this); - } -} +export type LockAttributes = Record<string, unknown>; interface LockOptions { attributes: LockAttributes; @@ -203,6 +199,7 @@ export default class Locks extends EventEmitter<LockEventMap> { this.emit('update', { ...lock, member }); } + // TODO this lock that comes from the PresenceMessage has no type checking this.setLock({ ...lock, member }); }); diff --git a/src/index.ts b/src/index.ts index f51aacdb..806f1e55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,4 +18,4 @@ export type { LockStatus, } from './types.js'; -export { LockAttributes } from './Locks.js'; +export type { LockAttributes } from './Locks.js'; From ec501c56693f118c6de7bad69d59bb20064db0e2 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 27 Sep 2023 15:20:54 -0300 Subject: [PATCH 094/191] Remove unused type `Provider` --- src/utilities/types.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/utilities/types.ts b/src/utilities/types.ts index cd3e2f5a..e1896cbf 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -1,6 +1,5 @@ import type { Types } from 'ably'; -import type { EventKey, EventListener, EventMap } from './EventEmitter.js'; import type { ProfileData, Lock } from '../types.js'; export type PresenceMember = { @@ -24,18 +23,6 @@ export type Subset<K> = { [attr in keyof K]?: K[attr] extends object ? Subset<K[attr]> : K[attr]; }; -export interface Provider<ProviderEventMap extends EventMap> { - subscribe<K extends EventKey<ProviderEventMap>>( - listenerOrEvents?: K | K[] | EventListener<ProviderEventMap[K]>, - listener?: EventListener<ProviderEventMap[K]>, - ): void; - - unsubscribe<K extends EventKey<ProviderEventMap>>( - listenerOrEvents?: K | K[] | EventListener<ProviderEventMap[K]>, - listener?: EventListener<ProviderEventMap[K]>, - ): void; -} - export type RealtimeMessage = Omit<Types.Message, 'connectionId'> & { connectionId: string; }; From 2ae2647b144bc0049ec7a9e474aa1538cf47eecb Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 20 Sep 2023 16:13:27 -0300 Subject: [PATCH 095/191] Make naming of event map types consistent Make them all of the form "EventMap" prefixed by the name of the class that emits them. Note that the use of "EventMap" as opposed to "EventsMap" naming is also consistent with the naming used by TypeScript for the DOM event maps (e.g. [1]). [1] https://github.com/microsoft/TypeScript/blob/f3dad2a07d007c40d260f13d6b087a2d5685d12d/src/lib/dom.generated.d.ts#L2418-L2422 --- src/Locks.ts | 16 ++++++++-------- src/Members.ts | 16 ++++++++-------- src/Space.ts | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Locks.ts b/src/Locks.ts index c8f9e20e..68433dc4 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -19,11 +19,11 @@ interface LockOptions { attributes: LockAttributes; } -type LockEventMap = { +type LocksEventMap = { update: Lock; }; -export default class Locks extends EventEmitter<LockEventMap> { +export default class Locks extends EventEmitter<LocksEventMap> { // locks tracks the local state of locks, which is used to determine whether // a lock's status has changed when processing presence updates. // @@ -131,9 +131,9 @@ export default class Locks extends EventEmitter<LockEventMap> { this.deleteLock(id, self.connectionId); } - subscribe<K extends EventKey<LockEventMap>>( - listenerOrEvents?: K | K[] | EventListener<LockEventMap[K]>, - listener?: EventListener<LockEventMap[K]>, + subscribe<K extends EventKey<LocksEventMap>>( + listenerOrEvents?: K | K[] | EventListener<LocksEventMap[K]>, + listener?: EventListener<LocksEventMap[K]>, ) { try { super.on(listenerOrEvents, listener); @@ -148,9 +148,9 @@ export default class Locks extends EventEmitter<LockEventMap> { } } - unsubscribe<K extends EventKey<LockEventMap>>( - listenerOrEvents?: K | K[] | EventListener<LockEventMap[K]>, - listener?: EventListener<LockEventMap[K]>, + unsubscribe<K extends EventKey<LocksEventMap>>( + listenerOrEvents?: K | K[] | EventListener<LocksEventMap[K]>, + listener?: EventListener<LocksEventMap[K]>, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Members.ts b/src/Members.ts index 2edae1f3..6ca88b73 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -10,7 +10,7 @@ import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; -type MemberEventsMap = { +type MembersEventMap = { leave: SpaceMember; enter: SpaceMember; update: SpaceMember; @@ -18,7 +18,7 @@ type MemberEventsMap = { remove: SpaceMember; }; -class Members extends EventEmitter<MemberEventsMap> { +class Members extends EventEmitter<MembersEventMap> { private lastMemberUpdate: Record<string, PresenceMember['data']['profileUpdate']['id']> = {}; private leavers: Leavers; @@ -69,9 +69,9 @@ class Members extends EventEmitter<MemberEventsMap> { return members.filter((m) => m.connectionId !== this.space.connectionId); } - subscribe<K extends EventKey<MemberEventsMap>>( - listenerOrEvents?: K | K[] | EventListener<MemberEventsMap[K]>, - listener?: EventListener<MemberEventsMap[K]>, + subscribe<K extends EventKey<MembersEventMap>>( + listenerOrEvents?: K | K[] | EventListener<MembersEventMap[K]>, + listener?: EventListener<MembersEventMap[K]>, ) { try { super.on(listenerOrEvents, listener); @@ -86,9 +86,9 @@ class Members extends EventEmitter<MemberEventsMap> { } } - unsubscribe<K extends EventKey<MemberEventsMap>>( - listenerOrEvents?: K | K[] | EventListener<MemberEventsMap[K]>, - listener?: EventListener<MemberEventsMap[K]>, + unsubscribe<K extends EventKey<MembersEventMap>>( + listenerOrEvents?: K | K[] | EventListener<MembersEventMap[K]>, + listener?: EventListener<MembersEventMap[K]>, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Space.ts b/src/Space.ts index 50dec7d2..a93b9959 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -29,11 +29,11 @@ const SPACE_OPTIONS_DEFAULTS = { }, }; -type SpaceEventsMap = { +type SpaceEventMap = { update: { members: SpaceMember[] }; }; -class Space extends EventEmitter<SpaceEventsMap> { +class Space extends EventEmitter<SpaceEventMap> { readonly channelName: string; readonly connectionId: string | undefined; readonly options: SpaceOptions; @@ -177,9 +177,9 @@ class Space extends EventEmitter<SpaceEventsMap> { return { members }; } - subscribe<K extends EventKey<SpaceEventsMap>>( - listenerOrEvents?: K | K[] | EventListener<SpaceEventsMap[K]>, - listener?: EventListener<SpaceEventsMap[K]>, + subscribe<K extends EventKey<SpaceEventMap>>( + listenerOrEvents?: K | K[] | EventListener<SpaceEventMap[K]>, + listener?: EventListener<SpaceEventMap[K]>, ) { try { super.on(listenerOrEvents, listener); @@ -194,9 +194,9 @@ class Space extends EventEmitter<SpaceEventsMap> { } } - unsubscribe<K extends EventKey<SpaceEventsMap>>( - listenerOrEvents?: K | K[] | EventListener<SpaceEventsMap[K]>, - listener?: EventListener<SpaceEventsMap[K]>, + unsubscribe<K extends EventKey<SpaceEventMap>>( + listenerOrEvents?: K | K[] | EventListener<SpaceEventMap[K]>, + listener?: EventListener<SpaceEventMap[K]>, ) { try { super.off(listenerOrEvents, listener); From 75ff9ad2564be55b5228da081b17bc8083a50b9b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 20 Sep 2023 15:03:37 -0300 Subject: [PATCH 096/191] =?UTF-8?q?Mark=20non-public=20API=20where=20we=20?= =?UTF-8?q?haven=E2=80=99t=20already?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I am shortly going to be adding documentation comments to the codebase, but only for public API. I’ll then use TypeDoc to generate documentation based on these comments. So, to avoid TypeDoc errors of the form "X does not have any documentation" for non-public API, I’m marking the non-public API as `private` or `@internal`, and will configure TypeDoc to ignore any such-marked items. My guessing of what constitutes public API is done based on what’s mentioned in the documentation on ably.com and what’s used in the demo code bundled in this repo. --- src/Cursors.ts | 3 ++- src/Locations.ts | 2 ++ src/Locks.ts | 12 ++++++++---- src/Members.ts | 7 +++++-- src/Space.ts | 13 +++++++++++-- src/utilities/EventEmitter.ts | 11 ++++++++++- 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index e3318bc2..a677bc38 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -27,10 +27,11 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { private readonly cursorDispensing: CursorDispensing; private readonly cursorHistory: CursorHistory; readonly options: CursorsOptions; - readonly channelName: string; + private readonly channelName: string; public channel?: Types.RealtimeChannelPromise; + /** @internal */ constructor(private space: Space) { super(); diff --git a/src/Locations.ts b/src/Locations.ts index f2b57106..58936210 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -18,10 +18,12 @@ type LocationsEventMap = { export default class Locations extends EventEmitter<LocationsEventMap> { private lastLocationUpdate: Record<string, PresenceMember['data']['locationUpdate']['id']> = {}; + /** @internal */ constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { super(); } + /** @internal */ async processPresenceMessage(message: PresenceMember) { // Only an update action is currently a valid location update. if (message.action !== 'update') return; diff --git a/src/Locks.ts b/src/Locks.ts index 68433dc4..6d11a201 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -32,6 +32,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { // have requested. private locks: Map<string, Map<string, Lock>>; + /** @internal */ constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { super(); this.locks = new Map(); @@ -165,6 +166,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { } } + /** @internal */ async processPresenceMessage(message: Types.PresenceMessage) { const member = await this.space.members.getByConnectionId(message.connectionId); if (!member) return; @@ -219,7 +221,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { // // TODO: remove this once the Ably system processes PENDING requests // internally using this same logic. - processPending(member: SpaceMember, pendingLock: Lock) { + private processPending(member: SpaceMember, pendingLock: Lock) { // if the requested lock ID is not currently locked, then mark the PENDING // lock request as LOCKED const lock = this.get(pendingLock.id); @@ -263,18 +265,19 @@ export default class Locks extends EventEmitter<LocksEventMap> { pendingLock.reason = ERR_LOCK_IS_LOCKED(); } - updatePresence(self: SpaceMember) { + private updatePresence(self: SpaceMember) { const update = new SpaceUpdate({ self, extras: this.getLockExtras(self.connectionId) }); return this.presenceUpdate(update.noop()); } + /** @internal */ getLock(id: string, connectionId: string): Lock | undefined { const locks = this.locks.get(id); if (!locks) return; return locks.get(connectionId); } - setLock(lock: Lock) { + private setLock(lock: Lock) { let locks = this.locks.get(lock.id); if (!locks) { locks = new Map(); @@ -283,7 +286,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { locks.set(lock.member.connectionId, lock); } - deleteLock(id: string, connectionId: string) { + private deleteLock(id: string, connectionId: string) { const locks = this.locks.get(id); if (!locks) return; return locks.delete(connectionId); @@ -303,6 +306,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { return requests; } + /** @internal */ getLockExtras(connectionId: string): PresenceMember['extras'] { const locks = this.getLocksForConnectionId(connectionId); if (locks.length === 0) return; diff --git a/src/Members.ts b/src/Members.ts index 6ca88b73..6c3b9611 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -22,11 +22,13 @@ class Members extends EventEmitter<MembersEventMap> { private lastMemberUpdate: Record<string, PresenceMember['data']['profileUpdate']['id']> = {}; private leavers: Leavers; + /** @internal */ constructor(private space: Space) { super(); this.leavers = new Leavers(this.space.options.offlineTimeout); } + /** @internal */ async processPresenceMessage(message: PresenceMember) { const { action, connectionId } = message; const isLeaver = !!this.leavers.getByConnectionId(connectionId); @@ -103,12 +105,13 @@ class Members extends EventEmitter<MembersEventMap> { } } + /** @internal */ async getByConnectionId(connectionId: string): Promise<SpaceMember | null> { const members = await this.getAll(); return members.find((m) => m.connectionId === connectionId) ?? null; } - createMember(message: PresenceMember): SpaceMember { + private createMember(message: PresenceMember): SpaceMember { return { clientId: message.clientId, connectionId: message.connectionId, @@ -122,7 +125,7 @@ class Members extends EventEmitter<MembersEventMap> { }; } - async onMemberOffline(member: SpaceMember) { + private async onMemberOffline(member: SpaceMember) { this.leavers.removeLeaver(member.connectionId); this.emit('remove', member); diff --git a/src/Space.ts b/src/Space.ts index a93b9959..1465a2db 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -34,7 +34,14 @@ type SpaceEventMap = { }; class Space extends EventEmitter<SpaceEventMap> { - readonly channelName: string; + /** + * @internal + */ + readonly client: Types.RealtimePromise; + private readonly channelName: string; + /** + * @internal + */ readonly connectionId: string | undefined; readonly options: SpaceOptions; readonly locations: Locations; @@ -44,9 +51,11 @@ class Space extends EventEmitter<SpaceEventMap> { readonly locks: Locks; readonly name: string; - constructor(name: string, readonly client: Types.RealtimePromise, options?: Subset<SpaceOptions>) { + /** @internal */ + constructor(name: string, client: Types.RealtimePromise, options?: Subset<SpaceOptions>) { super(); + this.client = client; this.options = this.setOptions(options); this.connectionId = this.client.connection.id; this.name = name; diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 6c2d1101..f23289a8 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -70,11 +70,18 @@ export type EventKey<T extends EventMap> = string & keyof T; export type EventListener<T> = (params: T) => void; export default class EventEmitter<T extends EventMap> { + /** @internal */ any: Array<Function>; + /** @internal */ events: Record<string, Function[]>; + /** @internal */ anyOnce: Array<Function>; + /** @internal */ eventsOnce: Record<string, Function[]>; + /** + * @internal + */ constructor() { this.any = []; this.events = Object.create(null); @@ -188,6 +195,8 @@ export default class EventEmitter<T extends EventMap> { } /** + * @internal + * * Emit an event * @param event the event name * @param arg the arguments to pass to the listener @@ -247,7 +256,7 @@ export default class EventEmitter<T extends EventMap> { } /** - * Private API + * @internal * * Listen for a single occurrence of a state event and fire immediately if currentState matches targetState * @param targetState the name of the state event to listen to From ff2d5c3eb238be915fb6e1d02ab06d85b48276b1 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 20 Sep 2023 14:43:31 -0300 Subject: [PATCH 097/191] Export all types that are referenced by exported types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As mentioned in 9f9ff98, I’ll be adding TypeDoc to the project. The aim of the change in this commit is to avoid TypeDoc warnings of the form "X (...) is referenced by Y but not included in the documentation." --- src/Cursors.ts | 2 +- src/Locations.ts | 2 +- src/Locks.ts | 4 ++-- src/Members.ts | 2 +- src/Space.ts | 2 +- src/index.ts | 11 ++++++++++- 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index a677bc38..c15975ec 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -16,7 +16,7 @@ import type { CursorsOptions, CursorUpdate } from './types.js'; import type { RealtimeMessage } from './utilities/types.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; -type CursorsEventMap = { +export type CursorsEventMap = { update: CursorUpdate; }; diff --git a/src/Locations.ts b/src/Locations.ts index 58936210..8ae53523 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -11,7 +11,7 @@ import type Space from './Space.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; import SpaceUpdate from './SpaceUpdate.js'; -type LocationsEventMap = { +export type LocationsEventMap = { update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown }; }; diff --git a/src/Locks.ts b/src/Locks.ts index 6d11a201..5ba13bdd 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -15,11 +15,11 @@ import SpaceUpdate from './SpaceUpdate.js'; export type LockAttributes = Record<string, unknown>; -interface LockOptions { +export interface LockOptions { attributes: LockAttributes; } -type LocksEventMap = { +export type LocksEventMap = { update: Lock; }; diff --git a/src/Members.ts b/src/Members.ts index 6c3b9611..67330e38 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -10,7 +10,7 @@ import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; -type MembersEventMap = { +export type MembersEventMap = { leave: SpaceMember; enter: SpaceMember; update: SpaceMember; diff --git a/src/Space.ts b/src/Space.ts index 1465a2db..d6f18ecb 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -29,7 +29,7 @@ const SPACE_OPTIONS_DEFAULTS = { }, }; -type SpaceEventMap = { +export type SpaceEventMap = { update: { members: SpaceMember[] }; }; diff --git a/src/index.ts b/src/index.ts index 806f1e55..a436977a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,11 @@ import Spaces from './Spaces.js'; -export type Space = Awaited<ReturnType<Spaces['get']>>; +export type { default as Space, SpaceEventMap } from './Space.js'; + +export type { default as Cursors, CursorsEventMap } from './Cursors.js'; +export type { default as Locations, LocationsEventMap } from './Locations.js'; +export type { default as Locks, LocksEventMap, LockOptions } from './Locks.js'; +export type { default as Members, MembersEventMap } from './Members.js'; // Can be changed to * when we update to TS5 @@ -19,3 +24,7 @@ export type { } from './types.js'; export type { LockAttributes } from './Locks.js'; + +export type { default as EventEmitter, EventListener, EventKey, EventMap } from './utilities/EventEmitter.js'; + +export type { Subset } from './utilities/types.js'; From 11aa44e5926121bd02ba11d73c9ecdc6f34e5bf4 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 21 Sep 2023 13:56:25 -0300 Subject: [PATCH 098/191] Extract a couple of events to their own types So that I can write documentation comments for them. --- src/Locations.ts | 10 +++++++++- src/Space.ts | 8 +++++++- src/index.ts | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Locations.ts b/src/Locations.ts index 8ae53523..b2ac3d27 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -11,8 +11,16 @@ import type Space from './Space.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; import SpaceUpdate from './SpaceUpdate.js'; +export namespace LocationsEvents { + export interface UpdateEvent { + member: SpaceMember; + currentLocation: unknown; + previousLocation: unknown; + } +} + export type LocationsEventMap = { - update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown }; + update: LocationsEvents.UpdateEvent; }; export default class Locations extends EventEmitter<LocationsEventMap> { diff --git a/src/Space.ts b/src/Space.ts index d6f18ecb..ae99823f 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -29,8 +29,14 @@ const SPACE_OPTIONS_DEFAULTS = { }, }; +export namespace SpaceEvents { + export interface UpdateEvent { + members: SpaceMember[]; + } +} + export type SpaceEventMap = { - update: { members: SpaceMember[] }; + update: SpaceEvents.UpdateEvent; }; class Space extends EventEmitter<SpaceEventMap> { diff --git a/src/index.ts b/src/index.ts index a436977a..e6c6b9e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ import Spaces from './Spaces.js'; -export type { default as Space, SpaceEventMap } from './Space.js'; +export type { default as Space, SpaceEventMap, SpaceEvents } from './Space.js'; export type { default as Cursors, CursorsEventMap } from './Cursors.js'; -export type { default as Locations, LocationsEventMap } from './Locations.js'; +export type { default as Locations, LocationsEventMap, LocationsEvents } from './Locations.js'; export type { default as Locks, LocksEventMap, LockOptions } from './Locks.js'; export type { default as Members, MembersEventMap } from './Members.js'; From 4dd9eb57915c5effaae478a13490273aecc2796b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 27 Sep 2023 15:27:14 -0300 Subject: [PATCH 099/191] Get rid of EventMap and EventKey types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I want to convert the *EventMap types from types to interfaces, but the EventMap type complicates this. So, let’s get rid of it and the accompanying EventKey type. I can’t see a compelling reason for their existence. Removing them also means fewer types for users to understand, and fewer types for us to document. --- src/Cursors.ts | 13 ++++------ src/Locations.ts | 11 +++------ src/Locks.ts | 11 +++------ src/Members.ts | 11 +++------ src/Space.ts | 11 +++------ src/index.ts | 2 +- src/utilities/EventEmitter.test.ts | 2 +- src/utilities/EventEmitter.ts | 38 ++++++++++++++++-------------- src/utilities/is.ts | 2 +- 9 files changed, 39 insertions(+), 62 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index c15975ec..11ef3177 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -3,12 +3,7 @@ import { Types } from 'ably'; import Space from './Space.js'; import CursorBatching from './CursorBatching.js'; import CursorDispensing from './CursorDispensing.js'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import CursorHistory from './CursorHistory.js'; import { CURSOR_UPDATE } from './CursorConstants.js'; @@ -91,7 +86,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { return !this.emitterHasListeners(subscriptions); } - private emitterHasListeners = (emitter: EventEmitter<{}>) => { + private emitterHasListeners = <T>(emitter: EventEmitter<T>) => { const flattenEvents = (obj: Record<string, Function[]>) => Object.entries(obj) .map((_, v) => v) @@ -105,7 +100,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { ); }; - subscribe<K extends EventKey<CursorsEventMap>>( + subscribe<K extends keyof CursorsEventMap>( listenerOrEvents?: K | K[] | EventListener<CursorsEventMap[K]>, listener?: EventListener<CursorsEventMap[K]>, ) { @@ -130,7 +125,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { } } - unsubscribe<K extends EventKey<CursorsEventMap>>( + unsubscribe<K extends keyof CursorsEventMap>( listenerOrEvents?: K | K[] | EventListener<CursorsEventMap[K]>, listener?: EventListener<CursorsEventMap[K]>, ) { diff --git a/src/Locations.ts b/src/Locations.ts index b2ac3d27..a5118786 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -1,9 +1,4 @@ -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; @@ -71,7 +66,7 @@ export default class Locations extends EventEmitter<LocationsEventMap> { await this.presenceUpdate(update.updateLocation(location)); } - subscribe<K extends EventKey<LocationsEventMap>>( + subscribe<K extends keyof LocationsEventMap>( listenerOrEvents?: K | K[] | EventListener<LocationsEventMap[K]>, listener?: EventListener<LocationsEventMap[K]>, ) { @@ -88,7 +83,7 @@ export default class Locations extends EventEmitter<LocationsEventMap> { } } - unsubscribe<K extends EventKey<LocationsEventMap>>( + unsubscribe<K extends keyof LocationsEventMap>( listenerOrEvents?: K | K[] | EventListener<LocationsEventMap[K]>, listener?: EventListener<LocationsEventMap[K]>, ) { diff --git a/src/Locks.ts b/src/Locks.ts index 5ba13bdd..a0ec4f14 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -4,12 +4,7 @@ import Space from './Space.js'; import type { Lock, SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import { ERR_LOCK_IS_LOCKED, ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS, ERR_NOT_ENTERED_SPACE } from './Errors.js'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import SpaceUpdate from './SpaceUpdate.js'; @@ -132,7 +127,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { this.deleteLock(id, self.connectionId); } - subscribe<K extends EventKey<LocksEventMap>>( + subscribe<K extends keyof LocksEventMap>( listenerOrEvents?: K | K[] | EventListener<LocksEventMap[K]>, listener?: EventListener<LocksEventMap[K]>, ) { @@ -149,7 +144,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { } } - unsubscribe<K extends EventKey<LocksEventMap>>( + unsubscribe<K extends keyof LocksEventMap>( listenerOrEvents?: K | K[] | EventListener<LocksEventMap[K]>, listener?: EventListener<LocksEventMap[K]>, ) { diff --git a/src/Members.ts b/src/Members.ts index 67330e38..f0461f4e 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -1,9 +1,4 @@ -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import Leavers from './Leavers.js'; import type { SpaceMember } from './types.js'; @@ -71,7 +66,7 @@ class Members extends EventEmitter<MembersEventMap> { return members.filter((m) => m.connectionId !== this.space.connectionId); } - subscribe<K extends EventKey<MembersEventMap>>( + subscribe<K extends keyof MembersEventMap>( listenerOrEvents?: K | K[] | EventListener<MembersEventMap[K]>, listener?: EventListener<MembersEventMap[K]>, ) { @@ -88,7 +83,7 @@ class Members extends EventEmitter<MembersEventMap> { } } - unsubscribe<K extends EventKey<MembersEventMap>>( + unsubscribe<K extends keyof MembersEventMap>( listenerOrEvents?: K | K[] | EventListener<MembersEventMap[K]>, listener?: EventListener<MembersEventMap[K]>, ) { diff --git a/src/Space.ts b/src/Space.ts index ae99823f..60d939b3 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -1,11 +1,6 @@ import Ably, { Types } from 'ably'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import Locations from './Locations.js'; import Cursors from './Cursors.js'; import Members from './Members.js'; @@ -192,7 +187,7 @@ class Space extends EventEmitter<SpaceEventMap> { return { members }; } - subscribe<K extends EventKey<SpaceEventMap>>( + subscribe<K extends keyof SpaceEventMap>( listenerOrEvents?: K | K[] | EventListener<SpaceEventMap[K]>, listener?: EventListener<SpaceEventMap[K]>, ) { @@ -209,7 +204,7 @@ class Space extends EventEmitter<SpaceEventMap> { } } - unsubscribe<K extends EventKey<SpaceEventMap>>( + unsubscribe<K extends keyof SpaceEventMap>( listenerOrEvents?: K | K[] | EventListener<SpaceEventMap[K]>, listener?: EventListener<SpaceEventMap[K]>, ) { diff --git a/src/index.ts b/src/index.ts index e6c6b9e7..1f2b1691 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,6 @@ export type { export type { LockAttributes } from './Locks.js'; -export type { default as EventEmitter, EventListener, EventKey, EventMap } from './utilities/EventEmitter.js'; +export type { default as EventEmitter, EventListener } from './utilities/EventEmitter.js'; export type { Subset } from './utilities/types.js'; diff --git a/src/utilities/EventEmitter.test.ts b/src/utilities/EventEmitter.test.ts index 330102ea..8e2a62e6 100644 --- a/src/utilities/EventEmitter.test.ts +++ b/src/utilities/EventEmitter.test.ts @@ -174,7 +174,7 @@ describe('EventEmitter', () => { }); it('removes a specific listener from multiple events', () => { - const eventEmitter = new EventEmitter(); + const eventEmitter = new EventEmitter<{ myEvent: unknown; myOtherEvent: unknown; myThirdEvent: unknown }>(); const specificListener = vi.fn(); eventEmitter['on'](['myEvent', 'myOtherEvent', 'myThirdEvent'], specificListener); eventEmitter['off'](['myEvent', 'myOtherEvent'], specificListener); diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index f23289a8..4ffa4929 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -1,6 +1,6 @@ import { isArray, isFunction, isObject, isString } from './is.js'; -function callListener(eventThis: { event: string }, listener: Function, args: unknown[]) { +function callListener<K>(eventThis: { event: K }, listener: Function, args: unknown[]) { try { listener.apply(eventThis, args); } catch (e) { @@ -17,14 +17,14 @@ function callListener(eventThis: { event: string }, listener: Function, args: un * @param listener the listener callback to remove * @param eventFilter (optional) event name instructing the function to only remove listeners for the specified event */ -export function removeListener( - targetListeners: (Function[] | Record<string, Function[]>)[], +export function removeListener<T>( + targetListeners: (Function[] | Record<keyof T, Function[]>)[], listener: Function, - eventFilter?: string, + eventFilter?: keyof T, ) { - let listeners: Function[] | Record<string, Function[]>; + let listeners: Function[] | Record<keyof T, Function[]>; let index: number; - let eventName: string; + let eventName: keyof T; for (let targetListenersIndex = 0; targetListenersIndex < targetListeners.length; targetListenersIndex++) { listeners = targetListeners[targetListenersIndex]; @@ -64,20 +64,17 @@ export class InvalidArgumentError extends Error { } } -export type EventMap = Record<string, unknown>; -// extract all the keys of an event map and use them as a type -export type EventKey<T extends EventMap> = string & keyof T; export type EventListener<T> = (params: T) => void; -export default class EventEmitter<T extends EventMap> { +export default class EventEmitter<T> { /** @internal */ any: Array<Function>; /** @internal */ - events: Record<string, Function[]>; + events: Record<keyof T, Function[]>; /** @internal */ anyOnce: Array<Function>; /** @internal */ - eventsOnce: Record<string, Function[]>; + eventsOnce: Record<keyof T, Function[]>; /** * @internal @@ -94,7 +91,7 @@ export default class EventEmitter<T extends EventMap> { * @param listenerOrEvents (optional) the name of the event to listen to or the listener to be called. * @param listener (optional) the listener to be called. */ - on<K extends EventKey<T>>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void { + on<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void { // .on(() => {}) if (isFunction(listenerOrEvents)) { this.any.push(listenerOrEvents); @@ -125,7 +122,7 @@ export default class EventEmitter<T extends EventMap> { * the listener is treated as an 'any' listener. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ - off<K extends EventKey<T>>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void { + off<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void { // .off() // don't use arguments.length === 0 here as don't won't handle // cases like .off(undefined) which is a valid call @@ -180,7 +177,7 @@ export default class EventEmitter<T extends EventMap> { * @param event (optional) the name of the event, or none for 'any' * @return array of events, or null if none */ - listeners<K extends EventKey<T>>(event: K): Function[] | null { + listeners<K extends keyof T>(event: K): Function[] | null { if (event) { const listeners = [...(this.events[event] ?? [])]; @@ -201,7 +198,7 @@ export default class EventEmitter<T extends EventMap> { * @param event the event name * @param arg the arguments to pass to the listener */ - emit<K extends EventKey<T>>(event: K, arg: T[K]) { + emit<K extends keyof T>(event: K, arg: T[K]) { const eventThis = { event }; const listeners: Function[] = []; @@ -235,7 +232,7 @@ export default class EventEmitter<T extends EventMap> { * @param listenerOrEvent (optional) the name of the event to listen to * @param listener (optional) the listener to be called */ - once<K extends EventKey<T>>( + once<K extends keyof T>( listenerOrEvent: K | EventListener<T[K]>, listener?: EventListener<T[K]>, ): void | Promise<any> { @@ -264,7 +261,12 @@ export default class EventEmitter<T extends EventMap> { * @param listener the listener to be called * @param listenerArgs */ - whenState(targetState: string, currentState: string, listener: EventListener<T[string]>, ...listenerArgs: unknown[]) { + whenState( + targetState: keyof T, + currentState: keyof T, + listener: EventListener<T[keyof T]>, + ...listenerArgs: unknown[] + ) { const eventThis = { event: targetState }; if (typeof targetState !== 'string' || typeof currentState !== 'string') { diff --git a/src/utilities/is.ts b/src/utilities/is.ts index 8a6fd0d3..5e3e4435 100644 --- a/src/utilities/is.ts +++ b/src/utilities/is.ts @@ -11,7 +11,7 @@ function isFunction(arg: unknown): arg is Function { return ['Function', 'AsyncFunction', 'GeneratorFunction', 'Proxy'].includes(typeOf(arg)); } -function isString(arg: unknown): arg is String { +function isString(arg: unknown): arg is string { return typeOf(arg) === 'String'; } From 3c1c394c475633a4284207e9ce3c0a5500df994c Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 27 Sep 2023 20:52:07 -0300 Subject: [PATCH 100/191] Improve typing of callListener function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change `listener`’s type from Function to EventListener, and make it clearer that an EventListener takes a single argument. --- src/utilities/EventEmitter.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 4ffa4929..c34fae2a 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -1,8 +1,8 @@ import { isArray, isFunction, isObject, isString } from './is.js'; -function callListener<K>(eventThis: { event: K }, listener: Function, args: unknown[]) { +function callListener<T, K extends keyof T>(eventThis: { event: K }, listener: EventListener<T[K]>, arg: T[K]) { try { - listener.apply(eventThis, args); + listener.apply(eventThis, [arg]); } catch (e) { console.error( 'EventEmitter.emit()', @@ -64,7 +64,7 @@ export class InvalidArgumentError extends Error { } } -export type EventListener<T> = (params: T) => void; +export type EventListener<T> = (param: T) => void; export default class EventEmitter<T> { /** @internal */ @@ -200,7 +200,7 @@ export default class EventEmitter<T> { */ emit<K extends keyof T>(event: K, arg: T[K]) { const eventThis = { event }; - const listeners: Function[] = []; + const listeners: EventListener<T[K]>[] = []; if (this.anyOnce.length > 0) { Array.prototype.push.apply(listeners, this.anyOnce); @@ -223,7 +223,7 @@ export default class EventEmitter<T> { } listeners.forEach(function (listener) { - callListener(eventThis, listener, [arg]); + callListener(eventThis, listener, arg); }); } @@ -259,13 +259,13 @@ export default class EventEmitter<T> { * @param targetState the name of the state event to listen to * @param currentState the name of the current state of this object * @param listener the listener to be called - * @param listenerArgs + * @param listenerArg the argument to pass to the listener */ - whenState( - targetState: keyof T, + whenState<K extends keyof T>( + targetState: K, currentState: keyof T, - listener: EventListener<T[keyof T]>, - ...listenerArgs: unknown[] + listener: EventListener<T[K]>, + listenerArg: T[K], ) { const eventThis = { event: targetState }; @@ -274,14 +274,11 @@ export default class EventEmitter<T> { } if (typeof listener !== 'function' && Promise) { return new Promise((resolve) => { - EventEmitter.prototype.whenState.apply( - this, - [targetState, currentState, resolve].concat(listenerArgs as any[]) as any, - ); + EventEmitter.prototype.whenState.apply(this, [targetState, currentState, resolve, listenerArg]); }); } if (targetState === currentState) { - callListener(eventThis, listener, listenerArgs); + callListener(eventThis, listener, listenerArg); } else { this.once(targetState, listener); } From 69128ab04fd1f793fdffefaa663d2814a3203fd7 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 21 Sep 2023 15:26:58 -0300 Subject: [PATCH 101/191] Convert *EventMap types to interfaces So that I can use {@link} in documentation comments to link to the documentation of each of their properties. --- src/Cursors.ts | 4 ++-- src/Locations.ts | 4 ++-- src/Locks.ts | 4 ++-- src/Members.ts | 4 ++-- src/Space.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 11ef3177..80aaa5b4 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -11,9 +11,9 @@ import type { CursorsOptions, CursorUpdate } from './types.js'; import type { RealtimeMessage } from './utilities/types.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; -export type CursorsEventMap = { +export interface CursorsEventMap { update: CursorUpdate; -}; +} const CURSORS_CHANNEL_TAG = '::$cursors'; diff --git a/src/Locations.ts b/src/Locations.ts index a5118786..9aa10b07 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -14,9 +14,9 @@ export namespace LocationsEvents { } } -export type LocationsEventMap = { +export interface LocationsEventMap { update: LocationsEvents.UpdateEvent; -}; +} export default class Locations extends EventEmitter<LocationsEventMap> { private lastLocationUpdate: Record<string, PresenceMember['data']['locationUpdate']['id']> = {}; diff --git a/src/Locks.ts b/src/Locks.ts index a0ec4f14..2f5942f0 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -14,9 +14,9 @@ export interface LockOptions { attributes: LockAttributes; } -export type LocksEventMap = { +export interface LocksEventMap { update: Lock; -}; +} export default class Locks extends EventEmitter<LocksEventMap> { // locks tracks the local state of locks, which is used to determine whether diff --git a/src/Members.ts b/src/Members.ts index f0461f4e..9243debb 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -5,13 +5,13 @@ import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; -export type MembersEventMap = { +export interface MembersEventMap { leave: SpaceMember; enter: SpaceMember; update: SpaceMember; updateProfile: SpaceMember; remove: SpaceMember; -}; +} class Members extends EventEmitter<MembersEventMap> { private lastMemberUpdate: Record<string, PresenceMember['data']['profileUpdate']['id']> = {}; diff --git a/src/Space.ts b/src/Space.ts index 60d939b3..b958f6cf 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -30,9 +30,9 @@ export namespace SpaceEvents { } } -export type SpaceEventMap = { +export interface SpaceEventMap { update: SpaceEvents.UpdateEvent; -}; +} class Space extends EventEmitter<SpaceEventMap> { /** From f792926c830d915f5ff0084f17fb97db1fefdd50 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 26 Sep 2023 15:21:58 -0300 Subject: [PATCH 102/191] =?UTF-8?q?Split=20LockStatus=E2=80=99s=20allowed?= =?UTF-8?q?=20values=20into=20separate=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So that they can be individually documented. --- src/index.ts | 1 + src/types.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1f2b1691..7a8629d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export type { SpaceMember, Lock, LockStatus, + LockStatuses, } from './types.js'; export type { LockAttributes } from './Locks.js'; diff --git a/src/types.ts b/src/types.ts index 9641b53c..96a16ba7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,7 +39,13 @@ export interface SpaceMember { }; } -export type LockStatus = 'pending' | 'locked' | 'unlocked'; +export namespace LockStatuses { + export type Pending = 'pending'; + export type Locked = 'locked'; + export type Unlocked = 'unlocked'; +} + +export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatuses.Unlocked; export type Lock = { id: string; From f4cd589758aef1a52f626467e8a67d6162a51960 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 27 Sep 2023 14:50:02 -0300 Subject: [PATCH 103/191] Create overload signatures for event-related methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subscribe, unsubscribe, … etc methods are easier to understand (and easier to document) when thought of as two variants: one which accepts one or more event names, and one which doesn’t. So create these overload signatures. --- src/Cursors.ts | 10 ++++++++ src/Locations.ts | 10 ++++++++ src/Locks.ts | 4 +++ src/Members.ts | 10 ++++++++ src/Space.ts | 4 +++ src/utilities/EventEmitter.ts | 46 ++++++++++++++++++++++++++++++++--- 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 80aaa5b4..88864305 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -100,6 +100,11 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { ); }; + subscribe<K extends keyof CursorsEventMap>( + eventOrEvents: K | K[], + listener?: EventListener<CursorsEventMap[K]>, + ): void; + subscribe(listener?: EventListener<CursorsEventMap[keyof CursorsEventMap]>): void; subscribe<K extends keyof CursorsEventMap>( listenerOrEvents?: K | K[] | EventListener<CursorsEventMap[K]>, listener?: EventListener<CursorsEventMap[K]>, @@ -125,6 +130,11 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { } } + unsubscribe<K extends keyof CursorsEventMap>( + eventOrEvents: K | K[], + listener?: EventListener<CursorsEventMap[K]>, + ): void; + unsubscribe(listener?: EventListener<CursorsEventMap[keyof CursorsEventMap]>): void; unsubscribe<K extends keyof CursorsEventMap>( listenerOrEvents?: K | K[] | EventListener<CursorsEventMap[K]>, listener?: EventListener<CursorsEventMap[K]>, diff --git a/src/Locations.ts b/src/Locations.ts index 9aa10b07..1a8cc804 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -66,6 +66,11 @@ export default class Locations extends EventEmitter<LocationsEventMap> { await this.presenceUpdate(update.updateLocation(location)); } + subscribe<K extends keyof LocationsEventMap>( + eventOrEvents: K | K[], + listener?: EventListener<LocationsEventMap[K]>, + ): void; + subscribe(listener?: EventListener<LocationsEventMap[keyof LocationsEventMap]>): void; subscribe<K extends keyof LocationsEventMap>( listenerOrEvents?: K | K[] | EventListener<LocationsEventMap[K]>, listener?: EventListener<LocationsEventMap[K]>, @@ -83,6 +88,11 @@ export default class Locations extends EventEmitter<LocationsEventMap> { } } + unsubscribe<K extends keyof LocationsEventMap>( + eventOrEvents: K | K[], + listener?: EventListener<LocationsEventMap[K]>, + ): void; + unsubscribe(listener?: EventListener<LocationsEventMap[keyof LocationsEventMap]>): void; unsubscribe<K extends keyof LocationsEventMap>( listenerOrEvents?: K | K[] | EventListener<LocationsEventMap[K]>, listener?: EventListener<LocationsEventMap[K]>, diff --git a/src/Locks.ts b/src/Locks.ts index 2f5942f0..e0475ecd 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -127,6 +127,8 @@ export default class Locks extends EventEmitter<LocksEventMap> { this.deleteLock(id, self.connectionId); } + subscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap[K]>): void; + subscribe(listener?: EventListener<LocksEventMap[keyof LocksEventMap]>): void; subscribe<K extends keyof LocksEventMap>( listenerOrEvents?: K | K[] | EventListener<LocksEventMap[K]>, listener?: EventListener<LocksEventMap[K]>, @@ -144,6 +146,8 @@ export default class Locks extends EventEmitter<LocksEventMap> { } } + unsubscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap[K]>): void; + unsubscribe(listener?: EventListener<LocksEventMap[keyof LocksEventMap]>): void; unsubscribe<K extends keyof LocksEventMap>( listenerOrEvents?: K | K[] | EventListener<LocksEventMap[K]>, listener?: EventListener<LocksEventMap[K]>, diff --git a/src/Members.ts b/src/Members.ts index 9243debb..16543aa7 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -66,6 +66,11 @@ class Members extends EventEmitter<MembersEventMap> { return members.filter((m) => m.connectionId !== this.space.connectionId); } + subscribe<K extends keyof MembersEventMap>( + eventOrEvents: K | K[], + listener?: EventListener<MembersEventMap[K]>, + ): void; + subscribe(listener?: EventListener<MembersEventMap[keyof MembersEventMap]>): void; subscribe<K extends keyof MembersEventMap>( listenerOrEvents?: K | K[] | EventListener<MembersEventMap[K]>, listener?: EventListener<MembersEventMap[K]>, @@ -83,6 +88,11 @@ class Members extends EventEmitter<MembersEventMap> { } } + unsubscribe<K extends keyof MembersEventMap>( + eventOrEvents: K | K[], + listener?: EventListener<MembersEventMap[K]>, + ): void; + unsubscribe(listener?: EventListener<MembersEventMap[keyof MembersEventMap]>): void; unsubscribe<K extends keyof MembersEventMap>( listenerOrEvents?: K | K[] | EventListener<MembersEventMap[K]>, listener?: EventListener<MembersEventMap[K]>, diff --git a/src/Space.ts b/src/Space.ts index b958f6cf..5faa252c 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -187,6 +187,8 @@ class Space extends EventEmitter<SpaceEventMap> { return { members }; } + subscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap[K]>): void; + subscribe(listener?: EventListener<SpaceEventMap[keyof SpaceEventMap]>): void; subscribe<K extends keyof SpaceEventMap>( listenerOrEvents?: K | K[] | EventListener<SpaceEventMap[K]>, listener?: EventListener<SpaceEventMap[K]>, @@ -204,6 +206,8 @@ class Space extends EventEmitter<SpaceEventMap> { } } + unsubscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap[K]>): void; + unsubscribe(listener?: EventListener<SpaceEventMap[keyof SpaceEventMap]>): void; unsubscribe<K extends keyof SpaceEventMap>( listenerOrEvents?: K | K[] | EventListener<SpaceEventMap[K]>, listener?: EventListener<SpaceEventMap[K]>, diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index c34fae2a..b7145ab7 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -87,10 +87,22 @@ export default class EventEmitter<T> { } /** + * {@label WITH_EVENTS} * Add an event listener - * @param listenerOrEvents (optional) the name of the event to listen to or the listener to be called. + * @param eventOrEvents the name of the event to listen to or the listener to be called. * @param listener (optional) the listener to be called. */ + on<K extends keyof T>(eventOrEvents?: K | K[], listener?: EventListener<T[K]>): void; + /** + * Behaves the same as { @link on:WITH_EVENTS | the overload which accepts one or more event names }, but listens to _all_ events. + * @param listener (optional) the listener to be called. + */ + on(listener?: EventListener<T[keyof T]>): void; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + on<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void; on<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void { // .on(() => {}) if (isFunction(listenerOrEvents)) { @@ -117,11 +129,22 @@ export default class EventEmitter<T> { } /** + * {@label WITH_EVENTS} * Remove one or more event listeners - * @param listenerOrEvents (optional) the name of the event whose listener is to be removed. If not supplied, - * the listener is treated as an 'any' listener. + * @param eventOrEvents the name of the event whose listener is to be removed. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ + off<K extends keyof T>(eventOrEvents?: K | K[], listener?: EventListener<T[K]>): void; + /** + * Behaves the same as { @link off:WITH_EVENTS | the overload which accepts one or more event names }, but removes the listener from _all_ events. + * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. + */ + off(listener?: EventListener<T[keyof T]>): void; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + off<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void; off<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void { // .off() // don't use arguments.length === 0 here as don't won't handle @@ -228,10 +251,25 @@ export default class EventEmitter<T> { } /** + * {@label WITH_EVENTS} * Listen for a single occurrence of an event - * @param listenerOrEvent (optional) the name of the event to listen to + * @param event the name of the event to listen to * @param listener (optional) the listener to be called */ + once<K extends keyof T>(event: K, listener?: EventListener<T[K]>): void | Promise<any>; + /** + * Behaves the same as { @link once:WITH_EVENTS | the overload which accepts one or more event names }, but listens for _all_ events. + * @param listener (optional) the listener to be called + */ + once(listener?: EventListener<T[keyof T]>): void | Promise<any>; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + once<K extends keyof T>( + listenerOrEvent: K | EventListener<T[K]>, + listener?: EventListener<T[K]>, + ): void | Promise<any>; once<K extends keyof T>( listenerOrEvent: K | EventListener<T[K]>, listener?: EventListener<T[K]>, From 7a08ffa93cd1747973bb036d478fe2e7ff2a8f83 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 27 Sep 2023 22:45:10 -0300 Subject: [PATCH 104/191] Add `this` type parameter to EventListener Make it clear, via the type system, that a listener can access the name of the emitted event via the `this` variable. --- src/Cursors.ts | 16 ++++++++-------- src/Locations.ts | 16 ++++++++-------- src/Locks.ts | 16 ++++++++-------- src/Members.ts | 16 ++++++++-------- src/Space.ts | 16 ++++++++-------- src/utilities/EventEmitter.ts | 36 +++++++++++++++++------------------ 6 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 88864305..82c28213 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -102,12 +102,12 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { subscribe<K extends keyof CursorsEventMap>( eventOrEvents: K | K[], - listener?: EventListener<CursorsEventMap[K]>, + listener?: EventListener<CursorsEventMap, K>, ): void; - subscribe(listener?: EventListener<CursorsEventMap[keyof CursorsEventMap]>): void; + subscribe(listener?: EventListener<CursorsEventMap, keyof CursorsEventMap>): void; subscribe<K extends keyof CursorsEventMap>( - listenerOrEvents?: K | K[] | EventListener<CursorsEventMap[K]>, - listener?: EventListener<CursorsEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<CursorsEventMap, K>, + listener?: EventListener<CursorsEventMap, K>, ) { try { super.on(listenerOrEvents, listener); @@ -132,12 +132,12 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { unsubscribe<K extends keyof CursorsEventMap>( eventOrEvents: K | K[], - listener?: EventListener<CursorsEventMap[K]>, + listener?: EventListener<CursorsEventMap, K>, ): void; - unsubscribe(listener?: EventListener<CursorsEventMap[keyof CursorsEventMap]>): void; + unsubscribe(listener?: EventListener<CursorsEventMap, keyof CursorsEventMap>): void; unsubscribe<K extends keyof CursorsEventMap>( - listenerOrEvents?: K | K[] | EventListener<CursorsEventMap[K]>, - listener?: EventListener<CursorsEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<CursorsEventMap, K>, + listener?: EventListener<CursorsEventMap, K>, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Locations.ts b/src/Locations.ts index 1a8cc804..ddf78885 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -68,12 +68,12 @@ export default class Locations extends EventEmitter<LocationsEventMap> { subscribe<K extends keyof LocationsEventMap>( eventOrEvents: K | K[], - listener?: EventListener<LocationsEventMap[K]>, + listener?: EventListener<LocationsEventMap, K>, ): void; - subscribe(listener?: EventListener<LocationsEventMap[keyof LocationsEventMap]>): void; + subscribe(listener?: EventListener<LocationsEventMap, keyof LocationsEventMap>): void; subscribe<K extends keyof LocationsEventMap>( - listenerOrEvents?: K | K[] | EventListener<LocationsEventMap[K]>, - listener?: EventListener<LocationsEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<LocationsEventMap, K>, + listener?: EventListener<LocationsEventMap, K>, ) { try { super.on(listenerOrEvents, listener); @@ -90,12 +90,12 @@ export default class Locations extends EventEmitter<LocationsEventMap> { unsubscribe<K extends keyof LocationsEventMap>( eventOrEvents: K | K[], - listener?: EventListener<LocationsEventMap[K]>, + listener?: EventListener<LocationsEventMap, K>, ): void; - unsubscribe(listener?: EventListener<LocationsEventMap[keyof LocationsEventMap]>): void; + unsubscribe(listener?: EventListener<LocationsEventMap, keyof LocationsEventMap>): void; unsubscribe<K extends keyof LocationsEventMap>( - listenerOrEvents?: K | K[] | EventListener<LocationsEventMap[K]>, - listener?: EventListener<LocationsEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<LocationsEventMap, K>, + listener?: EventListener<LocationsEventMap, K>, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Locks.ts b/src/Locks.ts index e0475ecd..23d7ce96 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -127,11 +127,11 @@ export default class Locks extends EventEmitter<LocksEventMap> { this.deleteLock(id, self.connectionId); } - subscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap[K]>): void; - subscribe(listener?: EventListener<LocksEventMap[keyof LocksEventMap]>): void; + subscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; + subscribe(listener?: EventListener<LocksEventMap, keyof LocksEventMap>): void; subscribe<K extends keyof LocksEventMap>( - listenerOrEvents?: K | K[] | EventListener<LocksEventMap[K]>, - listener?: EventListener<LocksEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<LocksEventMap, K>, + listener?: EventListener<LocksEventMap, K>, ) { try { super.on(listenerOrEvents, listener); @@ -146,11 +146,11 @@ export default class Locks extends EventEmitter<LocksEventMap> { } } - unsubscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap[K]>): void; - unsubscribe(listener?: EventListener<LocksEventMap[keyof LocksEventMap]>): void; + unsubscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; + unsubscribe(listener?: EventListener<LocksEventMap, keyof LocksEventMap>): void; unsubscribe<K extends keyof LocksEventMap>( - listenerOrEvents?: K | K[] | EventListener<LocksEventMap[K]>, - listener?: EventListener<LocksEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<LocksEventMap, K>, + listener?: EventListener<LocksEventMap, K>, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Members.ts b/src/Members.ts index 16543aa7..b058fc4b 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -68,12 +68,12 @@ class Members extends EventEmitter<MembersEventMap> { subscribe<K extends keyof MembersEventMap>( eventOrEvents: K | K[], - listener?: EventListener<MembersEventMap[K]>, + listener?: EventListener<MembersEventMap, K>, ): void; - subscribe(listener?: EventListener<MembersEventMap[keyof MembersEventMap]>): void; + subscribe(listener?: EventListener<MembersEventMap, keyof MembersEventMap>): void; subscribe<K extends keyof MembersEventMap>( - listenerOrEvents?: K | K[] | EventListener<MembersEventMap[K]>, - listener?: EventListener<MembersEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<MembersEventMap, K>, + listener?: EventListener<MembersEventMap, K>, ) { try { super.on(listenerOrEvents, listener); @@ -90,12 +90,12 @@ class Members extends EventEmitter<MembersEventMap> { unsubscribe<K extends keyof MembersEventMap>( eventOrEvents: K | K[], - listener?: EventListener<MembersEventMap[K]>, + listener?: EventListener<MembersEventMap, K>, ): void; - unsubscribe(listener?: EventListener<MembersEventMap[keyof MembersEventMap]>): void; + unsubscribe(listener?: EventListener<MembersEventMap, keyof MembersEventMap>): void; unsubscribe<K extends keyof MembersEventMap>( - listenerOrEvents?: K | K[] | EventListener<MembersEventMap[K]>, - listener?: EventListener<MembersEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<MembersEventMap, K>, + listener?: EventListener<MembersEventMap, K>, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Space.ts b/src/Space.ts index 5faa252c..bfb50354 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -187,11 +187,11 @@ class Space extends EventEmitter<SpaceEventMap> { return { members }; } - subscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap[K]>): void; - subscribe(listener?: EventListener<SpaceEventMap[keyof SpaceEventMap]>): void; + subscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap, K>): void; + subscribe(listener?: EventListener<SpaceEventMap, keyof SpaceEventMap>): void; subscribe<K extends keyof SpaceEventMap>( - listenerOrEvents?: K | K[] | EventListener<SpaceEventMap[K]>, - listener?: EventListener<SpaceEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<SpaceEventMap, K>, + listener?: EventListener<SpaceEventMap, K>, ) { try { super.on(listenerOrEvents, listener); @@ -206,11 +206,11 @@ class Space extends EventEmitter<SpaceEventMap> { } } - unsubscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap[K]>): void; - unsubscribe(listener?: EventListener<SpaceEventMap[keyof SpaceEventMap]>): void; + unsubscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap, K>): void; + unsubscribe(listener?: EventListener<SpaceEventMap, keyof SpaceEventMap>): void; unsubscribe<K extends keyof SpaceEventMap>( - listenerOrEvents?: K | K[] | EventListener<SpaceEventMap[K]>, - listener?: EventListener<SpaceEventMap[K]>, + listenerOrEvents?: K | K[] | EventListener<SpaceEventMap, K>, + listener?: EventListener<SpaceEventMap, K>, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index b7145ab7..ce1139c1 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -1,6 +1,6 @@ import { isArray, isFunction, isObject, isString } from './is.js'; -function callListener<T, K extends keyof T>(eventThis: { event: K }, listener: EventListener<T[K]>, arg: T[K]) { +function callListener<T, K extends keyof T>(eventThis: { event: K }, listener: EventListener<T, K>, arg: T[K]) { try { listener.apply(eventThis, [arg]); } catch (e) { @@ -64,7 +64,7 @@ export class InvalidArgumentError extends Error { } } -export type EventListener<T> = (param: T) => void; +export type EventListener<T, K extends keyof T> = (this: { event: K }, param: T[K]) => void; export default class EventEmitter<T> { /** @internal */ @@ -92,18 +92,18 @@ export default class EventEmitter<T> { * @param eventOrEvents the name of the event to listen to or the listener to be called. * @param listener (optional) the listener to be called. */ - on<K extends keyof T>(eventOrEvents?: K | K[], listener?: EventListener<T[K]>): void; + on<K extends keyof T>(eventOrEvents?: K | K[], listener?: EventListener<T, K>): void; /** * Behaves the same as { @link on:WITH_EVENTS | the overload which accepts one or more event names }, but listens to _all_ events. * @param listener (optional) the listener to be called. */ - on(listener?: EventListener<T[keyof T]>): void; + on(listener?: EventListener<T, keyof T>): void; /** * @internal * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. */ - on<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void; - on<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void { + on<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T, K>, listener?: EventListener<T, K>): void; + on<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T, K>, listener?: EventListener<T, K>): void { // .on(() => {}) if (isFunction(listenerOrEvents)) { this.any.push(listenerOrEvents); @@ -134,18 +134,18 @@ export default class EventEmitter<T> { * @param eventOrEvents the name of the event whose listener is to be removed. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ - off<K extends keyof T>(eventOrEvents?: K | K[], listener?: EventListener<T[K]>): void; + off<K extends keyof T>(eventOrEvents?: K | K[], listener?: EventListener<T, K>): void; /** * Behaves the same as { @link off:WITH_EVENTS | the overload which accepts one or more event names }, but removes the listener from _all_ events. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ - off(listener?: EventListener<T[keyof T]>): void; + off(listener?: EventListener<T, keyof T>): void; /** * @internal * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. */ - off<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void; - off<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T[K]>, listener?: EventListener<T[K]>): void { + off<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T, K>, listener?: EventListener<T, K>): void; + off<K extends keyof T>(listenerOrEvents?: K | K[] | EventListener<T, K>, listener?: EventListener<T, K>): void { // .off() // don't use arguments.length === 0 here as don't won't handle // cases like .off(undefined) which is a valid call @@ -223,7 +223,7 @@ export default class EventEmitter<T> { */ emit<K extends keyof T>(event: K, arg: T[K]) { const eventThis = { event }; - const listeners: EventListener<T[K]>[] = []; + const listeners: EventListener<T, K>[] = []; if (this.anyOnce.length > 0) { Array.prototype.push.apply(listeners, this.anyOnce); @@ -256,23 +256,23 @@ export default class EventEmitter<T> { * @param event the name of the event to listen to * @param listener (optional) the listener to be called */ - once<K extends keyof T>(event: K, listener?: EventListener<T[K]>): void | Promise<any>; + once<K extends keyof T>(event: K, listener?: EventListener<T, K>): void | Promise<any>; /** * Behaves the same as { @link once:WITH_EVENTS | the overload which accepts one or more event names }, but listens for _all_ events. * @param listener (optional) the listener to be called */ - once(listener?: EventListener<T[keyof T]>): void | Promise<any>; + once(listener?: EventListener<T, keyof T>): void | Promise<any>; /** * @internal * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. */ once<K extends keyof T>( - listenerOrEvent: K | EventListener<T[K]>, - listener?: EventListener<T[K]>, + listenerOrEvent: K | EventListener<T, K>, + listener?: EventListener<T, K>, ): void | Promise<any>; once<K extends keyof T>( - listenerOrEvent: K | EventListener<T[K]>, - listener?: EventListener<T[K]>, + listenerOrEvent: K | EventListener<T, K>, + listener?: EventListener<T, K>, ): void | Promise<any> { // .once("eventName", () => {}) if (isString(listenerOrEvent) && isFunction(listener)) { @@ -302,7 +302,7 @@ export default class EventEmitter<T> { whenState<K extends keyof T>( targetState: K, currentState: keyof T, - listener: EventListener<T[K]>, + listener: EventListener<T, K>, listenerArg: T[K], ) { const eventThis = { event: targetState }; From c7c735ac37403331479ce8d5bfda09ad1b7fb61f Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 2 Oct 2023 16:02:19 -0300 Subject: [PATCH 105/191] =?UTF-8?q?Create=20type=20for=20`EventListener`?= =?UTF-8?q?=E2=80=99s=20`this`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Easier to document. --- src/index.ts | 2 +- src/utilities/EventEmitter.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7a8629d6..b5ebc7d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,6 @@ export type { export type { LockAttributes } from './Locks.js'; -export type { default as EventEmitter, EventListener } from './utilities/EventEmitter.js'; +export type { default as EventEmitter, EventListener, EventListenerThis } from './utilities/EventEmitter.js'; export type { Subset } from './utilities/types.js'; diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index ce1139c1..fa7da99b 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -64,7 +64,11 @@ export class InvalidArgumentError extends Error { } } -export type EventListener<T, K extends keyof T> = (this: { event: K }, param: T[K]) => void; +export interface EventListenerThis<K> { + event: K; +} + +export type EventListener<T, K extends keyof T> = (this: EventListenerThis<K>, param: T[K]) => void; export default class EventEmitter<T> { /** @internal */ From f5ec64bd029ba6147dcc029ad4fdf782121c3f6e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 2 Oct 2023 16:19:58 -0300 Subject: [PATCH 106/191] Create overload signatures for Space.prototype.updateProfileData Motivation as 97f5ac2. --- src/Space.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Space.ts b/src/Space.ts index bfb50354..53c85dee 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -137,6 +137,8 @@ class Space extends EventEmitter<SpaceEventMap> { }); } + async updateProfileData(profileData: ProfileData): Promise<void>; + async updateProfileData(updateFn: (update: ProfileData) => ProfileData): Promise<void>; async updateProfileData(profileDataOrUpdateFn: ProfileData | ((update: ProfileData) => ProfileData)): Promise<void> { const self = await this.members.getSelf(); From f820438ca91998c0ed5d778e3fa1a61e707978cc Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 2 Oct 2023 16:26:41 -0300 Subject: [PATCH 107/191] Rename type param in Subset type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elsewhere in the codebase we’ve only used `K` for types related to keys. --- src/utilities/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/types.ts b/src/utilities/types.ts index e1896cbf..7eadcd05 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -19,8 +19,8 @@ export type PresenceMember = { }; } & Omit<Types.PresenceMessage, 'data'>; -export type Subset<K> = { - [attr in keyof K]?: K[attr] extends object ? Subset<K[attr]> : K[attr]; +export type Subset<T> = { + [attr in keyof T]?: T[attr] extends object ? Subset<T[attr]> : T[attr]; }; export type RealtimeMessage = Omit<Types.Message, 'connectionId'> & { From 0c7fb06b1f38eb8796bea05a2f45e30e4950d24e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 2 Oct 2023 16:58:31 -0300 Subject: [PATCH 108/191] Extract return value of Space.prototype.getState to its own type To make it easier to document. --- src/Space.ts | 6 +++++- src/index.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Space.ts b/src/Space.ts index 53c85dee..87d20a64 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -34,6 +34,10 @@ export interface SpaceEventMap { update: SpaceEvents.UpdateEvent; } +export interface SpaceState { + members: SpaceMember[]; +} + class Space extends EventEmitter<SpaceEventMap> { /** * @internal @@ -184,7 +188,7 @@ class Space extends EventEmitter<SpaceEventMap> { await this.presenceLeave(data); } - async getState(): Promise<{ members: SpaceMember[] }> { + async getState(): Promise<SpaceState> { const members = await this.members.getAll(); return { members }; } diff --git a/src/index.ts b/src/index.ts index b5ebc7d4..b5ac3fda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import Spaces from './Spaces.js'; -export type { default as Space, SpaceEventMap, SpaceEvents } from './Space.js'; +export type { default as Space, SpaceEventMap, SpaceEvents, SpaceState } from './Space.js'; export type { default as Cursors, CursorsEventMap } from './Cursors.js'; export type { default as Locations, LocationsEventMap, LocationsEvents } from './Locations.js'; From 273cc0640e3fd4a5317565fc74c25c991abfe025 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 3 Oct 2023 09:39:35 -0300 Subject: [PATCH 109/191] =?UTF-8?q?Extract=20type=20of=20updateProfileData?= =?UTF-8?q?=E2=80=99s=20function=20param?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I didn’t really want to do this, but with the TypeDoc configuration that I intend to use (i.e. with `"validation": true` and `requiredToBeDocumented` populated with all types) it seems unavoidable, else I get an error of the form > [warning] Space.updateProfileData.updateFn.__type, defined in ./dist/mjs/Space.d.ts, does not have any documentation. I could not figure out how to satisfy this warning without creating a type for this callback. I think that in a codebase which makes heavy use of callbacks this would not be ideal, but I guess we can live with it in our codebase, where there are few callbacks. The alternative would be to try and change the TypeDoc configuration, but I couldn’t find a non-trivial requiredToBeDocumented value which made the warning go away. And I don’t really want to make requiredToBeDocumented trivial nor turn off the `validation` option. It’s a shame that, as far as I can tell, TypeDoc doesn’t offer the option of suppressing a warning for a single symbol. --- src/Space.ts | 6 ++++-- src/index.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Space.ts b/src/Space.ts index 87d20a64..81685ce1 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -38,6 +38,8 @@ export interface SpaceState { members: SpaceMember[]; } +export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileData; + class Space extends EventEmitter<SpaceEventMap> { /** * @internal @@ -142,8 +144,8 @@ class Space extends EventEmitter<SpaceEventMap> { } async updateProfileData(profileData: ProfileData): Promise<void>; - async updateProfileData(updateFn: (update: ProfileData) => ProfileData): Promise<void>; - async updateProfileData(profileDataOrUpdateFn: ProfileData | ((update: ProfileData) => ProfileData)): Promise<void> { + async updateProfileData(updateFn: UpdateProfileDataFunction): Promise<void>; + async updateProfileData(profileDataOrUpdateFn: ProfileData | UpdateProfileDataFunction): Promise<void> { const self = await this.members.getSelf(); if (!isObject(profileDataOrUpdateFn) && !isFunction(profileDataOrUpdateFn)) { diff --git a/src/index.ts b/src/index.ts index b5ac3fda..a58d2e69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import Spaces from './Spaces.js'; -export type { default as Space, SpaceEventMap, SpaceEvents, SpaceState } from './Space.js'; +export type { default as Space, SpaceEventMap, SpaceEvents, SpaceState, UpdateProfileDataFunction } from './Space.js'; export type { default as Cursors, CursorsEventMap } from './Cursors.js'; export type { default as Locations, LocationsEventMap, LocationsEvents } from './Locations.js'; From b1597abb68fae640d9b6fc53c1b7011d58939914 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 3 Oct 2023 14:36:38 -0300 Subject: [PATCH 110/191] Fix ably-js links in class-definitions.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I haven’t seen this capitalisation used elsewhere, and I don’t think the fragment is appropriate. --- docs/class-definitions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index c117ce93..8d4cefe1 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -22,7 +22,7 @@ Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/se #### client -Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) client that was passed to the [constructor](#constructor). +Instance of the [ably-js](https://github.com/ably/ably-js) client that was passed to the [constructor](#constructor). ```ts type client = Ably.RealtimePromise; @@ -30,7 +30,7 @@ type client = Ably.RealtimePromise; #### connection -Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) connection, belonging to the client that was passed to the [constructor](#constructor). +Instance of the [ably-js](https://github.com/ably/ably-js) connection, belonging to the client that was passed to the [constructor](#constructor). ```ts type connection = Ably.ConnectionPromise; From 3fec9590ad59f6b3b22b27a084cd46e2b9e752ab Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 20 Sep 2023 14:26:38 -0300 Subject: [PATCH 111/191] =?UTF-8?q?Remove=20types=20from=20class-definitio?= =?UTF-8?q?ns.md=20that=20don=E2=80=99t=20exist=20in=20codebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/class-definitions.md | 50 ++++++++++----------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 8d4cefe1..186f065b 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -296,8 +296,11 @@ type SpaceMember = { connectionId: string; isConnected: boolean; profileData: Record<string, unknown>; - location: Location; - lastEvent: PresenceEvent; + location: unknown; + lastEvent: { + name: Types.PresenceAction; + timestamp: number; + }; }; ``` @@ -325,15 +328,6 @@ The current location of the user within the space. The most recent event emitted by [presence](https://ably.com/docs/presence-occupancy/presence?lang=javascript) and its timestamp. Events will be either `enter`, `leave`, `update` or `present`. -#### PresenceEvent - -```ts -type PresenceEvent = { - name: 'enter' | 'leave' | 'update' | 'present'; - timestamp: number; -}; -``` - ## Member Locations Handles the tracking of member locations within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). @@ -348,18 +342,18 @@ Available events: - ##### **update** - Fires when a member updates their location. The argument supplied to the event listener is a [LocationUpdate](#locationupdate-1). + Fires when a member updates their location. The argument supplied to the event listener is an UpdateEvent. ```ts - space.locations.subscribe('update', (locationUpdate: LocationUpdate) => {}); + space.locations.subscribe('update', (locationUpdate: LocationsEvents.UpdateEvent) => {}); ``` #### set() -Set your current location. [Location](#location-1) can be any JSON-serializable object. Emits a [locationUpdate](#locationupdate) event to all connected clients in this space. +Set your current location. The `location` argument can be any JSON-serializable object. Emits a `locationUpdate` event to all connected clients in this space. ```ts -type set = (update: Location) => Promise<void>; +type set = (location: unknown) => Promise<void>; ``` #### unsubscribe() @@ -375,7 +369,7 @@ space.locations.unsubscribe('update'); Get location for self. ```ts -type getSelf = () => Promise<Location>; +type getSelf = () => Promise<unknown>; ``` Example: @@ -389,7 +383,7 @@ const myLocation = await space.locations.getSelf(); Get location for all members. ```ts -type getAll = () => Promise<Record<ConnectionId, Location>>; +type getAll = () => Promise<Record<ConnectionId, unknown>>; ``` Example: @@ -403,7 +397,7 @@ const allLocations = await space.locations.getAll(); Get location for other members ```ts -type getOthers = () => Promise<Record<ConnectionId, Location>>; +type getOthers = () => Promise<Record<ConnectionId, unknown>>; ``` Example: @@ -414,26 +408,6 @@ const otherLocations = await space.locations.getOthers() ### Related types -#### Location - -Represents a location in an application. - -```ts -type Location = string | Record<string, unknown> | null; -``` - -#### LocationUpdate - -Represents a change between locations for a given [`SpaceMember`](#spacemember). - -```ts -type LocationUpdate = { - member: SpaceMember; - currentLocation: Location; - previousLocation: Location; -}; -``` - ## Live Cursors Handles tracking of member cursors within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). From c9ef8b9c6d06ef1e81912d98d727e54494d40429 Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 16 Oct 2023 09:09:21 +0100 Subject: [PATCH 112/191] Pass agent in param in channel get See ADR101 --- src/Space.test.ts | 10 +++++++++- src/Space.ts | 4 +++- src/Spaces.test.ts | 10 +++++++++- src/Spaces.ts | 4 +++- src/version.test.ts | 10 ++++++++++ src/version.ts | 3 +++ 6 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 src/version.test.ts create mode 100644 src/version.ts diff --git a/src/Space.test.ts b/src/Space.test.ts index eb0c62dd..52ed97cf 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -46,7 +46,15 @@ describe('Space', () => { const channelSpy = vi.spyOn(channels, 'get'); const space = new Space('test', client); - expect(channelSpy).toHaveBeenNthCalledWith(1, 'test-space'); + expect(channelSpy).toHaveBeenNthCalledWith( + 1, + 'test-space', + expect.objectContaining({ + params: expect.objectContaining({ + agent: expect.stringContaining('spaces') + }) + }) + ); expectTypeOf(space).toMatchTypeOf<Space>(); }); }); diff --git a/src/Space.ts b/src/Space.ts index 81685ce1..c3de9e56 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -13,6 +13,8 @@ import { isFunction, isObject } from './utilities/is.js'; import type { SpaceOptions, SpaceMember, ProfileData } from './types.js'; import type { Subset, PresenceMember } from './utilities/types.js'; +import { VERSION } from './version.js'; + // Replace by ::$space when that channel tag will be available const SPACE_CHANNEL_TAG = '-space'; @@ -68,7 +70,7 @@ class Space extends EventEmitter<SpaceEventMap> { this.name = name; this.channelName = `${name}${SPACE_CHANNEL_TAG}`; - this.channel = this.client.channels.get(this.channelName); + this.channel = this.client.channels.get(this.channelName, { params: { agent: `spaces/${VERSION}` } }); this.onPresenceUpdate = this.onPresenceUpdate.bind(this); this.channel.presence.subscribe(this.onPresenceUpdate); diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index 47e42eb1..a4d01305 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -27,7 +27,15 @@ describe('Spaces', () => { await spaces.get('test'); expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith('test-space'); + expect(spy).toHaveBeenNthCalledWith( + 1, + 'test-space', + expect.objectContaining({ + params: expect.objectContaining({ + agent: expect.stringContaining('spaces') + }) + }) + ); }); it<SpacesTestContext>('applies the agent header to an existing SDK instance', ({ client }) => { diff --git a/src/Spaces.ts b/src/Spaces.ts index 5f252819..4aec58a5 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -6,6 +6,8 @@ import Space from './Space.js'; import type { SpaceOptions } from './types.js'; import type { Subset } from './utilities/types.js'; +import { VERSION } from './version.js'; + export interface ClientWithOptions extends Types.RealtimePromise { options: { agents?: Record<string, string | boolean>; @@ -17,7 +19,7 @@ class Spaces { client: Types.RealtimePromise; connection: Types.ConnectionPromise; - readonly version = '0.1.3'; + readonly version = VERSION; constructor(client: Types.RealtimePromise) { this.client = client; diff --git a/src/version.test.ts b/src/version.test.ts new file mode 100644 index 00000000..16e76af6 --- /dev/null +++ b/src/version.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from 'vitest'; + +import { VERSION } from './version.js'; +import packageJson from '../package.json'; + +describe('VERSION', () => { + it('runtime version matches package.json entry', () => { + expect(packageJson.version).toEqual(VERSION); + }) +}); diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 00000000..2df250dc --- /dev/null +++ b/src/version.ts @@ -0,0 +1,3 @@ +// Manually update when bumping version +const VERSION = '0.1.3'; +export { VERSION }; From 42fa577407297c9a58e2ce1c18a0701f8085f09d Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 16 Oct 2023 09:45:51 +0100 Subject: [PATCH 113/191] Formatting fixes --- src/Space.test.ts | 10 +++++----- src/Spaces.test.ts | 10 +++++----- src/version.test.ts | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Space.test.ts b/src/Space.test.ts index 52ed97cf..99e268f9 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -47,13 +47,13 @@ describe('Space', () => { const space = new Space('test', client); expect(channelSpy).toHaveBeenNthCalledWith( - 1, - 'test-space', + 1, + 'test-space', expect.objectContaining({ params: expect.objectContaining({ - agent: expect.stringContaining('spaces') - }) - }) + agent: expect.stringContaining('spaces'), + }), + }), ); expectTypeOf(space).toMatchTypeOf<Space>(); }); diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index a4d01305..bebb9c84 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -28,13 +28,13 @@ describe('Spaces', () => { expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenNthCalledWith( - 1, - 'test-space', + 1, + 'test-space', expect.objectContaining({ params: expect.objectContaining({ - agent: expect.stringContaining('spaces') - }) - }) + agent: expect.stringContaining('spaces'), + }), + }), ); }); diff --git a/src/version.test.ts b/src/version.test.ts index 16e76af6..cc5c2152 100644 --- a/src/version.test.ts +++ b/src/version.test.ts @@ -6,5 +6,5 @@ import packageJson from '../package.json'; describe('VERSION', () => { it('runtime version matches package.json entry', () => { expect(packageJson.version).toEqual(VERSION); - }) + }); }); From 8ccdb65eb9e82da8792af462d7cc166a4021a5ec Mon Sep 17 00:00:00 2001 From: Dominik Piatek <dominik.piatek@ably.com> Date: Mon, 16 Oct 2023 09:58:29 +0100 Subject: [PATCH 114/191] Exclude test files from build --- tsconfig.base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.base.json b/tsconfig.base.json index ca0aa81c..13c51ac7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,6 +1,6 @@ { "include": ["./src/**/*.ts"], - "exclude": ["./src/**/*.test.tsx", "./src/fakes/**/*.ts"], + "exclude": ["./src/**/*.test.tsx", "./src/**/*.test.ts", "./src/fakes/**/*.ts"], "compilerOptions": { "target": "es6", "rootDir": "./src", From cca0dd4d5651514b55031e4b29711178e09591d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 09:01:46 +0000 Subject: [PATCH 115/191] build(deps): bump nanoid from 4.0.2 to 5.0.2 Bumps [nanoid](https://github.com/ai/nanoid) from 4.0.2 to 5.0.2. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/4.0.2...5.0.2) --- updated-dependencies: - dependency-name: nanoid dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ebf0ce1..83da4914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.3", "license": "ISC", "dependencies": { - "nanoid": "^4.0.2" + "nanoid": "^5.0.2" }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.2.1", @@ -3572,9 +3572,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", - "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.2.tgz", + "integrity": "sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==", "funding": [ { "type": "github", @@ -3585,7 +3585,7 @@ "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": "^18 || >=20" } }, "node_modules/natural-compare": { @@ -7681,9 +7681,9 @@ "dev": true }, "nanoid": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", - "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.2.tgz", + "integrity": "sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==" }, "natural-compare": { "version": "1.4.0", diff --git a/package.json b/package.json index 89a38036..7c7b325c 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "vitest": "^0.34.3" }, "dependencies": { - "nanoid": "^4.0.2" + "nanoid": "^5.0.2" }, "peerDependencies": { "ably": "^1.2.45" From 829e5e385bd002eaa3a5cea9c662ce13941461b7 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 18 Sep 2023 15:51:25 -0300 Subject: [PATCH 116/191] Add website documentation as TSDoc comments Copied all relevant-seeming documentation from website (at commit cb5de6a) to roughly appropriate places in code. Done: - avatar.textile - cursors.textile - locations.textile - locking.textile - space.textile Skipped: - setup.textile - index.textile --- src/Cursors.ts | 214 ++++++++++++++++++++++++++++++++++++++ src/Locations.ts | 188 +++++++++++++++++++++++++++++++++ src/Locks.ts | 257 +++++++++++++++++++++++++++++++++++++++++++++ src/Members.ts | 265 +++++++++++++++++++++++++++++++++++++++++++++++ src/Space.ts | 182 ++++++++++++++++++++++++++++++++ src/Spaces.ts | 83 +++++++++++++++ 6 files changed, 1189 insertions(+) diff --git a/src/Cursors.ts b/src/Cursors.ts index 82c28213..7219edae 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -17,6 +17,31 @@ export interface CursorsEventMap { const CURSORS_CHANNEL_TAG = '::$cursors'; +/** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L9-L17) --> + * The live cursors feature enables you to track the cursors of members within a space in realtime. + * + * Cursor events are emitted whenever a member moves their mouse within a space. In order to optimize the efficiency and frequency of updates, cursor position events are automatically batched. The batching interval may be customized in order to further optimize for increased performance versus the number of events published. + * + * Live cursor updates are not available as part of the "space state":/spaces/space#subscribe and must be subscribed to using "@space.cursors.subscribe()@":#subscribe. + * + * <aside data-type='important'> + * <p>Live cursors are a great way of providing contextual awareness as to what members are looking at within an application. However, too many cursors moving across a page can often be a distraction rather than an enhancement. As such, Ably recommends a maximum of 20 members simultaneously streaming their cursors in a space at any one time for an optimal end-user experience.</p> + * </aside> + * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L254-L262) --> + * h2(#foundations). Live cursor foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * Live cursors build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. + * + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own "channel":/channels. The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the "space state":/spaces/space and may only be subscribed to on the @cursors@ namespace. + * + * The channel is only created when a member calls @space.cursors.set()@. The live cursors channel object can be accessed through @space.cursors.channel@. To monitor the "underlying state of the cursors channel":/channels#states, the channel object can be accessed through @space.cursors.channel@. + * <!-- END WEBSITE DOCUMENTATION --> + */ export default class Cursors extends EventEmitter<CursorsEventMap> { private readonly cursorBatching: CursorBatching; private readonly cursorDispensing: CursorDispensing; @@ -45,6 +70,63 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * * @param {CursorUpdate} cursor * @return {void} + * + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L21-L74) --> + * Set the position of a member's cursor using the @set()@ method. A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Calling @set()@ will emit a cursor event so that other members are informed of the cursor movement in realtime. + * + * A member must have been "entered":/spaces/space#enter into the space to set their cursor position. + * + * The @set()@ method takes the following parameters: + * + * |_. Parameter |_. Description |_. Type | + * | position.x | The position of the member's cursor on the X-axis. | Number | + * | position.y | The position of the member's cursor on the Y-axis. | Number | + * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor, such as a color. | Object | + * + * <aside data-type='note'> + * <p>The @data@ parameter can be used to stream additional information related to a cursor's movement, such as:</p> + * <ul><li>The color that other member's should display a cursor as.</li> + * <li>The ID of an element that a user may be dragging for drag and drop functionality.</li> + * <li>Details of any cursor annotations.</li></ul> + * <p>Be aware that as live cursor updates are batched it is not advisable to publish data unrelated to cursor position in the @data@ parameter. Use a "pub/sub channel":/channels instead.</p> + * </aside> + * + * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the @set()@ method: + * + * ```[javascript] + * window.addEventListener('mousemove', ({ clientX, clientY }) => { + * space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); + * }); + * ``` + * + * The following is an example payload of a cursor event. Cursor events are uniquely identifiable by the @connectionId@ of a cursor. + * + * ```[json] + * { + * "hd9743gjDc": { + * "connectionId": "hd9743gjDc", + * "clientId": "clemons#142", + * "position": { + * "x": 864, + * "y": 32 + * }, + * "data": { + * "color": "red" + * } + * } + * } + * ``` + * + * The following are the properties of a cursor event payload: + * + * |_. Property |_. Description |_. Type | + * | connectionId | The unique identifier of the member's "connection":/connect. | String | + * | clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | position | An object containing the position of a member's cursor. | Object | + * | position.x | The position of the member's cursor on the X-axis. | Number | + * | position.y | The position of the member's cursor on the Y-axis. | Number | + * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | + * <!-- END WEBSITE DOCUMENTATION --> */ async set(cursor: Pick<CursorUpdate, 'position' | 'data'>) { const self = await this.space.members.getSelf(); @@ -100,6 +182,23 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { ); }; + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L78-L90) --> + * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling @set()@. Use the @subscribe()@ method on the @cursors@ object of a space to receive updates. + * + * <aside data-type='note'> + * <p>The rate at which cursor events are published is controlled by the @outboundBatchInterval@ property set in the "cursor options":#options of a space.</p> + * </aside> + * + * The following is an example of subscribing to cursor events: + * + * ```[javascript] + * space.cursors.subscribe('update', (cursorUpdate) => { + * console.log(cursorUpdate); + * }); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ subscribe<K extends keyof CursorsEventMap>( eventOrEvents: K | K[], listener?: EventListener<CursorsEventMap, K>, @@ -130,6 +229,23 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { } } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L94-L106) --> + * Unsubscribe from cursor events to remove previously registered listeners. + * + * The following is an example of removing a listener for cursor update events: + * + * ```[javascript] + * space.cursors.unsubscribe(`update`, listener); + * ``` + * + * Or remove all listeners: + * + * ```[javascript] + * space.cursors.unsubscribe(); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ unsubscribe<K extends keyof CursorsEventMap>( eventOrEvents: K | K[], listener?: EventListener<CursorsEventMap, K>, @@ -159,6 +275,10 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { } } + /** + * <!-- This is to avoid duplication of the website documentation. --> + * See the documentation for {@link getAll}. + */ async getSelf(): Promise<CursorUpdate | null> { const self = await this.space.members.getSelf(); if (!self) return null; @@ -167,6 +287,10 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { return allCursors[self.connectionId]; } + /** + * <!-- This is to avoid duplication of the website documentation. --> + * See the documentation for {@link getAll}. + */ async getOthers(): Promise<Record<string, null | CursorUpdate>> { const self = await this.space.members.getSelf(); if (!self) return {}; @@ -177,6 +301,96 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { return allCursorsFiltered; } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L126-L211) --> + * Cursor positions can be retrieved in one-off calls. These are local calls that retrieve the latest position of cursors retained in memory by the SDK. + * + * The following is an example of retrieving a member's own cursor position: + * + * ```[javascript] + * const myCursor = await space.cursors.getSelf(); + * ``` + * + * The following is an example payload returned by @space.cursors.getSelf()@: + * + * ```[json] + * { + * “clientId”: “DzOBJqgGXzyUBb816Oa6i”, + * “connectionId”: “__UJBKZchX”, + * "position": { + * "x": 864, + * "y": 32 + * } + * } + * ``` + * + * The following is an example of retrieving the cursor positions for all members other than the member themselves: + * + * ```[javascript] + * const othersCursors = await space.cursors.getOthers(); + * ``` + * + * The following is an example payload returned by @space.cursors.getOthers()@: + * + * ```[json] + * { + * "3ej3q7yZZz": { + * "clientId": "yyXidHatpP3hJpMpXZi8W", + * "connectionId": "3ej3q7yZZz", + * "position": { + * "x": 12, + * "y": 3 + * } + * }, + * "Z7CA3-1vlR": { + * "clientId": "b18mj5B5hm-govdFEYRyb", + * "connectionId": "Z7CA3-1vlR", + * "position": { + * "x": 502, + * "y": 43 + * } + * } + * } + * ``` + * + * The following is an example of retrieving the cursor positions for all members, including the member themselves. @getAll()@ is useful for retrieving the initial position of members' cursors. + * + * ```[javascript] + * const allCursors = await space.cursors.getAll(); + * ``` + * + * The following is an example payload returned by @space.cursors.getAll()@: + * + * ```[json] + * { + * "3ej3q7yZZz": { + * "clientId": "yyXidHatpP3hJpMpXZi8W", + * "connectionId": "3ej3q7yZZz", + * "position": { + * "x": 12, + * "y": 3 + * } + * }, + * "Z7CA3-1vlR": { + * "clientId": "b18mj5B5hm-govdFEYRyb", + * "connectionId": "Z7CA3-1vlR", + * "position": { + * "x": 502, + * "y": 43 + * } + * }, + * "__UJBKZchX": { + * “clientId”: “DzOBJqgGXzyUBb816Oa6i”, + * “connectionId”: “__UJBKZchX”, + * "position": { + * "x": 864, + * "y": 32 + * } + * } + * } + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ async getAll(): Promise<Record<string, null | CursorUpdate>> { const channel = this.getChannel(); return await this.cursorHistory.getLastCursorUpdate(channel, this.options.paginationLimit); diff --git a/src/Locations.ts b/src/Locations.ts index ddf78885..c117ffe8 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -18,6 +18,21 @@ export interface LocationsEventMap { update: LocationsEvents.UpdateEvent; } +/** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L9-L11) --> + * The member location feature enables you to track where members are within a space, to see which part of your application they're interacting with. A location could be the form field they have selected, the cell they're currently editing in a spreadsheet, or the slide they're viewing within a slide deck. Multiple members can be present in the same location. + * + * Member locations are used to visually display which component other members currently have selected, or are currently active on. Events are emitted whenever a member sets their location, such as when they click on a new cell, or slide. Events are received by members subscribed to location events and the UI component can be highlighted with the active member's profile data to visually display their location. + * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L211-L215) --> + * h2(#foundations). Member location foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * Member locations build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. + * <!-- END WEBSITE DOCUMENTATION --> + */ export default class Locations extends EventEmitter<LocationsEventMap> { private lastLocationUpdate: Record<string, PresenceMember['data']['locationUpdate']['id']> = {}; @@ -55,6 +70,21 @@ export default class Locations extends EventEmitter<LocationsEventMap> { } } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L15-L25) --> + * Use the @set()@ method to emit a location event in realtime when a member changes their location. This will be received by all location subscribers to inform them of the location change. A @location@ can be any JSON-serializable object, such as a slide number or element ID. + * + * A member must have been "entered":/spaces/space#enter into the space to set their location. + * + * The @set()@ method is commonly combined with "@addEventListener()@":https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener or a React "synthetic event":https://react.dev/learn/responding-to-events#adding-event-handlers, such as @onClick@ or @onHover@. + * + * The following is an example of a member setting their location to a specific slide number, and element on that slide: + * + * ```[javascript] + * await space.locations.set({ slide: '3', component: 'slide-title' }); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ async set(location: unknown) { const self = await this.space.members.getSelf(); @@ -66,6 +96,73 @@ export default class Locations extends EventEmitter<LocationsEventMap> { await this.presenceUpdate(update.updateLocation(location)); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L29-L91) --> + * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling "@set()@":#set. Use the @subscribe()@ method on the @locations@ namespace of the space to receive updates. + * + * All location changes are @update@ events. When a location update is received, clear the highlight from the UI component of the member's @previousLocation@ and add it to @currentLocation@. + * + * <aside data-type='note'> + * <p> A location update is also emitted when a member "leaves":/spaces/space#leave a space. The member's @currentLocation@ will be @null@ for these events so that any UI component highlighting can be cleared.</p> + * </aside> + * + * The following is an example of subscribing to location events: + * + * ```[javascript] + * space.locations.subscribe('update', (locationUpdate) => { + * console.log(locationUpdate); + * }); + * ``` + * + * The following is an example payload of a location event. Information about location is returned in @currentLocation@ and @previousLocation@: + * + * ```[json] + * { + * "member": { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": true, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * }, + * "location": { + * "slide": "3", + * "component": "slide-title" + * }, + * "lastEvent": { + * "name": "update", + * "timestamp": 1972395669758 + * } + * }, + * "previousLocation": { + * "slide": "2", + * "component": null + * }, + * "currentLocation": { + * "slide": "3", + * "component": "slide-title" + * } + * } + * ``` + * + * The following are the properties of a location event payload: + * + * |_. Property |_. Description |_. Type | + * | member.clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | member.connectionId | The unique identifier of the member's "connection":/connect. | String | + * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | + * | member.lastEvent.name | The most recent "event":/spaces/avatar emitted by the member. Will be one of @enter@, @update@, @leave@ or @remove@. | String | + * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | + * | previousLocation | The previous location of the member. | Object | + * | currentLocation | The new location of the member. | Object | + * + * <aside data-type='further-reading'> + * <p>Member location subscription listeners only trigger on events related to members' locations. Each event only contains the payload of the member that triggered it. Alternatively, "space state":/spaces/space can be subscribed to which returns an array of all members with their latest state every time any event is triggered.</p> + * </aside> + * <!-- END WEBSITE DOCUMENTATION --> + */ subscribe<K extends keyof LocationsEventMap>( eventOrEvents: K | K[], listener?: EventListener<LocationsEventMap, K>, @@ -88,6 +185,23 @@ export default class Locations extends EventEmitter<LocationsEventMap> { } } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L95-L107) --> + * Unsubscribe from location events to remove previously registered listeners. + * + * The following is an example of removing a listener for location update events: + * + * ```[javascript] + * space.locations.unsubscribe('update', listener); + * ``` + * + * Or remove all listeners: + * + * ```[javascript] + * space.locations.unsubscribe(); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ unsubscribe<K extends keyof LocationsEventMap>( eventOrEvents: K | K[], listener?: EventListener<LocationsEventMap, K>, @@ -110,11 +224,19 @@ export default class Locations extends EventEmitter<LocationsEventMap> { } } + /** + * <!-- This is to avoid duplication of the website documentation. --> + * See the documentation for {@link getAll}. + */ async getSelf(): Promise<unknown> { const self = await this.space.members.getSelf(); return self ? self.location : null; } + /** + * <!-- This is to avoid duplication of the website documentation. --> + * See the documentation for {@link getAll}. + */ async getOthers(): Promise<Record<string, unknown>> { const members = await this.space.members.getOthers(); @@ -124,6 +246,72 @@ export default class Locations extends EventEmitter<LocationsEventMap> { }, {}); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L111-L172) --> + * Member locations can also be retrieved in one-off calls. These are local calls and retrieve the location of members retained in memory by the SDK. + * + * The following is an example of retrieving a member's own location: + * + * ```[javascript] + * const myLocation = await space.locations.getSelf(); + * ``` + * + * The following is an example payload returned by @space.locations.getSelf()@. It will return the properties of the member's @location@: + * + * ```[json] + * { + * "slide": "3", + * "component": "slide-title" + * } + * ``` + * + * The following is an example of retrieving the location objects of all members other than the member themselves. + * + * ```[javascript] + * const othersLocations = await space.locations.getOthers(); + * ``` + * + * The following is an example payload returned by @space.locations.getOthers()@: It will return the properties of all member's @location@ by their @connectionId@: + * + * ```[json] + * { + * "xG6H3lnrCn": { + * "slide": "1", + * "component": "textBox-1" + * }, + * "el29SVLktW": { + * "slide": "1", + * "component": "image-2" + * } + * } + * ``` + * + * The following is an example of retrieving the location objects of all members, including the member themselves: + * + * ```[javascript] + * const allLocations = await space.locations.getAll(); + * ``` + * + * The following is an example payload returned by @space.locations.getAll()@. It will return the properties of all member's @location@ by their @connectionId@: + * + * ```[json] + * { + * "xG6H3lnrCn": { + * "slide": "1", + * "component": "textBox-1" + * }, + * "el29SVLktW": { + * "slide": "1", + * "component": "image-2" + * }, + * "dieF3291kT": { + * "slide": "3", + * "component": "slide-title" + * } + * } + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ async getAll(): Promise<Record<string, unknown>> { const members = await this.space.members.getAll(); return members.reduce((acc: Record<string, unknown>, member: SpaceMember) => { diff --git a/src/Locks.ts b/src/Locks.ts index 23d7ce96..34acb3be 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -18,6 +18,39 @@ export interface LocksEventMap { update: Lock; } +/** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L9-L37) --> + * The component locking feature enables members to optimistically lock stateful UI components before editing them. This reduces the chances of conflicting changes being made to the same component by different members. A component could be a cell in a spreadsheet that a member is updating, or an input field on a form they're filling in. + * + * Once a lock has been acquired by a member, the component that it relates to can be updated in the UI to visually indicate to other members that it is locked and and which member has the lock. The component can then be updated once the editing member has released the lock to indicate that it is now unlocked. + * + * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three "states":#states and may only transition between states in specific circumstances. + * + * <aside data-type='important'> + * <p>Optimistic locking means that there is a chance that two members may begin editing the same UI component before it is confirmed which member holds the lock. On average, the time taken to reconcile which member holds a lock is in the hundreds of milliseconds. Your application needs to handle the member that successfully obtained the lock, as well as the member that had their request invalidated.</p> + * </aside> + * + * h2(#states). Lock states + * + * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call "@acquire()@":#acquire on the lock identifier related to it. Alternatively, you could wait until they receive a @locked@ event and display a spinning symbol in the UI until this is received. In either case a subsequent @unlocked@ event may invalidate that member's lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. + * + * A lock will be in one of the following states: + * + * - @pending@ := A member has requested a lock by calling "@acquire()@":#acquire. + * - @locked@ := The lock is confirmed to be held by the requesting member. + * - @unlocked@ := The lock is confirmed to not be locked by the requesting member, or has been "released":#release by a member previously holding the lock. + * + * The following lock state transitions may occur: + * + * * None → @pending@: a member calls "@acquire()@":#acquire to request a lock. + * * @pending@ → @locked@: the requesting member holds the lock. + * * @pending@ → @unlocked@: the requesting member does not hold the lock, since another member already holds it. + * * @locked@ → @unlocked@: the lock was either explicitly "released":#release by the member, or their request was invalidated by a concurrent request which took precedence. + * * @unlocked@ → @locked@: the requesting member reacquired a lock they previously held. + * + * Only transitions that result in a @locked@ or @unlocked@ status will emit a lock event that members can "@subscribe()@":#subscribe to. + * <!-- END WEBSITE DOCUMENTATION --> + */ export default class Locks extends EventEmitter<LocksEventMap> { // locks tracks the local state of locks, which is used to determine whether // a lock's status has changed when processing presence updates. @@ -33,6 +66,32 @@ export default class Locks extends EventEmitter<LocksEventMap> { this.locks = new Map(); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L171-L192) --> + * Use the @get()@ method to query whether a lock is currently locked, and by which member if it is. The lock is identifiable by its unique string ID. + * + * The following is an example of checking whether a lock identifier is currently locked: + * + * ```[javascript] + * const isLocked = space.locks.get(id) !== undefined; + * ``` + * + * The following is an example of checking which member holds the lock: + * + * ```[javascript] + * const { member } = space.locks.get(id); + * ``` + * + * The following is an example of viewing the attributes assigned to the lock by the member holding it: + * + * ```[javascript] + * const { request } = space.locks.get(id); + * const viewLock = request.attributes.get(key); + * ``` + * + * If the lock is not currently held by a member, @get()@ will return @undefined@. Otherwise it will return the most recent lock event for the lock. + * <!-- END WEBSITE DOCUMENTATION --> + */ get(id: string): Lock | undefined { const locks = this.locks.get(id); if (!locks) return; @@ -45,6 +104,62 @@ export default class Locks extends EventEmitter<LocksEventMap> { // This will be async in the future, when pending requests are no longer processed // in the library. + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L196-L247) --> + * Locks can also be retrieved in one-off calls. These are local calls and retrieve the locks retained in memory by the SDK. + * + * The following is an example of retrieving an array of all currently held locks in a space: + * + * ```[javascript] + * const allLocks = await space.locks.getAll(); + * ``` + * + * The following is an example payload returned by @space.locks.getAll()@: + * + * ```[json] + * [ + * { + * "id": "s1-c2", + * "status": "locked", + * "timestamp": 1247525627533, + * "member": { + * "clientId": "amint#5", + * "connectionId": "hg35a4fgjAs", + * "isConnected": true, + * "lastEvent": { + * "name": "update", + * "timestamp": 173459567340 + * }, + * "location": null, + * "profileData": { + * "username": "Arit Mint", + * "avatar": "https://slides-internal.com/users/amint.png" + * } + * } + * }, + * { + * "id": "s3-c4", + * "status": "locked", + * "timestamp": 1247115627423, + * "member": { + * "clientId": "torange#1", + * "connectionId": "tt7233ghUa", + * "isConnected": true, + * "lastEvent": { + * "name": "update", + * "timestamp": 167759566354 + * }, + * "location": null, + * "profileData": { + * "username": "Tara Orange", + * "avatar": "https://slides-internal.com/users/torange.png" + * } + * } + * } + * ] + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ async getAll(): Promise<Lock[]> { const allLocks: Lock[] = []; @@ -59,6 +174,10 @@ export default class Locks extends EventEmitter<LocksEventMap> { return allLocks; } + /** + * <!-- This is to avoid duplication of the website documentation. --> + * See the documentation for {@link getAll}. + */ async getSelf(): Promise<Lock[]> { const self = await this.space.members.getSelf(); @@ -67,6 +186,10 @@ export default class Locks extends EventEmitter<LocksEventMap> { return this.getLocksForConnectionId(self.connectionId).filter((lock) => lock.status === 'locked'); } + /** + * <!-- This is to avoid duplication of the website documentation. --> + * See the documentation for {@link getAll}. + */ async getOthers(): Promise<Lock[]> { const self = await this.space.members.getSelf(); const allLocks = await this.getAll(); @@ -76,6 +199,42 @@ export default class Locks extends EventEmitter<LocksEventMap> { return allLocks.filter((lock) => lock.member.connectionId !== self.connectionId); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L41-L72) --> + * Use the @acquire()@ method to attempt to acquire a lock with a given unique ID. Additional @attributes@ may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using @attributes@ is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. + * + * A member must have been "entered":/spaces/space#enter into the space to acquire a lock. + * + * The following is an example of attempting to acquire a lock: + * + * ```[javascript] + * const acquireLock = await space.locks.acquire(id); + * ``` + * + * The following is an example of passing a set of @attributes@ when trying to acquire a lock: + * + * ```[javascript] + * const lockAttributes = new Map(); + * lockAttributes.set('component', 'cell-d3'); + * const acquireLock = await space.locks.acquire(id, { lockAttributes }); + * ``` + * + * The following is an example payload returned by @space.locks.acquire()@. The promise will resolve to a lock request with the @pending@ status: + * + * ```[json] + * { + * "id": "s2-d14", + * "status": "pending", + * "timestamp": 1247525689781, + * "attributes": { + * "componentId": "cell-d14" + * } + * } + * ``` + * + * Once a member requests a lock by calling @acquire()@, the lock is temporarily in the "pending state":#states. An event will be emitted based on whether the lock request was successful (a status of @locked@) or invalidated (a status of @unlocked@). This can be "subscribed":#subscribe to in order for the client to know whether their lock request was successful or not. + * <!-- END WEBSITE DOCUMENTATION --> + */ async acquire(id: string, opts?: LockOptions): Promise<Lock> { const self = await this.space.members.getSelf(); if (!self) { @@ -109,6 +268,23 @@ export default class Locks extends EventEmitter<LocksEventMap> { return lock; } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L76-L88) --> + * Use the @release()@ method to explicitly release a lock once a member has finished editing the related component. For example, the @release()@ method can be called once a user clicks outside of the component, such as clicking on another cell within a spreadsheet. Any UI indications that the previous cell was locked can then be cleared. + * + * The following is an example of releasing a lock: + * + * ```[javascript] + * await space.locks.release(id); + * ``` + * + * Releasing a lock will emit a lock event with a "lock status":#states of @unlocked@. + * + * <aside data-type='note'> + * <p>When a member "leaves":/spaces/space#leave a space, their locks are automatically released.</p> + * </aside> + * <!-- END WEBSITE DOCUMENTATION --> + */ async release(id: string): Promise<void> { const self = await this.space.members.getSelf(); @@ -127,6 +303,70 @@ export default class Locks extends EventEmitter<LocksEventMap> { this.deleteLock(id, self.connectionId); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L92-L151) --> + * Subscribe to lock events by registering a listener. Lock events are emitted whenever the "lock state":#states transitions into @locked@ or @unlocked@. Use the @subscribe()@ method on the @locks@ namespace of the space to receive updates. + * + * All lock events are @update@ events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. + * + * The following is an example of subscribing to lock events: + * + * ```[javascript] + * space.locks.subscribe('update', (lock) => { + * console.log(lock); + * }); + * ``` + * + * The following is an example payload of a lock event: + * + * ```json + * { + * "id": "s2-d14", + * "status": "unlocked", + * "timestamp": 1247525689781, + * "attributes": { + * "componentId": "cell-d14" + * }, + * "reason": { + * "message": "lock is currently locked", + * "code": 101003, + * "statusCode": 400 + * }, + * "member": { + * "clientId": "smango", + * "connectionId": "hs343gjsdc", + * "isConnected": true, + * "profileData": { + * "username": "Saiorse Mango" + * }, + * "location": { + * "slide": "sheet-2", + * "component": "d-14" + * }, + * "lastEvent": { + * "name": "update", + * "timestamp": 1247525689781 + * } + * } + * } + * ``` + * + * The following are the properties of a lock event payload: + * + * |_. Property |_. Description |_. Type | + * | id | The unique ID of the lock request. | String | + * | status | The lock "status":#states of the event. Will be one of @locked@, @unlocked@ or @pending@. | String | + * | timestamp | The timestamp of the lock event. | Number | + * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | + * | reason | The reason why the @request.status@ is @unlocked@. | ErrorInfo | + * | member.clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | member.connectionId | The unique identifier of the member's "connection":/connect. | String | + * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | + * | member.lastEvent.name | The most recent "event":/spaces/avatar#events emitted by the member. Will be one of @enter@, @update@, @leave@ or @remove@. | String | + * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | + * <!-- END WEBSITE DOCUMENTATION --> + */ subscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; subscribe(listener?: EventListener<LocksEventMap, keyof LocksEventMap>): void; subscribe<K extends keyof LocksEventMap>( @@ -146,6 +386,23 @@ export default class Locks extends EventEmitter<LocksEventMap> { } } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L155-L166) --> + * Unsubscribe from lock events to remove previously registered listeners. + * + * The following is an example of removing a listener for lock update events: + * + * ```[javascript] + * space.locks.unsubscribe('update', listener); + * ``` + * + * Or remove all listeners: + * + * ```[javascript] + * space.locks.unsubscribe(); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ unsubscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; unsubscribe(listener?: EventListener<LocksEventMap, keyof LocksEventMap>): void; unsubscribe<K extends keyof LocksEventMap>( diff --git a/src/Members.ts b/src/Members.ts index b058fc4b..4d6f5f4d 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -13,6 +13,35 @@ export interface MembersEventMap { remove: SpaceMember; } +/** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L9-L25) --> + * Avatar stacks are the most common way of showing the online status of members in an application by displaying an avatar for each member. Events are emitted whenever a member enters or leaves a space, or updates their profile data. Additional information can also be provided, such as a profile picture and email address. + * + * Subscribe to the @space.members@ namespace in order to keep your avatar stack updated in realtime. + * + * h2(#events). Event types + * + * The following four event types are emitted by members: + * + * - @enter@ := A new member has entered the space. The member has either entered explicitly by calling "@space.enter()@":/spaces/space#enter, or has attempted to update their profile data before entering a space, which will instead emit an @enter@ event. + * - @updateProfile@ := A member has updated their profile data by calling "@space.updateProfileData()@":/spaces/space#update-profile. + * - @leave@ := A member has left the space. The member has either left explicitly by calling "@space.leave()@":/spaces/space#leave, or has abruptly disconnected and not re-established a connection within 15 seconds. + * - @remove@ := A member has been removed from the members list after the "@offlineTimeout@":/spaces/space#options period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their @leave@ and @remove@ events. + * - @update@ := This event is emitted whenever any one of the above events is emitted. + * + * <aside data-type='note'> + * <p>Members "enter":/spaces/space#enter, "leave":/spaces/space#leave, and "update":/spaces/space#update-profile a "space":/spaces/space directly. The @members@ namespace is used to subscribe to these updates.</p> + * </aside> + * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L297-L301) --> + * h2(#foundations). Avatar stack foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * Avatar stacks build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. + * <!-- END WEBSITE DOCUMENTATION --> + */ class Members extends EventEmitter<MembersEventMap> { private lastMemberUpdate: Record<string, PresenceMember['data']['profileUpdate']['id']> = {}; private leavers: Leavers; @@ -51,21 +80,234 @@ class Members extends EventEmitter<MembersEventMap> { } } + /** + * <!-- This is to avoid duplication of the website documentation. --> + * See the documentation for {@link getAll}. + */ async getSelf(): Promise<SpaceMember | null> { return this.space.connectionId ? await this.getByConnectionId(this.space.connectionId) : null; } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L129-L250) --> + * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member's own profile data to them, or retrieving a list of all other members to use to "update their profile data":/spaces/space#update-profile. + * + * The following is an example of retrieving a member's own member object: + * + * ```[javascript] + * const myMemberInfo = await space.members.getSelf(); + * ``` + * + * The following is an example payload returned by @space.members.getSelf()@: + * + * ```[json] + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * } + * ``` + * + * The following is an example of retrieving an array of member objects for all members other than the member themselves. Ths includes members that have recently left the space, but have not yet been removed. + * + * ```[javascript] + * const othersMemberInfo = await space.members.getOthers(); + * ``` + * + * The following is an example payload returned by @space.members.getOthers()@: + * + * ```[json] + * [ + * { + * "clientId": "torange#1", + * "connectionId": "tt7233ghUa", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 167759566354 + * }, + * "location": null, + * "profileData": { + * "username": "Tara Orange", + * "avatar": "https://slides-internal.com/users/torange.png" + * } + * }, + * { + * "clientId": "amint#5", + * "connectionId": "hg35a4fgjAs", + * "isConnected": true, + * "lastEvent": { + * "name": "update", + * "timestamp": 173459567340 + * }, + * "location": null, + * "profileData": { + * "username": "Arit Mint", + * "avatar": "https://slides-internal.com/users/amint.png" + * } + * } + * ] + * ``` + * + * The following is an example of retrieving an array of all member objects, including the member themselves. Ths includes members that have recently left the space, but have not yet been removed. + * + * ```[javascript] + * const allMembers = await space.members.getAll(); + * ``` + * + * The following is an example payload returned by @space.members.getAll()@: + * + * ```[json] + * [ + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": false, + * "lastEvent": { + * "name": "enter", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * }, + * { + * "clientId": "amint#5", + * "connectionId": "hg35a4fgjAs", + * "isConnected": true, + * "lastEvent": { + * "name": "update", + * "timestamp": 173459567340 + * }, + * "location": null, + * "profileData": { + * "username": "Arit Mint", + * "avatar": "https://slides-internal.com/users/amint.png" + * } + * }, + * { + * "clientId": "torange#1", + * "connectionId": "tt7233ghUa", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 167759566354 + * }, + * "location": null, + * "profileData": { + * "username": "Tara Orange", + * "avatar": "https://slides-internal.com/users/torange.png" + * } + * } + * ] + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ async getAll(): Promise<SpaceMember[]> { const presenceMembers = await this.space.channel.presence.get(); const members = presenceMembers.map((m) => this.createMember(m)); return members.concat(this.leavers.getAll().map((l) => l.member)); } + /** + * <!-- This is to avoid duplication of the website documentation. --> + * See the documentation for {@link getAll}. + */ async getOthers(): Promise<SpaceMember[]> { const members = await this.getAll(); return members.filter((m) => m.connectionId !== this.space.connectionId); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L29-L103) --> + * Subscribe to members' online status and profile updates by registering a listener. Member events are emitted whenever a member "enters":/spaces/space#enter or "leaves":/spaces/space#leave the space, or updates their profile data. Use the @subscribe()@ method on the @members@ object of a space to receive updates. + * + * The following is an example of subscribing to the different member event types: + * + * ```[javascript] + * // Subscribe to member enters in a space + * space.members.subscribe('enter', (memberUpdate) => { + * console.log(memberUpdate); + * }); + * + * // Subscribe to member profile data updates in a space + * space.members.subscribe('update', (memberUpdate) => { + * console.log(memberUpdate); + * }); + * + * // Subscribe to member leaves in a space + * space.members.subscribe('leave', (memberUpdate) => { + * console.log(memberUpdate); + * }); + * + * // Subscribe to member removals in a space + * space.members.subscribe('remove', (memberUpdate) => { + * console.log(memberUpdate); + * }); + * ``` + * + * It's also possible to subscribe to multiple event types with the same listener by using an array: + * + * ```[javascript] + * space.members.subscribe(['enter', 'update'], (memberUpdate) => { + * console.log(memberUpdate); + * }); + * ``` + * + * Or subscribe to all event types: + * + * ```[javascript] + * space.members.subscribe((memberUpdate) => { + * console.log(memberUpdate); + * }); + * ``` + * + * The following is an example payload of a member event. The @lastEvent.name@ describes which "event type":#events a payload relates to. + * + * ```[json] + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * } + * ``` + * + * The following are the properties of a member event payload: + * + * |_. Property |_. Description |_. Type | + * | clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | connectionId | The unique identifier of the member's "connection":/connect. | String | + * | isConnected | Whether the member is connected to Ably or not. | Boolean | + * | profileData | The optional "profile data":#profile-data associated with the member. | Object | + * | location | The current "location":/spaces/locations of the member. Will be @null@ for @enter@, @leave@ and @remove@ events. | Object | + * | lastEvent.name | The most recent event emitted by the member. | String | + * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * + * <aside data-type='further-reading'> + * <p>Avatar stack subscription listeners only trigger on events related to members' online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, "space state":/spaces/space can be subscribed to which returns an array of all members with their latest state every time any event is triggered.</p> + * </aside> + * <!-- END WEBSITE DOCUMENTATION --> + */ subscribe<K extends keyof MembersEventMap>( eventOrEvents: K | K[], listener?: EventListener<MembersEventMap, K>, @@ -88,6 +330,29 @@ class Members extends EventEmitter<MembersEventMap> { } } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L107-L125) --> + * Unsubscribe from member events to remove previously registered listeners. + * + * The following is an example of removing a listener for one member event type: + * + * ```[javascript] + * space.members.unsubscribe('enter', listener); + * ``` + * + * It's also possible to remove listeners for multiple member event types: + * + * ```[javascript] + * space.members.unsubscribe(['enter', 'leave'], listener); + * ``` + * + * Or remove all listeners: + * + * ```[javascript] + * space.members.unsubscribe(); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ unsubscribe<K extends keyof MembersEventMap>( eventOrEvents: K | K[], listener?: EventListener<MembersEventMap, K>, diff --git a/src/Space.ts b/src/Space.ts index c3de9e56..6c734bf3 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -42,6 +42,24 @@ export interface SpaceState { export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileData; +/** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L9-L22) --> + * A space is a virtual area of your application in which realtime collaboration between users can take place. You can have any number of virtual spaces within an application, with a single space being anything from a web page, a sheet within a spreadsheet, an individual slide in a slideshow, or the entire slideshow itself. + * + * The following features can be implemented within a space: + * + * * "Avatar stack":/spaces/avatar + * * "Member location":/spaces/locations + * * "Live cursors":/spaces/cursors + * * "Component locking":/spaces/locking + * + * The @space@ namespace consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member's location within the application. The position of members' cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. + * + * Space state can be "subscribed":#subscribe to in the @space@ namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. + * + * To subscribe to any events in a space, you first need to create or retrieve a space. + * <!-- END WEBSITE DOCUMENTATION --> + */ class Space extends EventEmitter<SpaceEventMap> { /** * @internal @@ -123,6 +141,38 @@ class Space extends EventEmitter<SpaceEventMap> { this.emit('update', { members: await this.members.getAll() }); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L43-L55) --> + * Entering a space will register a client as a member and emit an "@enter@":/spaces/members#events event to all subscribers. Use the @enter()@ method to enter a space. + * + * Being entered into a space is required for members to: + * + * * Update their "profile data":#update-profile. + * * Set their "location":/spaces/locations. + * * Set their "cursor position":/spaces/cursors. + * + * The following is an example of entering a space: + * + * ```[javascript] + * await space.enter(); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L71-L82) --> + * Profile data can be set when "entering":#enter a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. + * + * Profile data is returned in the payload of all space events. + * + * The following is an example of setting profile data when entering a space: + * + * ```[javascript] + * await space.enter({ + * username: 'Claire Oranges', + * avatar: 'https://slides-internal.com/users/coranges.png', + * }); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ async enter(profileData: ProfileData = null): Promise<SpaceMember[]> { return new Promise((resolve) => { const presence = this.channel.presence; @@ -145,6 +195,28 @@ class Space extends EventEmitter<SpaceEventMap> { }); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L86-L103) --> + * Profile data can be updated at any point after entering a space by calling @updateProfileData()@. This will emit an @update@ event. If a client hasn't yet entered the space, @updateProfileData()@ will instead "enter the space":#enter, with the profile data, and emit an "@enter@":/spaces/members#events event. + * + * The following is an example of updating profile data: + * + * ```[javascript] + * space.updateProfileData({ + * username: 'Claire Lemons', + * avatar: 'https://slides-internal.com/users/clemons.png', + * }); + * ``` + * + * A function can be passed to @updateProfileData()@ in order to update a field based on the existing profile data: + * + * ```[javascript] + * space.updateProfileData(currentProfile => { + * return { ...currentProfile, username: 'Clara Lemons' } + * }); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ async updateProfileData(profileData: ProfileData): Promise<void>; async updateProfileData(updateFn: UpdateProfileDataFunction): Promise<void>; async updateProfileData(profileDataOrUpdateFn: ProfileData | UpdateProfileDataFunction): Promise<void> { @@ -172,6 +244,19 @@ class Space extends EventEmitter<SpaceEventMap> { } } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L59-L67) --> + * Leaving a space will emit a "@leave@":/spaces/members#events event to all subscribers. + * + * The following is an example of explicitly leaving a space: + * + * ```[javascript] + * await space.leave(); + * ``` + * + * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no @leave@ event will be emitted. + * <!-- END WEBSITE DOCUMENTATION --> + */ async leave(profileData: ProfileData = null) { const self = await this.members.getSelf(); @@ -192,11 +277,97 @@ class Space extends EventEmitter<SpaceEventMap> { await this.presenceLeave(data); } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L191-L197) --> + * The current state of the space can be retrieved in a one-off call. This will return an array of all @member@ objects currently in the space. This is a local call and retrieves the membership of the space retained in memory by the SDK. + * + * The following is an example of retrieving the current space state. Ths includes members that have recently left the space, but have not yet been removed: + * + * ```[javascript] + * const spaceState = await space.getState(); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ async getState(): Promise<SpaceState> { const members = await this.members.getAll(); return { members }; } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L107-L177) --> + * Subscribe to space state updates by registering a listener. Use the @subscribe()@ method on the @space@ object to receive updates. + * + * The following events will trigger a space event: + * + * * A member enters the space + * * A member leaves the space + * * A member is removed from the space state "after the offlineTimeout period":#options has elapsed + * * A member updates their profile data + * * A member sets a new location + * + * Space state contains a single object called @members@. Any events that trigger a change in space state will always return the current state of the space as an array of @member@ objects. + * + * <aside data-type='note'> + * <p>"Avatar stacks":/spaces/members and "member location":/spaces/locations events can be subscribed to on their individual namespaces; @space.members@ and @space.locations@. These events are filtered versions of space state events. Only a single "message":/channels/messages is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message.</p> + * <p>The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event.</p> + * </aside> + * + * The following is an example of subscribing to space events: + * + * ```[javascript] + * space.subscribe('update', (spaceState) => { + * console.log(spaceState.members); + * }); + * ``` + * + * The following is an example payload of a space event. + * + * ```[json] + * [ + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": false, + * "lastEvent": { + * "name": "leave", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * }, + * { + * "clientId": "amint#5", + * "connectionId": "hg35a4fgjAs", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 173459567340 + * }, + * "location": null, + * "profileData": { + * "username": "Arit Mint", + * "avatar": "https://slides-internal.com/users/amint.png" + * } + * }, + * ... + * ] + * ``` + * + * The following are the properties of an individual @member@ within a space event payload: + * + * |_. Property |_. Description |_. Type | + * | clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | connectionId | The unique identifier of the member's "connection":/connect. | String | + * | isConnected | Whether the member is connected to Ably or not. | Boolean | + * | profileData | The optional "profile data":#profile-data associated with the member. | Object | + * | location | The current "location":/spaces/locations of the member. | Object | + * | lastEvent.name | The most recent event emitted by the member. | String | + * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * <!-- END WEBSITE DOCUMENTATION --> + */ subscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap, K>): void; subscribe(listener?: EventListener<SpaceEventMap, keyof SpaceEventMap>): void; subscribe<K extends keyof SpaceEventMap>( @@ -216,6 +387,17 @@ class Space extends EventEmitter<SpaceEventMap> { } } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L181-L187) --> + * Unsubscribe from space events to remove previously registered listeners. + * + * The following is an example of removing a listener: + * + * ```[javascript] + * space.unsubscribe('update', listener); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + */ unsubscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap, K>): void; unsubscribe(listener?: EventListener<SpaceEventMap, keyof SpaceEventMap>): void; unsubscribe<K extends keyof SpaceEventMap>( diff --git a/src/Spaces.ts b/src/Spaces.ts index 4aec58a5..85983d7b 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -33,6 +33,89 @@ class Spaces { options.agents = { ...(options.agents ?? options.agents), ...agent }; } + /** + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L26-L39) --> + * A @space@ object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the @spaces@ collection using the @get()@ method. + * + * The following restrictions apply to space names: + * + * * Avoid starting names with @[@ or @:@ + * * Ensure names aren't empty + * * Exclude whitespace and wildcards, such as @*@ + * * Use the correct case, whether it be uppercase or lowercase + * + * The following is an example of creating a space: + * + * ```[javascript] + * const space = await spaces.get('board-presentation'); + * ``` + * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L199-L242) --> + * h2(#advanced). Advanced properties + * + * The following sections are only relevant if you want to further customize a space, or understand more about the Spaces SDK. They aren't required to get up and running with the basics. + * + * h3(#options). Space options + * + * An additional set of optional properties may be passed when "creating or retrieving":#create a space to customize the behavior of different features. + * + * The following properties can be customized: + * + * |_. Property |_. Description |_. Type | + * | offlineTimeout | Number of milliseconds after a member loses connection or closes their browser window to wait before they are removed from the member list. The default is 120,000ms (2 minutes). | Number | + * | cursors | A "cursor options":/spaces/cursors#options object for customizing live cursor behavior. | Object | + * | cursors.outboundBatchInterval | The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 100ms. | Number | + * | cursors.paginationLimit | The number of pages searched from history for the last published cursor position. The default is 5. | Number | + * + * The following is an example of customizing the space options when calling @spaces.get()@: + * + * ```[javascript] + * const space = await spaces.get('board-presentation', { + * offlineTimeout: 180_000, + * cursors: { paginationLimit: 10 } + * }); + * ``` + * + * h3(#foundations). Space foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * A space is created as an Ably "channel":/channels. Members "attach":/channels#attach to the channel and join its "presence set":/presence-occupancy/presence when they "enter":#enter the space. Avatar stacks, member locations and component locking are all handled on this channel. + * + * To manage the state of the space, you can monitor the "state of the underlying channel":/channels#states. The channel object can be accessed through @space.channel@. + * + * The following is an example of registering a listener to wait for a channel to become attached: + * + * ```[javascript] + * space.channel.on('attached', (stateChange) => { + * console.log(stateChange) + * }); + * ``` + * + * <aside data-type='note'> + * <p>Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own "channel":/channels.</p> + * </aside> + * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L108-L122) --> + * h2(#options). Cursor options + * + * Cursor options are set when creating or retrieving a @Space@ instance. They are used to control the behavior of live cursors. + * + * The following cursor options can be set: + * + * h3(#batch). outboundBatchInterval + * + * The @outboundBatchInterval@ is the interval at which a batch of cursor positions are published, in milliseconds, for each client. This is multiplied by the number of members in a space. + * + * The default value is 25ms which is optimal for the majority of use cases. If you wish to optimize the interval further, then decreasing the value will improve performance by further 'smoothing' the movement of cursors at the cost of increasing the number of events sent. Be aware that at a certain point the rate at which a browser is able to render the changes will impact optimizations. + * + * h3(#pagination). paginationLimit + * + * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members' cursor is obtained from "history":/storage-history/history. The @paginationLimit@ is the number of pages that should be searched to find the last position of each cursor. The default is 5. + * <!-- END WEBSITE DOCUMENTATION --> + */ async get(name: string, options?: Subset<SpaceOptions>): Promise<Space> { if (typeof name !== 'string' || name.length === 0) { throw ERR_SPACE_NAME_MISSING(); From a3cef5b3ba576f78ad25ce4edb1571ff5cd91cb8 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 20 Sep 2023 14:27:17 -0300 Subject: [PATCH 117/191] Add class-definitions.md documentation as TSDoc comments Copied all documentation from that document to appropriate places in code. --- src/Cursors.ts | 86 +++++++++++++++++++++++++++ src/Locations.ts | 76 ++++++++++++++++++++++++ src/Locks.ts | 126 +++++++++++++++++++++++++++++++++++++++ src/Members.ts | 123 ++++++++++++++++++++++++++++++++++++++ src/Space.ts | 65 +++++++++++++++++++++ src/Spaces.ts | 54 +++++++++++++++++ src/types.ts | 149 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 679 insertions(+) diff --git a/src/Cursors.ts b/src/Cursors.ts index 7219edae..41754042 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -41,6 +41,10 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * The channel is only created when a member calls @space.cursors.set()@. The live cursors channel object can be accessed through @space.cursors.channel@. To monitor the "underlying state of the cursors channel":/channels#states, the channel object can be accessed through @space.cursors.channel@. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Handles tracking of member cursors within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export default class Cursors extends EventEmitter<CursorsEventMap> { private readonly cursorBatching: CursorBatching; @@ -127,6 +131,24 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * | position.y | The position of the member's cursor on the Y-axis. | Number | * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Set the position of a cursor. If a member has not yet entered the space, this method will error. + * + * A event payload returned contains an object with 2 properties. `position` is an object with 2 required properties, `x` and `y`. These represent the position of the cursor on a 2D plane. A second optional property, `data` can also be passed. This is an object of any shape and is meant for data associated with the cursor movement (like drag or hover calculation results): + * + * ```ts + * type set = (update: { position: CursorPosition, data?: CursorData }) => void; + * ``` + * + * Example usage: + * + * ```ts + * window.addEventListener('mousemove', ({ clientX, clientY }) => { + * space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: "red" } }); + * }); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async set(cursor: Pick<CursorUpdate, 'position' | 'data'>) { const self = await this.space.members.getSelf(); @@ -198,6 +220,20 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * }); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Listen to `CursorUpdate` events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * + * Available events: + * + * - ##### **update** + * + * Emits an event when a new cursor position is set. The argument supplied to the event listener is a [CursorUpdate](#cursorupdate). + * + * ```ts + * space.cursors.subscribe('update', (cursorUpdate: CursorUpdate) => {}); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ subscribe<K extends keyof CursorsEventMap>( eventOrEvents: K | K[], @@ -245,6 +281,14 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * space.cursors.unsubscribe(); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * + * ```ts + * space.cursors.unsubscribe('update'); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ unsubscribe<K extends keyof CursorsEventMap>( eventOrEvents: K | K[], @@ -278,6 +322,20 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { /** * <!-- This is to avoid duplication of the website documentation. --> * See the documentation for {@link getAll}. + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get the last `CursorUpdate` object for self. + * + * ```ts + * type getSelf = () => Promise<CursorUpdate | null>; + * ``` + * + * Example: + * + * ```ts + * const selfPosition = await space.cursors.getSelf(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getSelf(): Promise<CursorUpdate | null> { const self = await this.space.members.getSelf(); @@ -290,6 +348,20 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { /** * <!-- This is to avoid duplication of the website documentation. --> * See the documentation for {@link getAll}. + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get the last `CursorUpdate` object for everyone else but yourself. + * + * ```ts + * type getOthers = () => Promise<Record<string, CursorUpdate | null>>; + * ``` + * + * Example: + * + * ```ts + * const otherPositions = await space.cursors.getOthers(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getOthers(): Promise<Record<string, null | CursorUpdate>> { const self = await this.space.members.getSelf(); @@ -390,6 +462,20 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * } * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get the last `CursorUpdate` object for all the members. + * + * ```ts + * type getAll = () => Promise<Record<string, CursorUpdate | null>>; + * ``` + * + * Example: + * + * ```ts + * const allLatestPositions = await space.cursors.getAll(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getAll(): Promise<Record<string, null | CursorUpdate>> { const channel = this.getChannel(); diff --git a/src/Locations.ts b/src/Locations.ts index c117ffe8..85e24b26 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -32,6 +32,10 @@ export interface LocationsEventMap { * * Member locations build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Handles the tracking of member locations within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export default class Locations extends EventEmitter<LocationsEventMap> { private lastLocationUpdate: Record<string, PresenceMember['data']['locationUpdate']['id']> = {}; @@ -84,6 +88,14 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * await space.locations.set({ slide: '3', component: 'slide-title' }); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Set your current location. The `location` argument can be any JSON-serializable object. Emits a `locationUpdate` event to all connected clients in this space. + * + * ```ts + * type set = (update: unknown) => Promise<void>; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async set(location: unknown) { const self = await this.space.members.getSelf(); @@ -162,6 +174,20 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * <p>Member location subscription listeners only trigger on events related to members' locations. Each event only contains the payload of the member that triggered it. Alternatively, "space state":/spaces/space can be subscribed to which returns an array of all members with their latest state every time any event is triggered.</p> * </aside> * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * + * Available events: + * + * - ##### **update** + * + * Fires when a member updates their location. The argument supplied to the event listener is an UpdateEvent. + * + * ```ts + * space.locations.subscribe('update', (locationUpdate: LocationsEvents.UpdateEvent) => {}); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ subscribe<K extends keyof LocationsEventMap>( eventOrEvents: K | K[], @@ -201,6 +227,14 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * space.locations.unsubscribe(); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * + * ```ts + * space.locations.unsubscribe('update'); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ unsubscribe<K extends keyof LocationsEventMap>( eventOrEvents: K | K[], @@ -227,6 +261,20 @@ export default class Locations extends EventEmitter<LocationsEventMap> { /** * <!-- This is to avoid duplication of the website documentation. --> * See the documentation for {@link getAll}. + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get location for self. + * + * ```ts + * type getSelf = () => Promise<unknown>; + * ``` + * + * Example: + * + * ```ts + * const myLocation = await space.locations.getSelf(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getSelf(): Promise<unknown> { const self = await this.space.members.getSelf(); @@ -236,6 +284,20 @@ export default class Locations extends EventEmitter<LocationsEventMap> { /** * <!-- This is to avoid duplication of the website documentation. --> * See the documentation for {@link getAll}. + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get location for other members + * + * ```ts + * type getOthers = () => Promise<Record<ConnectionId, unknown>>; + * ``` + * + * Example: + * + * ```ts + * const otherLocations = await space.locations.getOthers() + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getOthers(): Promise<Record<string, unknown>> { const members = await this.space.members.getOthers(); @@ -311,6 +373,20 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * } * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get location for all members. + * + * ```ts + * type getAll = () => Promise<Record<ConnectionId, unknown>>; + * ``` + * + * Example: + * + * ```ts + * const allLocations = await space.locations.getAll(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getAll(): Promise<Record<string, unknown>> { const members = await this.space.members.getAll(); diff --git a/src/Locks.ts b/src/Locks.ts index 34acb3be..88525a4c 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -8,6 +8,15 @@ import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from import SpaceUpdate from './SpaceUpdate.js'; +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Additional attributes that can be set when acquiring a lock. + * + * ```ts + * type LockAttributes = Map<string, string>; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export type LockAttributes = Record<string, unknown>; export interface LockOptions { @@ -50,6 +59,10 @@ export interface LocksEventMap { * * Only transitions that result in a @locked@ or @unlocked@ status will emit a lock event that members can "@subscribe()@":#subscribe to. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Provides a mechanism to "lock" a component, reducing the chances of conflict in an application whilst being edited by multiple members. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export default class Locks extends EventEmitter<LocksEventMap> { // locks tracks the local state of locks, which is used to determine whether @@ -91,6 +104,21 @@ export default class Locks extends EventEmitter<LocksEventMap> { * * If the lock is not currently held by a member, @get()@ will return @undefined@. Otherwise it will return the most recent lock event for the lock. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get a lock by its id. + * + * ```ts + * type get = (lockId: string) => Lock | undefined + * ``` + * + * Example: + * + * ```ts + * const id = "/slide/1/element/3"; + * const lock = space.locks.get(id); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ get(id: string): Lock | undefined { const locks = this.locks.get(id); @@ -159,6 +187,20 @@ export default class Locks extends EventEmitter<LocksEventMap> { * ] * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get all locks that have the `locked` status. + * + * ```ts + * type getAll = () => Promise<Lock[]> + * ``` + * + * Example: + * + * ```ts + * const locks = await space.locks.getAll(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getAll(): Promise<Lock[]> { const allLocks: Lock[] = []; @@ -177,6 +219,20 @@ export default class Locks extends EventEmitter<LocksEventMap> { /** * <!-- This is to avoid duplication of the website documentation. --> * See the documentation for {@link getAll}. + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get all locks belonging to self that have the `locked` status. + * + * ```ts + * type getSelf = () => Promise<Lock[]> + * ``` + * + * Example: + * + * ```ts + * const locks = await space.locks.getSelf(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getSelf(): Promise<Lock[]> { const self = await this.space.members.getSelf(); @@ -189,6 +245,20 @@ export default class Locks extends EventEmitter<LocksEventMap> { /** * <!-- This is to avoid duplication of the website documentation. --> * See the documentation for {@link getAll}. + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get all locks belonging to all members except self that have the `locked` status. + * + * ```ts + * type getOthers = () => Promise<Lock[]> + * ``` + * + * Example: + * + * ```ts + * const locks = await space.locks.getOthers(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getOthers(): Promise<Lock[]> { const self = await this.space.members.getSelf(); @@ -234,6 +304,23 @@ export default class Locks extends EventEmitter<LocksEventMap> { * * Once a member requests a lock by calling @acquire()@, the lock is temporarily in the "pending state":#states. An event will be emitted based on whether the lock request was successful (a status of @locked@) or invalidated (a status of @unlocked@). This can be "subscribed":#subscribe to in order for the client to know whether their lock request was successful or not. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Send a request to acquire a lock. Returns a Promise which resolves once the request has been sent. A resolved Promise holds a `pending` [Lock](#lock). An error will be thrown if a lock request with a status of `pending` or `locked` already exists, returning a rejected promise. + * + * When a lock acquisition by a member is confirmed with the `locked` status, an `update` event will be emitted. Hence to handle lock acquisition, `acquire()` needs to always be used together with `subscribe()`. + * + * ```ts + * type acquire = (lockId: string) => Promise<Lock>; + * ``` + * + * Example: + * + * ```ts + * const id = "/slide/1/element/3"; + * const lockRequest = await space.locks.acquire(id); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async acquire(id: string, opts?: LockOptions): Promise<Lock> { const self = await this.space.members.getSelf(); @@ -284,6 +371,21 @@ export default class Locks extends EventEmitter<LocksEventMap> { * <p>When a member "leaves":/spaces/space#leave a space, their locks are automatically released.</p> * </aside> * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Releases a previously requested lock. + * + * ```ts + * type release = (lockId: string) => Promise<void>; + * ``` + * + * Example: + * + * ```ts + * const id = "/slide/1/element/3"; + * await space.locks.release(id); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async release(id: string): Promise<void> { const self = await this.space.members.getSelf(); @@ -366,6 +468,22 @@ export default class Locks extends EventEmitter<LocksEventMap> { * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Listen to lock events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * + * Available events: + * + * - ##### **update** + * + * Listen to changes to locks. + * + * ```ts + * space.locks.subscribe('update', (lock: Lock) => {}) + * ``` + * + * The argument supplied to the callback is a [Lock](#lock), representing the lock request and it's status. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ subscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; subscribe(listener?: EventListener<LocksEventMap, keyof LocksEventMap>): void; @@ -402,6 +520,14 @@ export default class Locks extends EventEmitter<LocksEventMap> { * space.locks.unsubscribe(); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * + * ```ts + * space.locks.unsubscribe('update'); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ unsubscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; unsubscribe(listener?: EventListener<LocksEventMap, keyof LocksEventMap>): void; diff --git a/src/Members.ts b/src/Members.ts index 4d6f5f4d..896cee99 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -41,6 +41,10 @@ export interface MembersEventMap { * * Avatar stacks build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Handles members within a space. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ class Members extends EventEmitter<MembersEventMap> { private lastMemberUpdate: Record<string, PresenceMember['data']['profileUpdate']['id']> = {}; @@ -83,6 +87,20 @@ class Members extends EventEmitter<MembersEventMap> { /** * <!-- This is to avoid duplication of the website documentation. --> * See the documentation for {@link getAll}. + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `null` if the client hasn't entered the space yet. + * + * ```ts + * type getSelf = () => Promise<SpaceMember | null>; + * ``` + * + * Example: + * + * ```ts + * const myMember = await space.members.getSelf(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getSelf(): Promise<SpaceMember | null> { return this.space.connectionId ? await this.getByConnectionId(this.space.connectionId) : null; @@ -213,6 +231,20 @@ class Members extends EventEmitter<MembersEventMap> { * ] * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, including any who have left and not yet timed out. (_see: [offlineTimeout](#spaceoptions)_) + * + * ```ts + * type getAll = () => Promise<SpaceMember[]>; + * ``` + * + * Example: + * + * ```ts + * const allMembers = await space.members.getAll(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getAll(): Promise<SpaceMember[]> { const presenceMembers = await this.space.channel.presence.get(); @@ -223,6 +255,20 @@ class Members extends EventEmitter<MembersEventMap> { /** * <!-- This is to avoid duplication of the website documentation. --> * See the documentation for {@link getAll}. + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, excluding your own member object. + * + * ```ts + * type getSelf = () => Promise<SpaceMember[]>; + * ``` + * + * Example: + * + * ```ts + * const otherMembers = await space.members.getOthers(); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async getOthers(): Promise<SpaceMember[]> { const members = await this.getAll(); @@ -307,6 +353,68 @@ class Members extends EventEmitter<MembersEventMap> { * <p>Avatar stack subscription listeners only trigger on events related to members' online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, "space state":/spaces/space can be subscribed to which returns an array of all members with their latest state every time any event is triggered.</p> * </aside> * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * + * The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event. + * + * Example: + * + * ```ts + * space.members.subscribe((member: SpaceMember) => {}); + * ``` + * + * Available events: + * + * - ##### **enter** + * + * Listen to enter events of members. + * + * ```ts + * space.members.subscribe('enter', (member: SpaceMember) => {}) + * ``` + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. + * + * - ##### **leave** + * + * Listen to leave events of members. The leave event will be issued when a member calls `space.leave()` or is disconnected. + * + * ```ts + * space.members.subscribe('leave', (member: SpaceMember) => {}) + * ``` + * + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member leaving the space. + * + * - ##### **remove** + * + * Listen to remove events of members. The remove event will be triggered when the [offlineTimeout](#spaceoptions) has passed. + * + * ```ts + * space.members.subscribe('remove', (member: SpaceMember) => {}) + * ``` + * + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member removed from the space. + * + * - ##### **updateProfile** + * + * Listen to profile update events of members. + * + * ```ts + * space.members.subscribe('updateProfile', (member: SpaceMember) => {}) + * ``` + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. + * + * - ##### **update** + * + * Listen to `enter`, `leave`, `updateProfile` and `remove` events. + * + * ```ts + * space.members.subscribe('update', (member: SpaceMember) => {}) + * ``` + * + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member affected by the change. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ subscribe<K extends keyof MembersEventMap>( eventOrEvents: K | K[], @@ -352,6 +460,21 @@ class Members extends EventEmitter<MembersEventMap> { * space.members.unsubscribe(); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Remove all the event listeners or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * + * ```ts + * // Unsubscribe from all events + * space.members.unsubscribe(); + * + * // Unsubscribe from enter events + * space.members.unsubscribe('enter'); + * + * // Unsubscribe from leave events + * space.members.unsubscribe('leave'); + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ unsubscribe<K extends keyof MembersEventMap>( eventOrEvents: K | K[], diff --git a/src/Space.ts b/src/Space.ts index 6c734bf3..a00c59c4 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -59,6 +59,10 @@ export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileDat * * To subscribe to any events in a space, you first need to create or retrieve a space. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * An instance of a Space created using [spaces.get](#get). Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ class Space extends EventEmitter<SpaceEventMap> { /** @@ -71,8 +75,35 @@ class Space extends EventEmitter<SpaceEventMap> { */ readonly connectionId: string | undefined; readonly options: SpaceOptions; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * An instance of [Locations](#locations). + * + * ```ts + * type locations = instanceof Locations; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ readonly locations: Locations; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * An instance of [Cursors](#cursors). + * + * ```ts + * type cursors = instanceof Cursors; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ readonly cursors: Cursors; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * An instance of [Members](#members). + * + * ```ts + * type members = instanceof Members; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ readonly members: Members; readonly channel: Types.RealtimeChannelPromise; readonly locks: Locks; @@ -172,6 +203,14 @@ class Space extends EventEmitter<SpaceEventMap> { * }); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the [member object](#spacemember). Returns all current space members. + * + * ```ts + * type enter = (profileData?: Record<string, unknown>) => Promise<SpaceMember[]>; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async enter(profileData: ProfileData = null): Promise<SpaceMember[]> { return new Promise((resolve) => { @@ -216,6 +255,24 @@ class Space extends EventEmitter<SpaceEventMap> { * }); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Update `profileData`. This data can be an arbitrary JSON-serializable object which is attached to the [member object](#spacemember). If the connection + * has not entered the space, calling `updateProfileData` will call `enter` instead. + * + * ```ts + * type updateProfileData = (profileDataOrUpdateFn?: unknown| (unknown) => unknown) => Promise<void>; + * ``` + * + * A function can also be passed in. This function will receive the existing `profileData` and lets you update based on the existing value of `profileData`: + * + * ```ts + * await space.updateProfileData((oldProfileData) => { + * const newProfileData = getNewProfileData(); + * return { ...oldProfileData, ...newProfileData }; + * }) + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async updateProfileData(profileData: ProfileData): Promise<void>; async updateProfileData(updateFn: UpdateProfileDataFunction): Promise<void>; @@ -256,6 +313,14 @@ class Space extends EventEmitter<SpaceEventMap> { * * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no @leave@ event will be emitted. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See [offlineTimeout](#spaceoptions). + * + * ```ts + * type leave = (profileData?: Record<string, unknown>) => Promise<void>; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async leave(profileData: ProfileData = null) { const self = await this.members.getSelf(); diff --git a/src/Spaces.ts b/src/Spaces.ts index 85983d7b..4c902dee 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -16,11 +16,57 @@ export interface ClientWithOptions extends Types.RealtimePromise { class Spaces { private spaces: Record<string, Space> = {}; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Instance of the [ably-js](https://github.com/ably/ably-js) client that was passed to the [constructor](#constructor). + * + * ```ts + * type client = Ably.RealtimePromise; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ client: Types.RealtimePromise; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Instance of the [ably-js](https://github.com/ably/ably-js) connection, belonging to the client that was passed to the [constructor](#constructor). + * + * ```ts + * type connection = Ably.ConnectionPromise; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ connection: Types.ConnectionPromise; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Version of the Spaces library. + * + * ```ts + * type version = string; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ readonly version = VERSION; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Create a new instance of the Space SDK by passing an instance of the realtime, promise-based [Ably client](https://github.com/ably/ably-js): + * + * ```ts + * import { Realtime } from 'ably/promise'; + * import Spaces from '@ably/spaces'; + * + * const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); + * const spaces = new Spaces(client); + * ``` + * + * Please note that a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascript) is required. + * + * An API key will required for [basic authentication](https://ably.com/docs/auth/basic?lang=javascript). We strongly recommended that you use [token authentication](https://ably.com/docs/realtime/authentication#token-authentication) in any production environments. + * + * Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/setup?lang=javascript) for information on setting up a realtime promise client. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ constructor(client: Types.RealtimePromise) { this.client = client; this.connection = client.connection; @@ -115,6 +161,14 @@ class Spaces { * * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members' cursor is obtained from "history":/storage-history/history. The @paginationLimit@ is the number of pages that should be searched to find the last position of each cursor. The default is 5. * <!-- END WEBSITE DOCUMENTATION --> + * + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Get or create a Space instance. Returns a [Space](#space) instance. Configure the space by passing [SpaceOptions](#spaceoptions) as the second argument. + * + * ```ts + * type get = (name: string, options?: SpaceOptions) => Promise<Space>; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async get(name: string, options?: Subset<SpaceOptions>): Promise<Space> { if (typeof name !== 'string' || name.length === 0) { diff --git a/src/types.ts b/src/types.ts index 96a16ba7..b41eb136 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,18 +1,74 @@ import { Types } from 'ably'; import type { LockAttributes } from './Locks.js'; +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * ```ts + * type CursorsOptions = { + * outboundBatchInterval?: number; + * paginationLimit?: number; + * }; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export interface CursorsOptions { + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * The interval in milliseconds at which a batch of cursor positions are published. This is multiplied by the number of members in the space minus 1. The default value is 25ms. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ outboundBatchInterval: number; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * The number of pages searched from [history](https://ably.com/docs/storage-history/history) for the last published cursor position. The default is 5. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ paginationLimit: number; } +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Represents a cursors position. + * + * ```ts + * type CursorPosition = { + * x: number; + * y: number; + * }; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export interface CursorPosition { x: number; y: number; } +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Represent data that can be associated with a cursor update. + * + * ```ts + * type CursorData = Record<string, unknown>; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export type CursorData = Record<string, unknown>; +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Represents an update to a cursor. + * + * ```ts + * type CursorUpdate = { + * name: string; + * clientId: string; + * connectionId: string; + * position: CursorPosition; + * data?: CursorData; + * }; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export interface CursorUpdate { clientId: string; connectionId: string; @@ -20,19 +76,87 @@ export interface CursorUpdate { data?: CursorData; } +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Used to configure a Space instance on creation. + * + * ```ts + * type SpaceOptions = { + * offlineTimeout?: number; + * cursors?: CursorsOptions; + * }; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export interface SpaceOptions { + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Number of milliseconds after a user loses connection or closes their browser window to wait before their [SpaceMember](#spacemember) object is removed from the members list. The default is 120000ms (2 minutes). + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ offlineTimeout: number; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Options relating to configuring the cursors API (see below). + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ cursors: CursorsOptions; } export type ProfileData = Record<string, unknown> | null; +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * A SpaceMember represents a member within a Space instance. Each new connection that enters will create a new member, even if they have the same [`clientId`](https://ably.com/docs/auth/identified-clients?lang=javascript). + * + * ```ts + * type SpaceMember = { + * clientId: string; + * connectionId: string; + * isConnected: boolean; + * profileData: Record<string, unknown>; + * location: Location; + * lastEvent: PresenceEvent; + * }; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export interface SpaceMember { + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * The client identifier for the user, provided to the ably client instance. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ clientId: string; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Identifier for the connection used by the user. This is a unique identifier. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ connectionId: string; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Whether the user is connected to Ably. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ isConnected: boolean; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Optional user data that can be attached to a user, such as a username or image to display in an avatar stack. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ profileData: ProfileData; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * The current location of the user within the space. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ location: unknown; + /** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * The most recent event emitted by [presence](https://ably.com/docs/presence-occupancy/presence?lang=javascript) and its timestamp. Events will be either `enter`, `leave`, `update` or `present`. + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ lastEvent: { name: Types.PresenceAction; timestamp: number; @@ -45,8 +169,33 @@ export namespace LockStatuses { export type Unlocked = 'unlocked'; } +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Represents a status of a lock. + * + * ```ts + * type LockStatus = 'pending' | 'locked' | 'unlocked'; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatuses.Unlocked; +/** + * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> + * Represents a Lock. + * + * ```ts + * type Lock = { + * id: string; + * status: LockStatus; + * member: SpaceMember; + * timestamp: number; + * attributes?: LockAttributes; + * reason?: Types.ErrorInfo; + * }; + * ``` + * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + */ export type Lock = { id: string; status: LockStatus; From a231fdbf1e54f586f0f34ddef466b8b8436bef3b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 3 Oct 2023 15:09:39 -0300 Subject: [PATCH 118/191] Remove redundant documentation added in 95ed746 Remove all of the stuff that is (or aims to be) a declaration that already exists in the source code. --- src/Cursors.ts | 16 ------------ src/Locations.ts | 16 ------------ src/Locks.ts | 28 --------------------- src/Members.ts | 12 --------- src/Space.ts | 24 ------------------ src/Spaces.ts | 24 ------------------ src/types.ts | 64 ------------------------------------------------ 7 files changed, 184 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 41754042..4c3a94d1 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -137,10 +137,6 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * * A event payload returned contains an object with 2 properties. `position` is an object with 2 required properties, `x` and `y`. These represent the position of the cursor on a 2D plane. A second optional property, `data` can also be passed. This is an object of any shape and is meant for data associated with the cursor movement (like drag or hover calculation results): * - * ```ts - * type set = (update: { position: CursorPosition, data?: CursorData }) => void; - * ``` - * * Example usage: * * ```ts @@ -326,10 +322,6 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get the last `CursorUpdate` object for self. * - * ```ts - * type getSelf = () => Promise<CursorUpdate | null>; - * ``` - * * Example: * * ```ts @@ -352,10 +344,6 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get the last `CursorUpdate` object for everyone else but yourself. * - * ```ts - * type getOthers = () => Promise<Record<string, CursorUpdate | null>>; - * ``` - * * Example: * * ```ts @@ -466,10 +454,6 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get the last `CursorUpdate` object for all the members. * - * ```ts - * type getAll = () => Promise<Record<string, CursorUpdate | null>>; - * ``` - * * Example: * * ```ts diff --git a/src/Locations.ts b/src/Locations.ts index 85e24b26..b644867c 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -91,10 +91,6 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Set your current location. The `location` argument can be any JSON-serializable object. Emits a `locationUpdate` event to all connected clients in this space. - * - * ```ts - * type set = (update: unknown) => Promise<void>; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async set(location: unknown) { @@ -265,10 +261,6 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get location for self. * - * ```ts - * type getSelf = () => Promise<unknown>; - * ``` - * * Example: * * ```ts @@ -288,10 +280,6 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get location for other members * - * ```ts - * type getOthers = () => Promise<Record<ConnectionId, unknown>>; - * ``` - * * Example: * * ```ts @@ -377,10 +365,6 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get location for all members. * - * ```ts - * type getAll = () => Promise<Record<ConnectionId, unknown>>; - * ``` - * * Example: * * ```ts diff --git a/src/Locks.ts b/src/Locks.ts index 88525a4c..a7b450d8 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -11,10 +11,6 @@ import SpaceUpdate from './SpaceUpdate.js'; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Additional attributes that can be set when acquiring a lock. - * - * ```ts - * type LockAttributes = Map<string, string>; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export type LockAttributes = Record<string, unknown>; @@ -108,10 +104,6 @@ export default class Locks extends EventEmitter<LocksEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get a lock by its id. * - * ```ts - * type get = (lockId: string) => Lock | undefined - * ``` - * * Example: * * ```ts @@ -191,10 +183,6 @@ export default class Locks extends EventEmitter<LocksEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get all locks that have the `locked` status. * - * ```ts - * type getAll = () => Promise<Lock[]> - * ``` - * * Example: * * ```ts @@ -223,10 +211,6 @@ export default class Locks extends EventEmitter<LocksEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get all locks belonging to self that have the `locked` status. * - * ```ts - * type getSelf = () => Promise<Lock[]> - * ``` - * * Example: * * ```ts @@ -249,10 +233,6 @@ export default class Locks extends EventEmitter<LocksEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get all locks belonging to all members except self that have the `locked` status. * - * ```ts - * type getOthers = () => Promise<Lock[]> - * ``` - * * Example: * * ```ts @@ -310,10 +290,6 @@ export default class Locks extends EventEmitter<LocksEventMap> { * * When a lock acquisition by a member is confirmed with the `locked` status, an `update` event will be emitted. Hence to handle lock acquisition, `acquire()` needs to always be used together with `subscribe()`. * - * ```ts - * type acquire = (lockId: string) => Promise<Lock>; - * ``` - * * Example: * * ```ts @@ -375,10 +351,6 @@ export default class Locks extends EventEmitter<LocksEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Releases a previously requested lock. * - * ```ts - * type release = (lockId: string) => Promise<void>; - * ``` - * * Example: * * ```ts diff --git a/src/Members.ts b/src/Members.ts index 896cee99..813e1cf8 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -91,10 +91,6 @@ class Members extends EventEmitter<MembersEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `null` if the client hasn't entered the space yet. * - * ```ts - * type getSelf = () => Promise<SpaceMember | null>; - * ``` - * * Example: * * ```ts @@ -235,10 +231,6 @@ class Members extends EventEmitter<MembersEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, including any who have left and not yet timed out. (_see: [offlineTimeout](#spaceoptions)_) * - * ```ts - * type getAll = () => Promise<SpaceMember[]>; - * ``` - * * Example: * * ```ts @@ -259,10 +251,6 @@ class Members extends EventEmitter<MembersEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, excluding your own member object. * - * ```ts - * type getSelf = () => Promise<SpaceMember[]>; - * ``` - * * Example: * * ```ts diff --git a/src/Space.ts b/src/Space.ts index a00c59c4..3098d72c 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -78,30 +78,18 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * An instance of [Locations](#locations). - * - * ```ts - * type locations = instanceof Locations; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ readonly locations: Locations; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * An instance of [Cursors](#cursors). - * - * ```ts - * type cursors = instanceof Cursors; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ readonly cursors: Cursors; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * An instance of [Members](#members). - * - * ```ts - * type members = instanceof Members; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ readonly members: Members; @@ -206,10 +194,6 @@ class Space extends EventEmitter<SpaceEventMap> { * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the [member object](#spacemember). Returns all current space members. - * - * ```ts - * type enter = (profileData?: Record<string, unknown>) => Promise<SpaceMember[]>; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async enter(profileData: ProfileData = null): Promise<SpaceMember[]> { @@ -260,10 +244,6 @@ class Space extends EventEmitter<SpaceEventMap> { * Update `profileData`. This data can be an arbitrary JSON-serializable object which is attached to the [member object](#spacemember). If the connection * has not entered the space, calling `updateProfileData` will call `enter` instead. * - * ```ts - * type updateProfileData = (profileDataOrUpdateFn?: unknown| (unknown) => unknown) => Promise<void>; - * ``` - * * A function can also be passed in. This function will receive the existing `profileData` and lets you update based on the existing value of `profileData`: * * ```ts @@ -316,10 +296,6 @@ class Space extends EventEmitter<SpaceEventMap> { * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See [offlineTimeout](#spaceoptions). - * - * ```ts - * type leave = (profileData?: Record<string, unknown>) => Promise<void>; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async leave(profileData: ProfileData = null) { diff --git a/src/Spaces.ts b/src/Spaces.ts index 4c902dee..f16cbe11 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -19,20 +19,12 @@ class Spaces { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Instance of the [ably-js](https://github.com/ably/ably-js) client that was passed to the [constructor](#constructor). - * - * ```ts - * type client = Ably.RealtimePromise; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ client: Types.RealtimePromise; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Instance of the [ably-js](https://github.com/ably/ably-js) connection, belonging to the client that was passed to the [constructor](#constructor). - * - * ```ts - * type connection = Ably.ConnectionPromise; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ connection: Types.ConnectionPromise; @@ -40,10 +32,6 @@ class Spaces { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Version of the Spaces library. - * - * ```ts - * type version = string; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ readonly version = VERSION; @@ -52,14 +40,6 @@ class Spaces { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Create a new instance of the Space SDK by passing an instance of the realtime, promise-based [Ably client](https://github.com/ably/ably-js): * - * ```ts - * import { Realtime } from 'ably/promise'; - * import Spaces from '@ably/spaces'; - * - * const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); - * const spaces = new Spaces(client); - * ``` - * * Please note that a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascript) is required. * * An API key will required for [basic authentication](https://ably.com/docs/auth/basic?lang=javascript). We strongly recommended that you use [token authentication](https://ably.com/docs/realtime/authentication#token-authentication) in any production environments. @@ -164,10 +144,6 @@ class Spaces { * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get or create a Space instance. Returns a [Space](#space) instance. Configure the space by passing [SpaceOptions](#spaceoptions) as the second argument. - * - * ```ts - * type get = (name: string, options?: SpaceOptions) => Promise<Space>; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async get(name: string, options?: Subset<SpaceOptions>): Promise<Space> { diff --git a/src/types.ts b/src/types.ts index b41eb136..6e4e40f9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,16 +1,6 @@ import { Types } from 'ably'; import type { LockAttributes } from './Locks.js'; -/** - * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * ```ts - * type CursorsOptions = { - * outboundBatchInterval?: number; - * paginationLimit?: number; - * }; - * ``` - * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> - */ export interface CursorsOptions { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -29,13 +19,6 @@ export interface CursorsOptions { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Represents a cursors position. - * - * ```ts - * type CursorPosition = { - * x: number; - * y: number; - * }; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export interface CursorPosition { @@ -46,10 +29,6 @@ export interface CursorPosition { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Represent data that can be associated with a cursor update. - * - * ```ts - * type CursorData = Record<string, unknown>; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export type CursorData = Record<string, unknown>; @@ -57,16 +36,6 @@ export type CursorData = Record<string, unknown>; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Represents an update to a cursor. - * - * ```ts - * type CursorUpdate = { - * name: string; - * clientId: string; - * connectionId: string; - * position: CursorPosition; - * data?: CursorData; - * }; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export interface CursorUpdate { @@ -79,13 +48,6 @@ export interface CursorUpdate { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Used to configure a Space instance on creation. - * - * ```ts - * type SpaceOptions = { - * offlineTimeout?: number; - * cursors?: CursorsOptions; - * }; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export interface SpaceOptions { @@ -108,17 +70,6 @@ export type ProfileData = Record<string, unknown> | null; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * A SpaceMember represents a member within a Space instance. Each new connection that enters will create a new member, even if they have the same [`clientId`](https://ably.com/docs/auth/identified-clients?lang=javascript). - * - * ```ts - * type SpaceMember = { - * clientId: string; - * connectionId: string; - * isConnected: boolean; - * profileData: Record<string, unknown>; - * location: Location; - * lastEvent: PresenceEvent; - * }; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export interface SpaceMember { @@ -172,10 +123,6 @@ export namespace LockStatuses { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Represents a status of a lock. - * - * ```ts - * type LockStatus = 'pending' | 'locked' | 'unlocked'; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatuses.Unlocked; @@ -183,17 +130,6 @@ export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatus /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Represents a Lock. - * - * ```ts - * type Lock = { - * id: string; - * status: LockStatus; - * member: SpaceMember; - * timestamp: number; - * attributes?: LockAttributes; - * reason?: Types.ErrorInfo; - * }; - * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export type Lock = { From bde56c944728faf2d80ba78f40ed21e970f4b2f7 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 21 Sep 2023 16:47:02 -0300 Subject: [PATCH 119/191] Split </p> from end of link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In an upcoming commit, I’m going to use Pandoc to convert the website documentation from Textile to Markdown. However, Pandoc’s parser (incorrectly, I think) thinks that the <p> tag belongs to the link’s URL, so split it up. --- src/Spaces.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Spaces.ts b/src/Spaces.ts index f16cbe11..9cceaff3 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -120,7 +120,9 @@ class Spaces { * ``` * * <aside data-type='note'> - * <p>Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own "channel":/channels.</p> + * <p> + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own "channel":/channels. + * </p> * </aside> * <!-- END WEBSITE DOCUMENTATION --> * From f0149ad9a757e12c84a8a5ea2b3319a9768d70e8 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 26 Sep 2023 16:07:27 -0300 Subject: [PATCH 120/191] Add a script to convert website documentation from Textile to Markdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’ll use this very shortly. --- package.json | 4 +- scripts/convert_website_documentation.ts | 183 +++++++++++++++++++++++ 2 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 scripts/convert_website_documentation.ts diff --git a/package.json b/package.json index 7c7b325c..6df6858b 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write --ignore-path .gitignore src demo", - "format:check": "prettier --check --ignore-path .gitignore src demo", + "format": "prettier --write --ignore-path .gitignore src demo scripts", + "format:check": "prettier --check --ignore-path .gitignore src demo scripts", "test": "vitest run", "test:watch": "vitest watch", "coverage": "vitest run --coverage", diff --git a/scripts/convert_website_documentation.ts b/scripts/convert_website_documentation.ts new file mode 100644 index 00000000..a79e4807 --- /dev/null +++ b/scripts/convert_website_documentation.ts @@ -0,0 +1,183 @@ +import { readdir, lstat, readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { execFile } from 'node:child_process'; + +async function processDirectory(path: string) { + const entries = await readdir(path); + + for (const entry of entries) { + const pathToEntry = join(path, entry); + + const stat = await lstat(pathToEntry); + if (stat.isDirectory()) { + await processDirectory(pathToEntry); + continue; + } + + if (entry.endsWith('.ts')) { + await processFile(pathToEntry); + } + } +} + +async function processFile(path: string) { + console.log(`Processing: ${path}`); + const contents = await readFile(path, { encoding: 'utf-8' }); + + const beginWebsiteDocumentationRegexp = /<!-- BEGIN WEBSITE DOCUMENTATION \(.*\) -->/g; + const endWebsiteDocumentationRegexp = /<!-- END WEBSITE DOCUMENTATION -->/g; + + const lines = contents.split('\n'); + + const startLineIndices: number[] = []; + const endLineIndices: number[] = []; + + for (const [lineIndex, line] of lines.entries()) { + if (beginWebsiteDocumentationRegexp.exec(line)) { + startLineIndices.push(lineIndex); + } + if (endWebsiteDocumentationRegexp.exec(line)) { + endLineIndices.push(lineIndex); + } + } + + for (let documentationIndex = 0; documentationIndex < startLineIndices.length; documentationIndex++) { + const startLineIndex = startLineIndices[documentationIndex]; + const endLineIndex = endLineIndices[documentationIndex]; + + const documentation = lines.slice(startLineIndex + 1, endLineIndex).join('\n'); + const documentationLineCount = endLineIndex - startLineIndex - 1; + + // Convert the documentation comment. + const converted = await convertWebsiteDocumentationCommentFragment(documentation); + + // Replace the documentation comment in `lines`. + const convertedLines = converted.split('\n'); + lines.splice(startLineIndex + 1, documentationLineCount, ...convertedLines); + + const addedLinesCount = convertedLines.length - documentationLineCount; + + // Shift the line indices to reflect the length of the converted documentation comment. + for ( + let indexOfDocumentationToShiftLineNumbers = documentationIndex + 1; + indexOfDocumentationToShiftLineNumbers < startLineIndices.length; + indexOfDocumentationToShiftLineNumbers++ + ) { + startLineIndices[indexOfDocumentationToShiftLineNumbers] += addedLinesCount; + endLineIndices[indexOfDocumentationToShiftLineNumbers] += addedLinesCount; + } + } + + // Write the new contents of the file. + const newContents = lines.join('\n'); + await writeFile(path, newContents, { encoding: 'utf-8' }); +} + +async function convertWebsiteDocumentationCommentFragment(commentFragment: string) { + const prefixStrippingResult = strippingPrefixOfCommentFragment(commentFragment); + const tagged = tagCommentFragmentLines(prefixStrippingResult.content); + + const lines: string[] = []; + + for (const taggedLines of tagged) { + switch (taggedLines.type) { + case 'textile': + lines.push(...(await convertTextileLines(taggedLines.lines))); + break; + case 'codeBlock': + lines.push(...convertCodeBlockLines(taggedLines.lines)); + break; + } + } + + return restoringPrefixOfCommentFragment(lines.join('\n'), prefixStrippingResult.prefix); +} + +async function convertTextileLines(textileLines: string[]) { + const pandocStdoutPromise = new Promise<string>((resolve, reject) => { + const childProcess = execFile( + '/opt/homebrew/bin/pandoc', + // We choose gfm over commonmark for tables support. + ['--from', 'textile', '--to', 'gfm', '--wrap=preserve'], + (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }, + ); + + // I don’t fully understand how writing works and whether this always succeeds in writing the full thing; keep an eye out for any weirdness and revisit if necessary. + childProcess.stdin!.write(textileLines.join('\n')); + childProcess.stdin!.end(); + }); + + const pandocStdout = await pandocStdoutPromise; + + return pandocStdout.split('\n'); +} + +function convertCodeBlockLines(codeBlockLines: string[]) { + // remove square brackets from language tag + const firstLine = codeBlockLines[0].replace(/[[\]]/g, ''); + return [firstLine, ...codeBlockLines.slice(1)]; +} + +type TaggedLines = { type: 'textile' | 'codeBlock'; lines: string[] }; + +function tagCommentFragmentLines(commentFragment: string) { + const lines = commentFragment.split('\n'); + + const result: TaggedLines[] = []; + + let current: TaggedLines | null = null; + + for (const line of lines) { + if (line.startsWith('```')) { + if (current && current.type === 'codeBlock') { + // end of code block + current.lines.push(line); + result.push(current); + current = null; + } else { + if (current) { + result.push(current); + } + + // start of code block + current = { type: 'codeBlock', lines: [line] }; + } + } else { + if (current) { + current.lines.push(line); + } else { + current = { type: 'textile', lines: [line] }; + } + } + } + + if (current) { + result.push(current); + current = null; + } + + return result; +} + +function strippingPrefixOfCommentFragment(commentFragment: string) { + const lines = commentFragment.split('\n'); + const prefix = /\s+\* /g.exec(lines[0])![0]; + const newLines = lines.map((line) => line.substring(prefix.length)); + + return { content: newLines.join('\n'), prefix }; +} + +function restoringPrefixOfCommentFragment(content: string, prefix: string) { + const lines = content.split('\n'); + const newLines = lines.map((line) => `${prefix}${line}`); + + return newLines.join('\n'); +} + +processDirectory('src'); From 2d198b493eb3d092eeb04ccfe1a3ceeb27febff4 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 20 Sep 2023 17:31:17 -0300 Subject: [PATCH 121/191] Convert website documentation from Textile to Markdown Ran `npx ts-node scripts/convert_website_documentation.ts && npm run format`. --- src/Cursors.ts | 135 ++++++++++++++++++++++++++++------------------- src/Locations.ts | 94 +++++++++++++++++---------------- src/Locks.ts | 128 +++++++++++++++++++++++--------------------- src/Members.ts | 116 +++++++++++++++++++++------------------- src/Space.ts | 100 +++++++++++++++++++---------------- src/Spaces.ts | 67 ++++++++++++----------- 6 files changed, 350 insertions(+), 290 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 4c3a94d1..e901fe64 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -23,23 +23,29 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * Cursor events are emitted whenever a member moves their mouse within a space. In order to optimize the efficiency and frequency of updates, cursor position events are automatically batched. The batching interval may be customized in order to further optimize for increased performance versus the number of events published. * - * Live cursor updates are not available as part of the "space state":/spaces/space#subscribe and must be subscribed to using "@space.cursors.subscribe()@":#subscribe. + * Live cursor updates are not available as part of the [space state](/spaces/space#subscribe) and must be subscribed to using [`space.cursors.subscribe()`](#subscribe). * * <aside data-type='important'> - * <p>Live cursors are a great way of providing contextual awareness as to what members are looking at within an application. However, too many cursors moving across a page can often be a distraction rather than an enhancement. As such, Ably recommends a maximum of 20 members simultaneously streaming their cursors in a space at any one time for an optimal end-user experience.</p> + * <p> + * + * Live cursors are a great way of providing contextual awareness as to what members are looking at within an application. However, too many cursors moving across a page can often be a distraction rather than an enhancement. As such, Ably recommends a maximum of 20 members simultaneously streaming their cursors in a space at any one time for an optimal end-user experience. + * + * </p> * </aside> + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L254-L262) --> - * h2(#foundations). Live cursor foundations + * ## Live cursor foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * Live cursors build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. * - * Live cursors build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the [space state](/spaces/space) and may only be subscribed to on the `cursors` namespace. * - * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own "channel":/channels. The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the "space state":/spaces/space and may only be subscribed to on the @cursors@ namespace. + * The channel is only created when a member calls `space.cursors.set()`. The live cursors channel object can be accessed through `space.cursors.channel`. To monitor the [underlying state of the cursors channel](/channels#states), the channel object can be accessed through `space.cursors.channel`. * - * The channel is only created when a member calls @space.cursors.set()@. The live cursors channel object can be accessed through @space.cursors.channel@. To monitor the "underlying state of the cursors channel":/channels#states, the channel object can be accessed through @space.cursors.channel@. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -76,36 +82,58 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * @return {void} * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L21-L74) --> - * Set the position of a member's cursor using the @set()@ method. A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Calling @set()@ will emit a cursor event so that other members are informed of the cursor movement in realtime. + * Set the position of a member’s cursor using the `set()` method. A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Calling `set()` will emit a cursor event so that other members are informed of the cursor movement in realtime. * - * A member must have been "entered":/spaces/space#enter into the space to set their cursor position. + * A member must have been [entered](/spaces/space#enter) into the space to set their cursor position. * - * The @set()@ method takes the following parameters: + * The `set()` method takes the following parameters: * - * |_. Parameter |_. Description |_. Type | - * | position.x | The position of the member's cursor on the X-axis. | Number | - * | position.y | The position of the member's cursor on the Y-axis. | Number | - * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor, such as a color. | Object | + * | Parameter | Description | Type | + * |------------|---------------------------------------------------------------------------------------------------------------------|--------| + * | position.x | The position of the member’s cursor on the X-axis. | Number | + * | position.y | The position of the member’s cursor on the Y-axis. | Number | + * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor, such as a color. | Object | * * <aside data-type='note'> - * <p>The @data@ parameter can be used to stream additional information related to a cursor's movement, such as:</p> - * <ul><li>The color that other member's should display a cursor as.</li> - * <li>The ID of an element that a user may be dragging for drag and drop functionality.</li> - * <li>Details of any cursor annotations.</li></ul> - * <p>Be aware that as live cursor updates are batched it is not advisable to publish data unrelated to cursor position in the @data@ parameter. Use a "pub/sub channel":/channels instead.</p> + * <p> + * + * The `data` parameter can be used to stream additional information related to a cursor’s movement, such as: + * + * </p> + * <ul> + * <li> + * + * The color that other member’s should display a cursor as. + * + * </li> + * <li> + * + * The ID of an element that a user may be dragging for drag and drop functionality. + * + * </li> + * <li> + * + * Details of any cursor annotations. + * + * </li> + * </ul> + * <p> + * + * Be aware that as live cursor updates are batched it is not advisable to publish data unrelated to cursor position in the `data` parameter. Use a [pub/sub channel](/channels) instead. + * + * </p> * </aside> * - * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the @set()@ method: + * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the `set()` method: * - * ```[javascript] + * ```javascript * window.addEventListener('mousemove', ({ clientX, clientY }) => { * space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); * }); * ``` + * The following is an example payload of a cursor event. Cursor events are uniquely identifiable by the `connectionId` of a cursor. * - * The following is an example payload of a cursor event. Cursor events are uniquely identifiable by the @connectionId@ of a cursor. - * - * ```[json] + * ```json * { * "hd9743gjDc": { * "connectionId": "hd9743gjDc", @@ -120,16 +148,17 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * } * } * ``` - * * The following are the properties of a cursor event payload: * - * |_. Property |_. Description |_. Type | - * | connectionId | The unique identifier of the member's "connection":/connect. | String | - * | clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | position | An object containing the position of a member's cursor. | Object | - * | position.x | The position of the member's cursor on the X-axis. | Number | - * | position.y | The position of the member's cursor on the Y-axis. | Number | - * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | + * | Property | Description | Type | + * |--------------|----------------------------------------------------------------------------------------------------|--------| + * | connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | position | An object containing the position of a member’s cursor. | Object | + * | position.x | The position of the member’s cursor on the X-axis. | Number | + * | position.y | The position of the member’s cursor on the Y-axis. | Number | + * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -202,15 +231,19 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L78-L90) --> - * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling @set()@. Use the @subscribe()@ method on the @cursors@ object of a space to receive updates. + * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling `set()`. Use the `subscribe()` method on the `cursors` object of a space to receive updates. * * <aside data-type='note'> - * <p>The rate at which cursor events are published is controlled by the @outboundBatchInterval@ property set in the "cursor options":#options of a space.</p> + * <p> + * + * The rate at which cursor events are published is controlled by the `outboundBatchInterval` property set in the [cursor options](#options) of a space. + * + * </p> * </aside> * * The following is an example of subscribing to cursor events: * - * ```[javascript] + * ```javascript * space.cursors.subscribe('update', (cursorUpdate) => { * console.log(cursorUpdate); * }); @@ -267,13 +300,12 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * * The following is an example of removing a listener for cursor update events: * - * ```[javascript] + * ```javascript * space.cursors.unsubscribe(`update`, listener); * ``` - * * Or remove all listeners: * - * ```[javascript] + * ```javascript * space.cursors.unsubscribe(); * ``` * <!-- END WEBSITE DOCUMENTATION --> @@ -365,15 +397,14 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L126-L211) --> * Cursor positions can be retrieved in one-off calls. These are local calls that retrieve the latest position of cursors retained in memory by the SDK. * - * The following is an example of retrieving a member's own cursor position: + * The following is an example of retrieving a member’s own cursor position: * - * ```[javascript] + * ```javascript * const myCursor = await space.cursors.getSelf(); * ``` + * The following is an example payload returned by `space.cursors.getSelf()`: * - * The following is an example payload returned by @space.cursors.getSelf()@: - * - * ```[json] + * ```json * { * “clientId”: “DzOBJqgGXzyUBb816Oa6i”, * “connectionId”: “__UJBKZchX”, @@ -383,16 +414,14 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * } * } * ``` - * * The following is an example of retrieving the cursor positions for all members other than the member themselves: * - * ```[javascript] + * ```javascript * const othersCursors = await space.cursors.getOthers(); * ``` + * The following is an example payload returned by `space.cursors.getOthers()`: * - * The following is an example payload returned by @space.cursors.getOthers()@: - * - * ```[json] + * ```json * { * "3ej3q7yZZz": { * "clientId": "yyXidHatpP3hJpMpXZi8W", @@ -412,16 +441,14 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * } * } * ``` + * The following is an example of retrieving the cursor positions for all members, including the member themselves. `getAll()` is useful for retrieving the initial position of members’ cursors. * - * The following is an example of retrieving the cursor positions for all members, including the member themselves. @getAll()@ is useful for retrieving the initial position of members' cursors. - * - * ```[javascript] + * ```javascript * const allCursors = await space.cursors.getAll(); * ``` + * The following is an example payload returned by `space.cursors.getAll()`: * - * The following is an example payload returned by @space.cursors.getAll()@: - * - * ```[json] + * ```json * { * "3ej3q7yZZz": { * "clientId": "yyXidHatpP3hJpMpXZi8W", diff --git a/src/Locations.ts b/src/Locations.ts index b644867c..b3f482a4 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -20,17 +20,19 @@ export interface LocationsEventMap { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L9-L11) --> - * The member location feature enables you to track where members are within a space, to see which part of your application they're interacting with. A location could be the form field they have selected, the cell they're currently editing in a spreadsheet, or the slide they're viewing within a slide deck. Multiple members can be present in the same location. + * The member location feature enables you to track where members are within a space, to see which part of your application they’re interacting with. A location could be the form field they have selected, the cell they’re currently editing in a spreadsheet, or the slide they’re viewing within a slide deck. Multiple members can be present in the same location. + * + * Member locations are used to visually display which component other members currently have selected, or are currently active on. Events are emitted whenever a member sets their location, such as when they click on a new cell, or slide. Events are received by members subscribed to location events and the UI component can be highlighted with the active member’s profile data to visually display their location. * - * Member locations are used to visually display which component other members currently have selected, or are currently active on. Events are emitted whenever a member sets their location, such as when they click on a new cell, or slide. Events are received by members subscribed to location events and the UI component can be highlighted with the active member's profile data to visually display their location. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L211-L215) --> - * h2(#foundations). Member location foundations + * ## Member location foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * Member locations build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). * - * Member locations build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -76,15 +78,15 @@ export default class Locations extends EventEmitter<LocationsEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L15-L25) --> - * Use the @set()@ method to emit a location event in realtime when a member changes their location. This will be received by all location subscribers to inform them of the location change. A @location@ can be any JSON-serializable object, such as a slide number or element ID. + * Use the `set()` method to emit a location event in realtime when a member changes their location. This will be received by all location subscribers to inform them of the location change. A `location` can be any JSON-serializable object, such as a slide number or element ID. * - * A member must have been "entered":/spaces/space#enter into the space to set their location. + * A member must have been [entered](/spaces/space#enter) into the space to set their location. * - * The @set()@ method is commonly combined with "@addEventListener()@":https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener or a React "synthetic event":https://react.dev/learn/responding-to-events#adding-event-handlers, such as @onClick@ or @onHover@. + * The `set()` method is commonly combined with [`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) or a React [synthetic event](https://react.dev/learn/responding-to-events#adding-event-handlers), such as `onClick` or `onHover`. * * The following is an example of a member setting their location to a specific slide number, and element on that slide: * - * ```[javascript] + * ```javascript * await space.locations.set({ slide: '3', component: 'slide-title' }); * ``` * <!-- END WEBSITE DOCUMENTATION --> @@ -106,25 +108,28 @@ export default class Locations extends EventEmitter<LocationsEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L29-L91) --> - * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling "@set()@":#set. Use the @subscribe()@ method on the @locations@ namespace of the space to receive updates. + * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling [`set()`](#set). Use the `subscribe()` method on the `locations` namespace of the space to receive updates. * - * All location changes are @update@ events. When a location update is received, clear the highlight from the UI component of the member's @previousLocation@ and add it to @currentLocation@. + * All location changes are `update` events. When a location update is received, clear the highlight from the UI component of the member’s `previousLocation` and add it to `currentLocation`. * * <aside data-type='note'> - * <p> A location update is also emitted when a member "leaves":/spaces/space#leave a space. The member's @currentLocation@ will be @null@ for these events so that any UI component highlighting can be cleared.</p> + * <p> + * + * A location update is also emitted when a member [leaves](/spaces/space#leave) a space. The member’s `currentLocation` will be `null` for these events so that any UI component highlighting can be cleared. + * + * </p> * </aside> * * The following is an example of subscribing to location events: * - * ```[javascript] + * ```javascript * space.locations.subscribe('update', (locationUpdate) => { * console.log(locationUpdate); * }); * ``` + * The following is an example payload of a location event. Information about location is returned in `currentLocation` and `previousLocation`: * - * The following is an example payload of a location event. Information about location is returned in @currentLocation@ and @previousLocation@: - * - * ```[json] + * ```json * { * "member": { * "clientId": "clemons#142", @@ -153,22 +158,27 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * } * } * ``` - * * The following are the properties of a location event payload: * - * |_. Property |_. Description |_. Type | - * | member.clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | member.connectionId | The unique identifier of the member's "connection":/connect. | String | - * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | - * | member.lastEvent.name | The most recent "event":/spaces/avatar emitted by the member. Will be one of @enter@, @update@, @leave@ or @remove@. | String | - * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | - * | previousLocation | The previous location of the member. | Object | - * | currentLocation | The new location of the member. | Object | + * | Property | Description | Type | + * |----------------------------|-----------------------------------------------------------------------------------------------------------------------|---------| + * | member.clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | member.connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | + * | member.lastEvent.name | The most recent [event](/spaces/avatar) emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | + * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | member.profileData | The optional [profile data](/spaces/avatar#profile-data) associated with the member. | Object | + * | previousLocation | The previous location of the member. | Object | + * | currentLocation | The new location of the member. | Object | * * <aside data-type='further-reading'> - * <p>Member location subscription listeners only trigger on events related to members' locations. Each event only contains the payload of the member that triggered it. Alternatively, "space state":/spaces/space can be subscribed to which returns an array of all members with their latest state every time any event is triggered.</p> + * <p> + * + * Member location subscription listeners only trigger on events related to members’ locations. Each event only contains the payload of the member that triggered it. Alternatively, [space state](/spaces/space) can be subscribed to which returns an array of all members with their latest state every time any event is triggered. + * + * </p> * </aside> + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -213,13 +223,12 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * * The following is an example of removing a listener for location update events: * - * ```[javascript] + * ```javascript * space.locations.unsubscribe('update', listener); * ``` - * * Or remove all listeners: * - * ```[javascript] + * ```javascript * space.locations.unsubscribe(); * ``` * <!-- END WEBSITE DOCUMENTATION --> @@ -300,30 +309,27 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L111-L172) --> * Member locations can also be retrieved in one-off calls. These are local calls and retrieve the location of members retained in memory by the SDK. * - * The following is an example of retrieving a member's own location: + * The following is an example of retrieving a member’s own location: * - * ```[javascript] + * ```javascript * const myLocation = await space.locations.getSelf(); * ``` + * The following is an example payload returned by `space.locations.getSelf()`. It will return the properties of the member’s `location`: * - * The following is an example payload returned by @space.locations.getSelf()@. It will return the properties of the member's @location@: - * - * ```[json] + * ```json * { * "slide": "3", * "component": "slide-title" * } * ``` - * * The following is an example of retrieving the location objects of all members other than the member themselves. * - * ```[javascript] + * ```javascript * const othersLocations = await space.locations.getOthers(); * ``` + * The following is an example payload returned by `space.locations.getOthers()`: It will return the properties of all member’s `location` by their `connectionId`: * - * The following is an example payload returned by @space.locations.getOthers()@: It will return the properties of all member's @location@ by their @connectionId@: - * - * ```[json] + * ```json * { * "xG6H3lnrCn": { * "slide": "1", @@ -335,16 +341,14 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * } * } * ``` - * * The following is an example of retrieving the location objects of all members, including the member themselves: * - * ```[javascript] + * ```javascript * const allLocations = await space.locations.getAll(); * ``` + * The following is an example payload returned by `space.locations.getAll()`. It will return the properties of all member’s `location` by their `connectionId`: * - * The following is an example payload returned by @space.locations.getAll()@. It will return the properties of all member's @location@ by their @connectionId@: - * - * ```[json] + * ```json * { * "xG6H3lnrCn": { * "slide": "1", diff --git a/src/Locks.ts b/src/Locks.ts index a7b450d8..cdfc2771 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -25,35 +25,45 @@ export interface LocksEventMap { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L9-L37) --> - * The component locking feature enables members to optimistically lock stateful UI components before editing them. This reduces the chances of conflicting changes being made to the same component by different members. A component could be a cell in a spreadsheet that a member is updating, or an input field on a form they're filling in. + * The component locking feature enables members to optimistically lock stateful UI components before editing them. This reduces the chances of conflicting changes being made to the same component by different members. A component could be a cell in a spreadsheet that a member is updating, or an input field on a form they’re filling in. * * Once a lock has been acquired by a member, the component that it relates to can be updated in the UI to visually indicate to other members that it is locked and and which member has the lock. The component can then be updated once the editing member has released the lock to indicate that it is now unlocked. * - * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three "states":#states and may only transition between states in specific circumstances. + * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three [states](#states) and may only transition between states in specific circumstances. * * <aside data-type='important'> - * <p>Optimistic locking means that there is a chance that two members may begin editing the same UI component before it is confirmed which member holds the lock. On average, the time taken to reconcile which member holds a lock is in the hundreds of milliseconds. Your application needs to handle the member that successfully obtained the lock, as well as the member that had their request invalidated.</p> + * <p> + * + * Optimistic locking means that there is a chance that two members may begin editing the same UI component before it is confirmed which member holds the lock. On average, the time taken to reconcile which member holds a lock is in the hundreds of milliseconds. Your application needs to handle the member that successfully obtained the lock, as well as the member that had their request invalidated. + * + * </p> * </aside> * - * h2(#states). Lock states + * ## Lock states * - * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call "@acquire()@":#acquire on the lock identifier related to it. Alternatively, you could wait until they receive a @locked@ event and display a spinning symbol in the UI until this is received. In either case a subsequent @unlocked@ event may invalidate that member's lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. + * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call [`acquire()`](#acquire) on the lock identifier related to it. Alternatively, you could wait until they receive a `locked` event and display a spinning symbol in the UI until this is received. In either case a subsequent `unlocked` event may invalidate that member’s lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. * * A lock will be in one of the following states: * - * - @pending@ := A member has requested a lock by calling "@acquire()@":#acquire. - * - @locked@ := The lock is confirmed to be held by the requesting member. - * - @unlocked@ := The lock is confirmed to not be locked by the requesting member, or has been "released":#release by a member previously holding the lock. + * `pending` + * A member has requested a lock by calling [`acquire()`](#acquire). + * + * `locked` + * The lock is confirmed to be held by the requesting member. + * + * `unlocked` + * The lock is confirmed to not be locked by the requesting member, or has been [released](#release) by a member previously holding the lock. * * The following lock state transitions may occur: * - * * None → @pending@: a member calls "@acquire()@":#acquire to request a lock. - * * @pending@ → @locked@: the requesting member holds the lock. - * * @pending@ → @unlocked@: the requesting member does not hold the lock, since another member already holds it. - * * @locked@ → @unlocked@: the lock was either explicitly "released":#release by the member, or their request was invalidated by a concurrent request which took precedence. - * * @unlocked@ → @locked@: the requesting member reacquired a lock they previously held. + * - None → `pending`: a member calls [`acquire()`](#acquire) to request a lock. + * - `pending` → `locked`: the requesting member holds the lock. + * - `pending` → `unlocked`: the requesting member does not hold the lock, since another member already holds it. + * - `locked` → `unlocked`: the lock was either explicitly [released](#release) by the member, or their request was invalidated by a concurrent request which took precedence. + * - `unlocked` → `locked`: the requesting member reacquired a lock they previously held. + * + * Only transitions that result in a `locked` or `unlocked` status will emit a lock event that members can [`subscribe()`](#subscribe) to. * - * Only transitions that result in a @locked@ or @unlocked@ status will emit a lock event that members can "@subscribe()@":#subscribe to. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -77,28 +87,26 @@ export default class Locks extends EventEmitter<LocksEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L171-L192) --> - * Use the @get()@ method to query whether a lock is currently locked, and by which member if it is. The lock is identifiable by its unique string ID. + * Use the `get()` method to query whether a lock is currently locked, and by which member if it is. The lock is identifiable by its unique string ID. * * The following is an example of checking whether a lock identifier is currently locked: * - * ```[javascript] + * ```javascript * const isLocked = space.locks.get(id) !== undefined; * ``` - * * The following is an example of checking which member holds the lock: * - * ```[javascript] + * ```javascript * const { member } = space.locks.get(id); * ``` - * * The following is an example of viewing the attributes assigned to the lock by the member holding it: * - * ```[javascript] + * ```javascript * const { request } = space.locks.get(id); * const viewLock = request.attributes.get(key); * ``` + * If the lock is not currently held by a member, `get()` will return `undefined`. Otherwise it will return the most recent lock event for the lock. * - * If the lock is not currently held by a member, @get()@ will return @undefined@. Otherwise it will return the most recent lock event for the lock. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -130,13 +138,12 @@ export default class Locks extends EventEmitter<LocksEventMap> { * * The following is an example of retrieving an array of all currently held locks in a space: * - * ```[javascript] + * ```javascript * const allLocks = await space.locks.getAll(); * ``` + * The following is an example payload returned by `space.locks.getAll()`: * - * The following is an example payload returned by @space.locks.getAll()@: - * - * ```[json] + * ```json * [ * { * "id": "s1-c2", @@ -251,27 +258,25 @@ export default class Locks extends EventEmitter<LocksEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L41-L72) --> - * Use the @acquire()@ method to attempt to acquire a lock with a given unique ID. Additional @attributes@ may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using @attributes@ is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. + * Use the `acquire()` method to attempt to acquire a lock with a given unique ID. Additional `attributes` may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using `attributes` is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. * - * A member must have been "entered":/spaces/space#enter into the space to acquire a lock. + * A member must have been [entered](/spaces/space#enter) into the space to acquire a lock. * * The following is an example of attempting to acquire a lock: * - * ```[javascript] + * ```javascript * const acquireLock = await space.locks.acquire(id); * ``` + * The following is an example of passing a set of `attributes` when trying to acquire a lock: * - * The following is an example of passing a set of @attributes@ when trying to acquire a lock: - * - * ```[javascript] + * ```javascript * const lockAttributes = new Map(); * lockAttributes.set('component', 'cell-d3'); * const acquireLock = await space.locks.acquire(id, { lockAttributes }); * ``` + * The following is an example payload returned by `space.locks.acquire()`. The promise will resolve to a lock request with the `pending` status: * - * The following is an example payload returned by @space.locks.acquire()@. The promise will resolve to a lock request with the @pending@ status: - * - * ```[json] + * ```json * { * "id": "s2-d14", * "status": "pending", @@ -281,8 +286,8 @@ export default class Locks extends EventEmitter<LocksEventMap> { * } * } * ``` + * Once a member requests a lock by calling `acquire()`, the lock is temporarily in the [pending state](#states). An event will be emitted based on whether the lock request was successful (a status of `locked`) or invalidated (a status of `unlocked`). This can be [subscribed](#subscribe) to in order for the client to know whether their lock request was successful or not. * - * Once a member requests a lock by calling @acquire()@, the lock is temporarily in the "pending state":#states. An event will be emitted based on whether the lock request was successful (a status of @locked@) or invalidated (a status of @unlocked@). This can be "subscribed":#subscribe to in order for the client to know whether their lock request was successful or not. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -333,19 +338,23 @@ export default class Locks extends EventEmitter<LocksEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L76-L88) --> - * Use the @release()@ method to explicitly release a lock once a member has finished editing the related component. For example, the @release()@ method can be called once a user clicks outside of the component, such as clicking on another cell within a spreadsheet. Any UI indications that the previous cell was locked can then be cleared. + * Use the `release()` method to explicitly release a lock once a member has finished editing the related component. For example, the `release()` method can be called once a user clicks outside of the component, such as clicking on another cell within a spreadsheet. Any UI indications that the previous cell was locked can then be cleared. * * The following is an example of releasing a lock: * - * ```[javascript] + * ```javascript * await space.locks.release(id); * ``` - * - * Releasing a lock will emit a lock event with a "lock status":#states of @unlocked@. + * Releasing a lock will emit a lock event with a [lock status](#states) of `unlocked`. * * <aside data-type='note'> - * <p>When a member "leaves":/spaces/space#leave a space, their locks are automatically released.</p> + * <p> + * + * When a member [leaves](/spaces/space#leave) a space, their locks are automatically released. + * + * </p> * </aside> + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -379,18 +388,17 @@ export default class Locks extends EventEmitter<LocksEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L92-L151) --> - * Subscribe to lock events by registering a listener. Lock events are emitted whenever the "lock state":#states transitions into @locked@ or @unlocked@. Use the @subscribe()@ method on the @locks@ namespace of the space to receive updates. + * Subscribe to lock events by registering a listener. Lock events are emitted whenever the [lock state](#states) transitions into `locked` or `unlocked`. Use the `subscribe()` method on the `locks` namespace of the space to receive updates. * - * All lock events are @update@ events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. + * All lock events are `update` events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. * * The following is an example of subscribing to lock events: * - * ```[javascript] + * ```javascript * space.locks.subscribe('update', (lock) => { * console.log(lock); * }); * ``` - * * The following is an example payload of a lock event: * * ```json @@ -424,21 +432,22 @@ export default class Locks extends EventEmitter<LocksEventMap> { * } * } * ``` - * * The following are the properties of a lock event payload: * - * |_. Property |_. Description |_. Type | - * | id | The unique ID of the lock request. | String | - * | status | The lock "status":#states of the event. Will be one of @locked@, @unlocked@ or @pending@. | String | - * | timestamp | The timestamp of the lock event. | Number | - * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | - * | reason | The reason why the @request.status@ is @unlocked@. | ErrorInfo | - * | member.clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | member.connectionId | The unique identifier of the member's "connection":/connect. | String | - * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | - * | member.lastEvent.name | The most recent "event":/spaces/avatar#events emitted by the member. Will be one of @enter@, @update@, @leave@ or @remove@. | String | - * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | + * | Property | Description | Type | + * |----------------------------|------------------------------------------------------------------------------------------------------------------------------|-----------| + * | id | The unique ID of the lock request. | String | + * | status | The lock [status](#states) of the event. Will be one of `locked`, `unlocked` or `pending`. | String | + * | timestamp | The timestamp of the lock event. | Number | + * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | + * | reason | The reason why the `request.status` is `unlocked`. | ErrorInfo | + * | member.clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | member.connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | + * | member.lastEvent.name | The most recent [event](/spaces/avatar#events) emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | + * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | member.profileData | The optional [profile data](/spaces/avatar#profile-data) associated with the member. | Object | + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -482,13 +491,12 @@ export default class Locks extends EventEmitter<LocksEventMap> { * * The following is an example of removing a listener for lock update events: * - * ```[javascript] + * ```javascript * space.locks.unsubscribe('update', listener); * ``` - * * Or remove all listeners: * - * ```[javascript] + * ```javascript * space.locks.unsubscribe(); * ``` * <!-- END WEBSITE DOCUMENTATION --> diff --git a/src/Members.ts b/src/Members.ts index 813e1cf8..60c04919 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -17,29 +17,44 @@ export interface MembersEventMap { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L9-L25) --> * Avatar stacks are the most common way of showing the online status of members in an application by displaying an avatar for each member. Events are emitted whenever a member enters or leaves a space, or updates their profile data. Additional information can also be provided, such as a profile picture and email address. * - * Subscribe to the @space.members@ namespace in order to keep your avatar stack updated in realtime. + * Subscribe to the `space.members` namespace in order to keep your avatar stack updated in realtime. * - * h2(#events). Event types + * ## Event types * * The following four event types are emitted by members: * - * - @enter@ := A new member has entered the space. The member has either entered explicitly by calling "@space.enter()@":/spaces/space#enter, or has attempted to update their profile data before entering a space, which will instead emit an @enter@ event. - * - @updateProfile@ := A member has updated their profile data by calling "@space.updateProfileData()@":/spaces/space#update-profile. - * - @leave@ := A member has left the space. The member has either left explicitly by calling "@space.leave()@":/spaces/space#leave, or has abruptly disconnected and not re-established a connection within 15 seconds. - * - @remove@ := A member has been removed from the members list after the "@offlineTimeout@":/spaces/space#options period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their @leave@ and @remove@ events. - * - @update@ := This event is emitted whenever any one of the above events is emitted. + * `enter` + * A new member has entered the space. The member has either entered explicitly by calling [`space.enter()`](/spaces/space#enter), or has attempted to update their profile data before entering a space, which will instead emit an `enter` event. + * + * `updateProfile` + * A member has updated their profile data by calling [`space.updateProfileData()`](/spaces/space#update-profile). + * + * `leave` + * A member has left the space. The member has either left explicitly by calling [`space.leave()`](/spaces/space#leave), or has abruptly disconnected and not re-established a connection within 15 seconds. + * + * `remove` + * A member has been removed from the members list after the [`offlineTimeout`](/spaces/space#options) period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their `leave` and `remove` events. + * + * `update` + * This event is emitted whenever any one of the above events is emitted. * * <aside data-type='note'> - * <p>Members "enter":/spaces/space#enter, "leave":/spaces/space#leave, and "update":/spaces/space#update-profile a "space":/spaces/space directly. The @members@ namespace is used to subscribe to these updates.</p> + * <p> + * + * Members [enter](/spaces/space#enter), [leave](/spaces/space#leave), and [update](/spaces/space#update-profile) a [space](/spaces/space) directly. The `members` namespace is used to subscribe to these updates. + * + * </p> * </aside> + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L297-L301) --> - * h2(#foundations). Avatar stack foundations + * ## Avatar stack foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * Avatar stacks build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). * - * Avatar stacks build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -104,17 +119,16 @@ class Members extends EventEmitter<MembersEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L129-L250) --> - * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member's own profile data to them, or retrieving a list of all other members to use to "update their profile data":/spaces/space#update-profile. + * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member’s own profile data to them, or retrieving a list of all other members to use to [update their profile data](/spaces/space#update-profile). * - * The following is an example of retrieving a member's own member object: + * The following is an example of retrieving a member’s own member object: * - * ```[javascript] + * ```javascript * const myMemberInfo = await space.members.getSelf(); * ``` + * The following is an example payload returned by `space.members.getSelf()`: * - * The following is an example payload returned by @space.members.getSelf()@: - * - * ```[json] + * ```json * { * "clientId": "clemons#142", * "connectionId": "hd9743gjDc", @@ -130,16 +144,14 @@ class Members extends EventEmitter<MembersEventMap> { * } * } * ``` - * * The following is an example of retrieving an array of member objects for all members other than the member themselves. Ths includes members that have recently left the space, but have not yet been removed. * - * ```[javascript] + * ```javascript * const othersMemberInfo = await space.members.getOthers(); * ``` + * The following is an example payload returned by `space.members.getOthers()`: * - * The following is an example payload returned by @space.members.getOthers()@: - * - * ```[json] + * ```json * [ * { * "clientId": "torange#1", @@ -171,16 +183,14 @@ class Members extends EventEmitter<MembersEventMap> { * } * ] * ``` - * * The following is an example of retrieving an array of all member objects, including the member themselves. Ths includes members that have recently left the space, but have not yet been removed. * - * ```[javascript] + * ```javascript * const allMembers = await space.members.getAll(); * ``` + * The following is an example payload returned by `space.members.getAll()`: * - * The following is an example payload returned by @space.members.getAll()@: - * - * ```[json] + * ```json * [ * { * "clientId": "clemons#142", @@ -265,11 +275,11 @@ class Members extends EventEmitter<MembersEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L29-L103) --> - * Subscribe to members' online status and profile updates by registering a listener. Member events are emitted whenever a member "enters":/spaces/space#enter or "leaves":/spaces/space#leave the space, or updates their profile data. Use the @subscribe()@ method on the @members@ object of a space to receive updates. + * Subscribe to members’ online status and profile updates by registering a listener. Member events are emitted whenever a member [enters](/spaces/space#enter) or [leaves](/spaces/space#leave) the space, or updates their profile data. Use the `subscribe()` method on the `members` object of a space to receive updates. * * The following is an example of subscribing to the different member event types: * - * ```[javascript] + * ```javascript * // Subscribe to member enters in a space * space.members.subscribe('enter', (memberUpdate) => { * console.log(memberUpdate); @@ -290,26 +300,23 @@ class Members extends EventEmitter<MembersEventMap> { * console.log(memberUpdate); * }); * ``` + * It’s also possible to subscribe to multiple event types with the same listener by using an array: * - * It's also possible to subscribe to multiple event types with the same listener by using an array: - * - * ```[javascript] + * ```javascript * space.members.subscribe(['enter', 'update'], (memberUpdate) => { * console.log(memberUpdate); * }); * ``` - * * Or subscribe to all event types: * - * ```[javascript] + * ```javascript * space.members.subscribe((memberUpdate) => { * console.log(memberUpdate); * }); * ``` + * The following is an example payload of a member event. The `lastEvent.name` describes which [event type](#events) a payload relates to. * - * The following is an example payload of a member event. The @lastEvent.name@ describes which "event type":#events a payload relates to. - * - * ```[json] + * ```json * { * "clientId": "clemons#142", * "connectionId": "hd9743gjDc", @@ -325,21 +332,26 @@ class Members extends EventEmitter<MembersEventMap> { * } * } * ``` - * * The following are the properties of a member event payload: * - * |_. Property |_. Description |_. Type | - * | clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | connectionId | The unique identifier of the member's "connection":/connect. | String | - * | isConnected | Whether the member is connected to Ably or not. | Boolean | - * | profileData | The optional "profile data":#profile-data associated with the member. | Object | - * | location | The current "location":/spaces/locations of the member. Will be @null@ for @enter@, @leave@ and @remove@ events. | Object | - * | lastEvent.name | The most recent event emitted by the member. | String | - * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | Property | Description | Type | + * |---------------------|-------------------------------------------------------------------------------------------------------------------|---------| + * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | isConnected | Whether the member is connected to Ably or not. | Boolean | + * | profileData | The optional [profile data](#profile-data) associated with the member. | Object | + * | location | The current [location](/spaces/locations) of the member. Will be `null` for `enter`, `leave` and `remove` events. | Object | + * | lastEvent.name | The most recent event emitted by the member. | String | + * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * * <aside data-type='further-reading'> - * <p>Avatar stack subscription listeners only trigger on events related to members' online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, "space state":/spaces/space can be subscribed to which returns an array of all members with their latest state every time any event is triggered.</p> + * <p> + * + * Avatar stack subscription listeners only trigger on events related to members’ online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, [space state](/spaces/space) can be subscribed to which returns an array of all members with their latest state every time any event is triggered. + * + * </p> * </aside> + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -432,19 +444,17 @@ class Members extends EventEmitter<MembersEventMap> { * * The following is an example of removing a listener for one member event type: * - * ```[javascript] + * ```javascript * space.members.unsubscribe('enter', listener); * ``` + * It’s also possible to remove listeners for multiple member event types: * - * It's also possible to remove listeners for multiple member event types: - * - * ```[javascript] + * ```javascript * space.members.unsubscribe(['enter', 'leave'], listener); * ``` - * * Or remove all listeners: * - * ```[javascript] + * ```javascript * space.members.unsubscribe(); * ``` * <!-- END WEBSITE DOCUMENTATION --> diff --git a/src/Space.ts b/src/Space.ts index 3098d72c..ef0bba8e 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -48,16 +48,17 @@ export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileDat * * The following features can be implemented within a space: * - * * "Avatar stack":/spaces/avatar - * * "Member location":/spaces/locations - * * "Live cursors":/spaces/cursors - * * "Component locking":/spaces/locking + * - [Avatar stack](/spaces/avatar) + * - [Member location](/spaces/locations) + * - [Live cursors](/spaces/cursors) + * - [Component locking](/spaces/locking) * - * The @space@ namespace consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member's location within the application. The position of members' cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. + * The `space` namespace consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member’s location within the application. The position of members’ cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. * - * Space state can be "subscribed":#subscribe to in the @space@ namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. + * Space state can be [subscribed](#subscribe) to in the `space` namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. * * To subscribe to any events in a space, you first need to create or retrieve a space. + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -162,29 +163,29 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L43-L55) --> - * Entering a space will register a client as a member and emit an "@enter@":/spaces/members#events event to all subscribers. Use the @enter()@ method to enter a space. + * Entering a space will register a client as a member and emit an [`enter`](/spaces/members#events) event to all subscribers. Use the `enter()` method to enter a space. * * Being entered into a space is required for members to: * - * * Update their "profile data":#update-profile. - * * Set their "location":/spaces/locations. - * * Set their "cursor position":/spaces/cursors. + * - Update their [profile data](#update-profile). + * - Set their [location](/spaces/locations). + * - Set their [cursor position](/spaces/cursors). * * The following is an example of entering a space: * - * ```[javascript] + * ```javascript * await space.enter(); * ``` * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L71-L82) --> - * Profile data can be set when "entering":#enter a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. + * Profile data can be set when [entering](#enter) a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. * * Profile data is returned in the payload of all space events. * * The following is an example of setting profile data when entering a space: * - * ```[javascript] + * ```javascript * await space.enter({ * username: 'Claire Oranges', * avatar: 'https://slides-internal.com/users/coranges.png', @@ -220,20 +221,19 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L86-L103) --> - * Profile data can be updated at any point after entering a space by calling @updateProfileData()@. This will emit an @update@ event. If a client hasn't yet entered the space, @updateProfileData()@ will instead "enter the space":#enter, with the profile data, and emit an "@enter@":/spaces/members#events event. + * Profile data can be updated at any point after entering a space by calling `updateProfileData()`. This will emit an `update` event. If a client hasn’t yet entered the space, `updateProfileData()` will instead [enter the space](#enter), with the profile data, and emit an [`enter`](/spaces/members#events) event. * * The following is an example of updating profile data: * - * ```[javascript] + * ```javascript * space.updateProfileData({ * username: 'Claire Lemons', * avatar: 'https://slides-internal.com/users/clemons.png', * }); * ``` + * A function can be passed to `updateProfileData()` in order to update a field based on the existing profile data: * - * A function can be passed to @updateProfileData()@ in order to update a field based on the existing profile data: - * - * ```[javascript] + * ```javascript * space.updateProfileData(currentProfile => { * return { ...currentProfile, username: 'Clara Lemons' } * }); @@ -283,15 +283,15 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L59-L67) --> - * Leaving a space will emit a "@leave@":/spaces/members#events event to all subscribers. + * Leaving a space will emit a [`leave`](/spaces/members#events) event to all subscribers. * * The following is an example of explicitly leaving a space: * - * ```[javascript] + * ```javascript * await space.leave(); * ``` + * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no `leave` event will be emitted. * - * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no @leave@ event will be emitted. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -320,11 +320,11 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L191-L197) --> - * The current state of the space can be retrieved in a one-off call. This will return an array of all @member@ objects currently in the space. This is a local call and retrieves the membership of the space retained in memory by the SDK. + * The current state of the space can be retrieved in a one-off call. This will return an array of all `member` objects currently in the space. This is a local call and retrieves the membership of the space retained in memory by the SDK. * * The following is an example of retrieving the current space state. Ths includes members that have recently left the space, but have not yet been removed: * - * ```[javascript] + * ```javascript * const spaceState = await space.getState(); * ``` * <!-- END WEBSITE DOCUMENTATION --> @@ -336,34 +336,41 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L107-L177) --> - * Subscribe to space state updates by registering a listener. Use the @subscribe()@ method on the @space@ object to receive updates. + * Subscribe to space state updates by registering a listener. Use the `subscribe()` method on the `space` object to receive updates. * * The following events will trigger a space event: * - * * A member enters the space - * * A member leaves the space - * * A member is removed from the space state "after the offlineTimeout period":#options has elapsed - * * A member updates their profile data - * * A member sets a new location + * - A member enters the space + * - A member leaves the space + * - A member is removed from the space state [after the offlineTimeout period](#options) has elapsed + * - A member updates their profile data + * - A member sets a new location * - * Space state contains a single object called @members@. Any events that trigger a change in space state will always return the current state of the space as an array of @member@ objects. + * Space state contains a single object called `members`. Any events that trigger a change in space state will always return the current state of the space as an array of `member` objects. * * <aside data-type='note'> - * <p>"Avatar stacks":/spaces/members and "member location":/spaces/locations events can be subscribed to on their individual namespaces; @space.members@ and @space.locations@. These events are filtered versions of space state events. Only a single "message":/channels/messages is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message.</p> - * <p>The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event.</p> + * <p> + * + * [Avatar stacks](/spaces/members) and [member location](/spaces/locations) events can be subscribed to on their individual namespaces; `space.members` and `space.locations`. These events are filtered versions of space state events. Only a single [message](/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. + * + * </p> + * <p> + * + * The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event. + * + * </p> * </aside> * * The following is an example of subscribing to space events: * - * ```[javascript] + * ```javascript * space.subscribe('update', (spaceState) => { * console.log(spaceState.members); * }); * ``` - * * The following is an example payload of a space event. * - * ```[json] + * ```json * [ * { * "clientId": "clemons#142", @@ -396,17 +403,18 @@ class Space extends EventEmitter<SpaceEventMap> { * ... * ] * ``` + * The following are the properties of an individual `member` within a space event payload: + * + * | Property | Description | Type | + * |---------------------|------------------------------------------------------------------------|---------| + * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | isConnected | Whether the member is connected to Ably or not. | Boolean | + * | profileData | The optional [profile data](#profile-data) associated with the member. | Object | + * | location | The current [location](/spaces/locations) of the member. | Object | + * | lastEvent.name | The most recent event emitted by the member. | String | + * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * - * The following are the properties of an individual @member@ within a space event payload: - * - * |_. Property |_. Description |_. Type | - * | clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | connectionId | The unique identifier of the member's "connection":/connect. | String | - * | isConnected | Whether the member is connected to Ably or not. | Boolean | - * | profileData | The optional "profile data":#profile-data associated with the member. | Object | - * | location | The current "location":/spaces/locations of the member. | Object | - * | lastEvent.name | The most recent event emitted by the member. | String | - * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * <!-- END WEBSITE DOCUMENTATION --> */ subscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap, K>): void; @@ -434,7 +442,7 @@ class Space extends EventEmitter<SpaceEventMap> { * * The following is an example of removing a listener: * - * ```[javascript] + * ```javascript * space.unsubscribe('update', listener); * ``` * <!-- END WEBSITE DOCUMENTATION --> diff --git a/src/Spaces.ts b/src/Spaces.ts index 9cceaff3..57abfcd5 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -61,87 +61,90 @@ class Spaces { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L26-L39) --> - * A @space@ object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the @spaces@ collection using the @get()@ method. + * A `space` object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the `spaces` collection using the `get()` method. * * The following restrictions apply to space names: * - * * Avoid starting names with @[@ or @:@ - * * Ensure names aren't empty - * * Exclude whitespace and wildcards, such as @*@ - * * Use the correct case, whether it be uppercase or lowercase + * - Avoid starting names with `[` or `:` + * - Ensure names aren’t empty + * - Exclude whitespace and wildcards, such as `*` + * - Use the correct case, whether it be uppercase or lowercase * * The following is an example of creating a space: * - * ```[javascript] + * ```javascript * const space = await spaces.get('board-presentation'); * ``` * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L199-L242) --> - * h2(#advanced). Advanced properties + * ## Advanced properties * - * The following sections are only relevant if you want to further customize a space, or understand more about the Spaces SDK. They aren't required to get up and running with the basics. + * The following sections are only relevant if you want to further customize a space, or understand more about the Spaces SDK. They aren’t required to get up and running with the basics. * - * h3(#options). Space options + * ### Space options * - * An additional set of optional properties may be passed when "creating or retrieving":#create a space to customize the behavior of different features. + * An additional set of optional properties may be passed when [creating or retrieving](#create) a space to customize the behavior of different features. * * The following properties can be customized: * - * |_. Property |_. Description |_. Type | - * | offlineTimeout | Number of milliseconds after a member loses connection or closes their browser window to wait before they are removed from the member list. The default is 120,000ms (2 minutes). | Number | - * | cursors | A "cursor options":/spaces/cursors#options object for customizing live cursor behavior. | Object | - * | cursors.outboundBatchInterval | The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 100ms. | Number | - * | cursors.paginationLimit | The number of pages searched from history for the last published cursor position. The default is 5. | Number | + * | Property | Description | Type | + * |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| + * | offlineTimeout | Number of milliseconds after a member loses connection or closes their browser window to wait before they are removed from the member list. The default is 120,000ms (2 minutes). | Number | + * | cursors | A [cursor options](/spaces/cursors#options) object for customizing live cursor behavior. | Object | + * | cursors.outboundBatchInterval | The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 100ms. | Number | + * | cursors.paginationLimit | The number of pages searched from history for the last published cursor position. The default is 5. | Number | * - * The following is an example of customizing the space options when calling @spaces.get()@: + * The following is an example of customizing the space options when calling `spaces.get()`: * - * ```[javascript] + * ```javascript * const space = await spaces.get('board-presentation', { * offlineTimeout: 180_000, * cursors: { paginationLimit: 10 } * }); * ``` + * ### Space foundations * - * h3(#foundations). Space foundations + * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * A space is created as an Ably [channel](/channels). Members [attach](/channels#attach) to the channel and join its [presence set](/presence-occupancy/presence) when they [enter](#enter) the space. Avatar stacks, member locations and component locking are all handled on this channel. * - * A space is created as an Ably "channel":/channels. Members "attach":/channels#attach to the channel and join its "presence set":/presence-occupancy/presence when they "enter":#enter the space. Avatar stacks, member locations and component locking are all handled on this channel. - * - * To manage the state of the space, you can monitor the "state of the underlying channel":/channels#states. The channel object can be accessed through @space.channel@. + * To manage the state of the space, you can monitor the [state of the underlying channel](/channels#states). The channel object can be accessed through `space.channel`. * * The following is an example of registering a listener to wait for a channel to become attached: * - * ```[javascript] + * ```javascript * space.channel.on('attached', (stateChange) => { * console.log(stateChange) * }); * ``` - * * <aside data-type='note'> * <p> - * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own "channel":/channels. + * + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](/channels). + * * </p> * </aside> + * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L108-L122) --> - * h2(#options). Cursor options + * ## Cursor options * - * Cursor options are set when creating or retrieving a @Space@ instance. They are used to control the behavior of live cursors. + * Cursor options are set when creating or retrieving a `Space` instance. They are used to control the behavior of live cursors. * * The following cursor options can be set: * - * h3(#batch). outboundBatchInterval + * ### outboundBatchInterval + * + * The `outboundBatchInterval` is the interval at which a batch of cursor positions are published, in milliseconds, for each client. This is multiplied by the number of members in a space. * - * The @outboundBatchInterval@ is the interval at which a batch of cursor positions are published, in milliseconds, for each client. This is multiplied by the number of members in a space. + * The default value is 25ms which is optimal for the majority of use cases. If you wish to optimize the interval further, then decreasing the value will improve performance by further ‘smoothing’ the movement of cursors at the cost of increasing the number of events sent. Be aware that at a certain point the rate at which a browser is able to render the changes will impact optimizations. * - * The default value is 25ms which is optimal for the majority of use cases. If you wish to optimize the interval further, then decreasing the value will improve performance by further 'smoothing' the movement of cursors at the cost of increasing the number of events sent. Be aware that at a certain point the rate at which a browser is able to render the changes will impact optimizations. + * ### paginationLimit * - * h3(#pagination). paginationLimit + * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members’ cursor is obtained from [history](/storage-history/history). The `paginationLimit` is the number of pages that should be searched to find the last position of each cursor. The default is 5. * - * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members' cursor is obtained from "history":/storage-history/history. The @paginationLimit@ is the number of pages that should be searched to find the last position of each cursor. The default is 5. * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> From ebd11eb4348ea0a2541e5c027e70788423158463 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 26 Sep 2023 16:09:08 -0300 Subject: [PATCH 122/191] Remove scripts/convert_website_documentation.ts This reverts 17a693d; the script is no longer needed after we ran it in e7f8ca4. --- package.json | 4 +- scripts/convert_website_documentation.ts | 183 ----------------------- 2 files changed, 2 insertions(+), 185 deletions(-) delete mode 100644 scripts/convert_website_documentation.ts diff --git a/package.json b/package.json index 6df6858b..7c7b325c 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write --ignore-path .gitignore src demo scripts", - "format:check": "prettier --check --ignore-path .gitignore src demo scripts", + "format": "prettier --write --ignore-path .gitignore src demo", + "format:check": "prettier --check --ignore-path .gitignore src demo", "test": "vitest run", "test:watch": "vitest watch", "coverage": "vitest run --coverage", diff --git a/scripts/convert_website_documentation.ts b/scripts/convert_website_documentation.ts deleted file mode 100644 index a79e4807..00000000 --- a/scripts/convert_website_documentation.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { readdir, lstat, readFile, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import { execFile } from 'node:child_process'; - -async function processDirectory(path: string) { - const entries = await readdir(path); - - for (const entry of entries) { - const pathToEntry = join(path, entry); - - const stat = await lstat(pathToEntry); - if (stat.isDirectory()) { - await processDirectory(pathToEntry); - continue; - } - - if (entry.endsWith('.ts')) { - await processFile(pathToEntry); - } - } -} - -async function processFile(path: string) { - console.log(`Processing: ${path}`); - const contents = await readFile(path, { encoding: 'utf-8' }); - - const beginWebsiteDocumentationRegexp = /<!-- BEGIN WEBSITE DOCUMENTATION \(.*\) -->/g; - const endWebsiteDocumentationRegexp = /<!-- END WEBSITE DOCUMENTATION -->/g; - - const lines = contents.split('\n'); - - const startLineIndices: number[] = []; - const endLineIndices: number[] = []; - - for (const [lineIndex, line] of lines.entries()) { - if (beginWebsiteDocumentationRegexp.exec(line)) { - startLineIndices.push(lineIndex); - } - if (endWebsiteDocumentationRegexp.exec(line)) { - endLineIndices.push(lineIndex); - } - } - - for (let documentationIndex = 0; documentationIndex < startLineIndices.length; documentationIndex++) { - const startLineIndex = startLineIndices[documentationIndex]; - const endLineIndex = endLineIndices[documentationIndex]; - - const documentation = lines.slice(startLineIndex + 1, endLineIndex).join('\n'); - const documentationLineCount = endLineIndex - startLineIndex - 1; - - // Convert the documentation comment. - const converted = await convertWebsiteDocumentationCommentFragment(documentation); - - // Replace the documentation comment in `lines`. - const convertedLines = converted.split('\n'); - lines.splice(startLineIndex + 1, documentationLineCount, ...convertedLines); - - const addedLinesCount = convertedLines.length - documentationLineCount; - - // Shift the line indices to reflect the length of the converted documentation comment. - for ( - let indexOfDocumentationToShiftLineNumbers = documentationIndex + 1; - indexOfDocumentationToShiftLineNumbers < startLineIndices.length; - indexOfDocumentationToShiftLineNumbers++ - ) { - startLineIndices[indexOfDocumentationToShiftLineNumbers] += addedLinesCount; - endLineIndices[indexOfDocumentationToShiftLineNumbers] += addedLinesCount; - } - } - - // Write the new contents of the file. - const newContents = lines.join('\n'); - await writeFile(path, newContents, { encoding: 'utf-8' }); -} - -async function convertWebsiteDocumentationCommentFragment(commentFragment: string) { - const prefixStrippingResult = strippingPrefixOfCommentFragment(commentFragment); - const tagged = tagCommentFragmentLines(prefixStrippingResult.content); - - const lines: string[] = []; - - for (const taggedLines of tagged) { - switch (taggedLines.type) { - case 'textile': - lines.push(...(await convertTextileLines(taggedLines.lines))); - break; - case 'codeBlock': - lines.push(...convertCodeBlockLines(taggedLines.lines)); - break; - } - } - - return restoringPrefixOfCommentFragment(lines.join('\n'), prefixStrippingResult.prefix); -} - -async function convertTextileLines(textileLines: string[]) { - const pandocStdoutPromise = new Promise<string>((resolve, reject) => { - const childProcess = execFile( - '/opt/homebrew/bin/pandoc', - // We choose gfm over commonmark for tables support. - ['--from', 'textile', '--to', 'gfm', '--wrap=preserve'], - (error, stdout, stderr) => { - if (error) { - reject(error); - } else { - resolve(stdout); - } - }, - ); - - // I don’t fully understand how writing works and whether this always succeeds in writing the full thing; keep an eye out for any weirdness and revisit if necessary. - childProcess.stdin!.write(textileLines.join('\n')); - childProcess.stdin!.end(); - }); - - const pandocStdout = await pandocStdoutPromise; - - return pandocStdout.split('\n'); -} - -function convertCodeBlockLines(codeBlockLines: string[]) { - // remove square brackets from language tag - const firstLine = codeBlockLines[0].replace(/[[\]]/g, ''); - return [firstLine, ...codeBlockLines.slice(1)]; -} - -type TaggedLines = { type: 'textile' | 'codeBlock'; lines: string[] }; - -function tagCommentFragmentLines(commentFragment: string) { - const lines = commentFragment.split('\n'); - - const result: TaggedLines[] = []; - - let current: TaggedLines | null = null; - - for (const line of lines) { - if (line.startsWith('```')) { - if (current && current.type === 'codeBlock') { - // end of code block - current.lines.push(line); - result.push(current); - current = null; - } else { - if (current) { - result.push(current); - } - - // start of code block - current = { type: 'codeBlock', lines: [line] }; - } - } else { - if (current) { - current.lines.push(line); - } else { - current = { type: 'textile', lines: [line] }; - } - } - } - - if (current) { - result.push(current); - current = null; - } - - return result; -} - -function strippingPrefixOfCommentFragment(commentFragment: string) { - const lines = commentFragment.split('\n'); - const prefix = /\s+\* /g.exec(lines[0])![0]; - const newLines = lines.map((line) => line.substring(prefix.length)); - - return { content: newLines.join('\n'), prefix }; -} - -function restoringPrefixOfCommentFragment(content: string, prefix: string) { - const lines = content.split('\n'); - const newLines = lines.map((line) => `${prefix}${line}`); - - return newLines.join('\n'); -} - -processDirectory('src'); From 8bbafe18687e7b6198535bcb5ab6f3c0bea9a6bd Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 21 Sep 2023 17:14:55 -0300 Subject: [PATCH 123/191] Convert <aside> elements into quotes with strong text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs repo has a bunch of CSS for making these stand out visually, but we don’t have any of that when using TypeDoc (I guess I could configure it but don’t really want to put time into it now), so instead just use the tools at our disposal to make these sections stand out visually. --- src/Cursors.ts | 58 +++++++++++++----------------------------------- src/Locations.ts | 20 +++++------------ src/Locks.ts | 20 +++++------------ src/Members.ts | 20 +++++------------ src/Space.ts | 17 +++++--------- src/Spaces.ts | 9 +++----- 6 files changed, 41 insertions(+), 103 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index e901fe64..6fc239cf 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -25,13 +25,9 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * Live cursor updates are not available as part of the [space state](/spaces/space#subscribe) and must be subscribed to using [`space.cursors.subscribe()`](#subscribe). * - * <aside data-type='important'> - * <p> - * - * Live cursors are a great way of providing contextual awareness as to what members are looking at within an application. However, too many cursors moving across a page can often be a distraction rather than an enhancement. As such, Ably recommends a maximum of 20 members simultaneously streaming their cursors in a space at any one time for an optimal end-user experience. - * - * </p> - * </aside> + * > **Important** + * > + * > Live cursors are a great way of providing contextual awareness as to what members are looking at within an application. However, too many cursors moving across a page can often be a distraction rather than an enhancement. As such, Ably recommends a maximum of 20 members simultaneously streaming their cursors in a space at any one time for an optimal end-user experience. * * <!-- END WEBSITE DOCUMENTATION --> * @@ -94,35 +90,15 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * | position.y | The position of the member’s cursor on the Y-axis. | Number | * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor, such as a color. | Object | * - * <aside data-type='note'> - * <p> - * - * The `data` parameter can be used to stream additional information related to a cursor’s movement, such as: - * - * </p> - * <ul> - * <li> - * - * The color that other member’s should display a cursor as. - * - * </li> - * <li> - * - * The ID of an element that a user may be dragging for drag and drop functionality. - * - * </li> - * <li> - * - * Details of any cursor annotations. - * - * </li> - * </ul> - * <p> - * - * Be aware that as live cursor updates are batched it is not advisable to publish data unrelated to cursor position in the `data` parameter. Use a [pub/sub channel](/channels) instead. - * - * </p> - * </aside> + * > **Note** + * > + * > The `data` parameter can be used to stream additional information related to a cursor’s movement, such as: + * > + * > - The color that other member’s should display a cursor as. + * > - The ID of an element that a user may be dragging for drag and drop functionality. + * > - Details of any cursor annotations. + * > + * > Be aware that as live cursor updates are batched it is not advisable to publish data unrelated to cursor position in the `data` parameter. Use a [pub/sub channel](/channels) instead. * * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the `set()` method: * @@ -233,13 +209,9 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L78-L90) --> * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling `set()`. Use the `subscribe()` method on the `cursors` object of a space to receive updates. * - * <aside data-type='note'> - * <p> - * - * The rate at which cursor events are published is controlled by the `outboundBatchInterval` property set in the [cursor options](#options) of a space. - * - * </p> - * </aside> + * > **Note** + * > + * > The rate at which cursor events are published is controlled by the `outboundBatchInterval` property set in the [cursor options](#options) of a space. * * The following is an example of subscribing to cursor events: * diff --git a/src/Locations.ts b/src/Locations.ts index b3f482a4..00df9933 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -112,13 +112,9 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * * All location changes are `update` events. When a location update is received, clear the highlight from the UI component of the member’s `previousLocation` and add it to `currentLocation`. * - * <aside data-type='note'> - * <p> - * - * A location update is also emitted when a member [leaves](/spaces/space#leave) a space. The member’s `currentLocation` will be `null` for these events so that any UI component highlighting can be cleared. - * - * </p> - * </aside> + * > **Note** + * > + * > A location update is also emitted when a member [leaves](/spaces/space#leave) a space. The member’s `currentLocation` will be `null` for these events so that any UI component highlighting can be cleared. * * The following is an example of subscribing to location events: * @@ -171,13 +167,9 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * | previousLocation | The previous location of the member. | Object | * | currentLocation | The new location of the member. | Object | * - * <aside data-type='further-reading'> - * <p> - * - * Member location subscription listeners only trigger on events related to members’ locations. Each event only contains the payload of the member that triggered it. Alternatively, [space state](/spaces/space) can be subscribed to which returns an array of all members with their latest state every time any event is triggered. - * - * </p> - * </aside> + * > **Further reading** + * > + * > Member location subscription listeners only trigger on events related to members’ locations. Each event only contains the payload of the member that triggered it. Alternatively, [space state](/spaces/space) can be subscribed to which returns an array of all members with their latest state every time any event is triggered. * * <!-- END WEBSITE DOCUMENTATION --> * diff --git a/src/Locks.ts b/src/Locks.ts index cdfc2771..818bc230 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -31,13 +31,9 @@ export interface LocksEventMap { * * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three [states](#states) and may only transition between states in specific circumstances. * - * <aside data-type='important'> - * <p> - * - * Optimistic locking means that there is a chance that two members may begin editing the same UI component before it is confirmed which member holds the lock. On average, the time taken to reconcile which member holds a lock is in the hundreds of milliseconds. Your application needs to handle the member that successfully obtained the lock, as well as the member that had their request invalidated. - * - * </p> - * </aside> + * > **Important** + * > + * > Optimistic locking means that there is a chance that two members may begin editing the same UI component before it is confirmed which member holds the lock. On average, the time taken to reconcile which member holds a lock is in the hundreds of milliseconds. Your application needs to handle the member that successfully obtained the lock, as well as the member that had their request invalidated. * * ## Lock states * @@ -347,13 +343,9 @@ export default class Locks extends EventEmitter<LocksEventMap> { * ``` * Releasing a lock will emit a lock event with a [lock status](#states) of `unlocked`. * - * <aside data-type='note'> - * <p> - * - * When a member [leaves](/spaces/space#leave) a space, their locks are automatically released. - * - * </p> - * </aside> + * > **Note** + * > + * > When a member [leaves](/spaces/space#leave) a space, their locks are automatically released. * * <!-- END WEBSITE DOCUMENTATION --> * diff --git a/src/Members.ts b/src/Members.ts index 60c04919..1e75db70 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -38,13 +38,9 @@ export interface MembersEventMap { * `update` * This event is emitted whenever any one of the above events is emitted. * - * <aside data-type='note'> - * <p> - * - * Members [enter](/spaces/space#enter), [leave](/spaces/space#leave), and [update](/spaces/space#update-profile) a [space](/spaces/space) directly. The `members` namespace is used to subscribe to these updates. - * - * </p> - * </aside> + * > **Note** + * > + * > Members [enter](/spaces/space#enter), [leave](/spaces/space#leave), and [update](/spaces/space#update-profile) a [space](/spaces/space) directly. The `members` namespace is used to subscribe to these updates. * * <!-- END WEBSITE DOCUMENTATION --> * @@ -344,13 +340,9 @@ class Members extends EventEmitter<MembersEventMap> { * | lastEvent.name | The most recent event emitted by the member. | String | * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * - * <aside data-type='further-reading'> - * <p> - * - * Avatar stack subscription listeners only trigger on events related to members’ online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, [space state](/spaces/space) can be subscribed to which returns an array of all members with their latest state every time any event is triggered. - * - * </p> - * </aside> + * > **Further reading** + * > + * > Avatar stack subscription listeners only trigger on events related to members’ online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, [space state](/spaces/space) can be subscribed to which returns an array of all members with their latest state every time any event is triggered. * * <!-- END WEBSITE DOCUMENTATION --> * diff --git a/src/Space.ts b/src/Space.ts index ef0bba8e..c0b76e90 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -348,18 +348,11 @@ class Space extends EventEmitter<SpaceEventMap> { * * Space state contains a single object called `members`. Any events that trigger a change in space state will always return the current state of the space as an array of `member` objects. * - * <aside data-type='note'> - * <p> - * - * [Avatar stacks](/spaces/members) and [member location](/spaces/locations) events can be subscribed to on their individual namespaces; `space.members` and `space.locations`. These events are filtered versions of space state events. Only a single [message](/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. - * - * </p> - * <p> - * - * The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event. - * - * </p> - * </aside> + * > **Note** + * > + * > [Avatar stacks](/spaces/members) and [member location](/spaces/locations) events can be subscribed to on their individual namespaces; `space.members` and `space.locations`. These events are filtered versions of space state events. Only a single [message](/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. + * > + * > The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event. * * The following is an example of subscribing to space events: * diff --git a/src/Spaces.ts b/src/Spaces.ts index 57abfcd5..6d5bb4ee 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -118,13 +118,10 @@ class Spaces { * console.log(stateChange) * }); * ``` - * <aside data-type='note'> - * <p> * - * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](/channels). - * - * </p> - * </aside> + * > **Note** + * > + * > Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](/channels). * * <!-- END WEBSITE DOCUMENTATION --> * From c652afdd8e807566a51a11f8b93526ae680f7110 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 21 Sep 2023 13:23:03 -0300 Subject: [PATCH 124/191] Make links to external documentation absolute The relative links were designed for content contained within the ably.com/docs site, which the HTML documentation we generate with TypeDoc will not necessarily be. --- src/Cursors.ts | 12 ++++++------ src/Locations.ts | 6 +++--- src/Locks.ts | 4 ++-- src/Members.ts | 6 +++--- src/Space.ts | 6 +++--- src/Spaces.ts | 8 ++++---- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 6fc239cf..c3824a0f 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -36,11 +36,11 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * Live cursors build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. + * Live cursors build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. * - * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the [space state](/spaces/space) and may only be subscribed to on the `cursors` namespace. + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](https://ably.com/docs/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the [space state](/spaces/space) and may only be subscribed to on the `cursors` namespace. * - * The channel is only created when a member calls `space.cursors.set()`. The live cursors channel object can be accessed through `space.cursors.channel`. To monitor the [underlying state of the cursors channel](/channels#states), the channel object can be accessed through `space.cursors.channel`. + * The channel is only created when a member calls `space.cursors.set()`. The live cursors channel object can be accessed through `space.cursors.channel`. To monitor the [underlying state of the cursors channel](https://ably.com/docs/channels#states), the channel object can be accessed through `space.cursors.channel`. * * <!-- END WEBSITE DOCUMENTATION --> * @@ -98,7 +98,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * > - The ID of an element that a user may be dragging for drag and drop functionality. * > - Details of any cursor annotations. * > - * > Be aware that as live cursor updates are batched it is not advisable to publish data unrelated to cursor position in the `data` parameter. Use a [pub/sub channel](/channels) instead. + * > Be aware that as live cursor updates are batched it is not advisable to publish data unrelated to cursor position in the `data` parameter. Use a [pub/sub channel](https://ably.com/docs/channels) instead. * * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the `set()` method: * @@ -128,8 +128,8 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * * | Property | Description | Type | * |--------------|----------------------------------------------------------------------------------------------------|--------| - * | connectionId | The unique identifier of the member’s [connection](/connect). | String | - * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | + * | clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | * | position | An object containing the position of a member’s cursor. | Object | * | position.x | The position of the member’s cursor on the X-axis. | Number | * | position.y | The position of the member’s cursor on the Y-axis. | Number | diff --git a/src/Locations.ts b/src/Locations.ts index 00df9933..663e22bf 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -31,7 +31,7 @@ export interface LocationsEventMap { * * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * Member locations build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). + * Member locations build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). * * <!-- END WEBSITE DOCUMENTATION --> * @@ -158,8 +158,8 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * * | Property | Description | Type | * |----------------------------|-----------------------------------------------------------------------------------------------------------------------|---------| - * | member.clientId | The [client identifier](/auth/identified-clients) for the member. | String | - * | member.connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | member.clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | + * | member.connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | * | member.lastEvent.name | The most recent [event](/spaces/avatar) emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | diff --git a/src/Locks.ts b/src/Locks.ts index 818bc230..516edc52 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -433,8 +433,8 @@ export default class Locks extends EventEmitter<LocksEventMap> { * | timestamp | The timestamp of the lock event. | Number | * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | * | reason | The reason why the `request.status` is `unlocked`. | ErrorInfo | - * | member.clientId | The [client identifier](/auth/identified-clients) for the member. | String | - * | member.connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | member.clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | + * | member.connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | * | member.lastEvent.name | The most recent [event](/spaces/avatar#events) emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | diff --git a/src/Members.ts b/src/Members.ts index 1e75db70..55d310a3 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -49,7 +49,7 @@ export interface MembersEventMap { * * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * Avatar stacks build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). + * Avatar stacks build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). * * <!-- END WEBSITE DOCUMENTATION --> * @@ -332,8 +332,8 @@ class Members extends EventEmitter<MembersEventMap> { * * | Property | Description | Type | * |---------------------|-------------------------------------------------------------------------------------------------------------------|---------| - * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | - * | connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | + * | connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | * | isConnected | Whether the member is connected to Ably or not. | Boolean | * | profileData | The optional [profile data](#profile-data) associated with the member. | Object | * | location | The current [location](/spaces/locations) of the member. Will be `null` for `enter`, `leave` and `remove` events. | Object | diff --git a/src/Space.ts b/src/Space.ts index c0b76e90..ce59de3f 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -350,7 +350,7 @@ class Space extends EventEmitter<SpaceEventMap> { * * > **Note** * > - * > [Avatar stacks](/spaces/members) and [member location](/spaces/locations) events can be subscribed to on their individual namespaces; `space.members` and `space.locations`. These events are filtered versions of space state events. Only a single [message](/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. + * > [Avatar stacks](/spaces/members) and [member location](/spaces/locations) events can be subscribed to on their individual namespaces; `space.members` and `space.locations`. These events are filtered versions of space state events. Only a single [message](https://ably.com/docs/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. * > * > The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event. * @@ -400,8 +400,8 @@ class Space extends EventEmitter<SpaceEventMap> { * * | Property | Description | Type | * |---------------------|------------------------------------------------------------------------|---------| - * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | - * | connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | + * | connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | * | isConnected | Whether the member is connected to Ably or not. | Boolean | * | profileData | The optional [profile data](#profile-data) associated with the member. | Object | * | location | The current [location](/spaces/locations) of the member. | Object | diff --git a/src/Spaces.ts b/src/Spaces.ts index 6d5bb4ee..28e34d6b 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -107,9 +107,9 @@ class Spaces { * * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * A space is created as an Ably [channel](/channels). Members [attach](/channels#attach) to the channel and join its [presence set](/presence-occupancy/presence) when they [enter](#enter) the space. Avatar stacks, member locations and component locking are all handled on this channel. + * A space is created as an Ably [channel](/channels). Members [attach](https://ably.com/docs/channels#attach) to the channel and join its [presence set](https://ably.com/docs/presence-occupancy/presence) when they [enter](#enter) the space. Avatar stacks, member locations and component locking are all handled on this channel. * - * To manage the state of the space, you can monitor the [state of the underlying channel](/channels#states). The channel object can be accessed through `space.channel`. + * To manage the state of the space, you can monitor the [state of the underlying channel](https://ably.com/docs/channels#states). The channel object can be accessed through `space.channel`. * * The following is an example of registering a listener to wait for a channel to become attached: * @@ -121,7 +121,7 @@ class Spaces { * * > **Note** * > - * > Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](/channels). + * > Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](https://ably.com/docs/channels). * * <!-- END WEBSITE DOCUMENTATION --> * @@ -140,7 +140,7 @@ class Spaces { * * ### paginationLimit * - * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members’ cursor is obtained from [history](/storage-history/history). The `paginationLimit` is the number of pages that should be searched to find the last position of each cursor. The default is 5. + * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members’ cursor is obtained from [history](https://ably.com/docs/storage-history/history). The `paginationLimit` is the number of pages that should be searched to find the last position of each cursor. The default is 5. * * <!-- END WEBSITE DOCUMENTATION --> * From 20b9f6c01518fbdb56eb8e76a3dc8a403dd7d52d Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 21 Sep 2023 14:18:00 -0300 Subject: [PATCH 125/191] Convert documentation links into TSDoc @link --- src/Cursors.ts | 16 ++++++++-------- src/Locations.ts | 24 ++++++++++++------------ src/Locks.ts | 40 ++++++++++++++++++++-------------------- src/Members.ts | 48 ++++++++++++++++++++++++------------------------ src/Space.ts | 46 +++++++++++++++++++++++----------------------- src/Spaces.ts | 18 +++++++++--------- src/types.ts | 2 +- 7 files changed, 97 insertions(+), 97 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index c3824a0f..788dccec 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -23,7 +23,7 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * Cursor events are emitted whenever a member moves their mouse within a space. In order to optimize the efficiency and frequency of updates, cursor position events are automatically batched. The batching interval may be customized in order to further optimize for increased performance versus the number of events published. * - * Live cursor updates are not available as part of the [space state](/spaces/space#subscribe) and must be subscribed to using [`space.cursors.subscribe()`](#subscribe). + * Live cursor updates are not available as part of the {@link Space.subscribe | space state} and must be subscribed to using {@link Cursors.subscribe | `space.cursors.subscribe()`}. * * > **Important** * > @@ -38,14 +38,14 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * Live cursors build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. * - * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](https://ably.com/docs/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the [space state](/spaces/space) and may only be subscribed to on the `cursors` namespace. + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](https://ably.com/docs/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the {@link Space.subscribe | space state } and may only be subscribed to on the `cursors` namespace. * * The channel is only created when a member calls `space.cursors.set()`. The live cursors channel object can be accessed through `space.cursors.channel`. To monitor the [underlying state of the cursors channel](https://ably.com/docs/channels#states), the channel object can be accessed through `space.cursors.channel`. * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Handles tracking of member cursors within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * Handles tracking of member cursors within a space. Inherits from {@link EventEmitter}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export default class Cursors extends EventEmitter<CursorsEventMap> { @@ -80,7 +80,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L21-L74) --> * Set the position of a member’s cursor using the `set()` method. A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Calling `set()` will emit a cursor event so that other members are informed of the cursor movement in realtime. * - * A member must have been [entered](/spaces/space#enter) into the space to set their cursor position. + * A member must have been { @link Space.enter | entered } into the space to set their cursor position. * * The `set()` method takes the following parameters: * @@ -211,7 +211,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * * > **Note** * > - * > The rate at which cursor events are published is controlled by the `outboundBatchInterval` property set in the [cursor options](#options) of a space. + * > The rate at which cursor events are published is controlled by the `outboundBatchInterval` property set in the {@link CursorsOptions | cursor options } of a space. * * The following is an example of subscribing to cursor events: * @@ -223,13 +223,13 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Listen to `CursorUpdate` events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * Listen to `CursorUpdate` events. See {@link EventEmitter} for overloaded usage. * * Available events: * * - ##### **update** * - * Emits an event when a new cursor position is set. The argument supplied to the event listener is a [CursorUpdate](#cursorupdate). + * Emits an event when a new cursor position is set. The argument supplied to the event listener is a {@link CursorUpdate}. * * ```ts * space.cursors.subscribe('update', (cursorUpdate: CursorUpdate) => {}); @@ -283,7 +283,7 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * Remove all event listeners, all event listeners for an event, or specific listeners. See {@link EventEmitter} for detailed usage. * * ```ts * space.cursors.unsubscribe('update'); diff --git a/src/Locations.ts b/src/Locations.ts index 663e22bf..00bf980d 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -31,12 +31,12 @@ export interface LocationsEventMap { * * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * Member locations build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). + * Member locations build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. Members are entered into the presence set when they {@link Space.enter | enter the space}. * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Handles the tracking of member locations within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * Handles the tracking of member locations within a space. Inherits from {@link EventEmitter}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export default class Locations extends EventEmitter<LocationsEventMap> { @@ -80,7 +80,7 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L15-L25) --> * Use the `set()` method to emit a location event in realtime when a member changes their location. This will be received by all location subscribers to inform them of the location change. A `location` can be any JSON-serializable object, such as a slide number or element ID. * - * A member must have been [entered](/spaces/space#enter) into the space to set their location. + * A member must have been { @link Space.enter | entered } into the space to set their location. * * The `set()` method is commonly combined with [`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) or a React [synthetic event](https://react.dev/learn/responding-to-events#adding-event-handlers), such as `onClick` or `onHover`. * @@ -108,13 +108,13 @@ export default class Locations extends EventEmitter<LocationsEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L29-L91) --> - * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling [`set()`](#set). Use the `subscribe()` method on the `locations` namespace of the space to receive updates. + * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling {@link set}. Use the `subscribe()` method on the `locations` namespace of the space to receive updates. * - * All location changes are `update` events. When a location update is received, clear the highlight from the UI component of the member’s `previousLocation` and add it to `currentLocation`. + * All location changes are {@link LocationsEventMap.update | `update`} events. When a location update is received, clear the highlight from the UI component of the member’s {@link LocationsEvents.UpdateEvent.previousLocation | `previousLocation`} and add it to {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation`}. * * > **Note** * > - * > A location update is also emitted when a member [leaves](/spaces/space#leave) a space. The member’s `currentLocation` will be `null` for these events so that any UI component highlighting can be cleared. + * > A location update is also emitted when a member {@link Space.leave | leaves} a space. The member’s {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation` } will be `null` for these events so that any UI component highlighting can be cleared. * * The following is an example of subscribing to location events: * @@ -123,7 +123,7 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * console.log(locationUpdate); * }); * ``` - * The following is an example payload of a location event. Information about location is returned in `currentLocation` and `previousLocation`: + * The following is an example payload of a location event. Information about location is returned in {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation`} and {@link LocationsEvents.UpdateEvent.previousLocation | `previousLocation`}: * * ```json * { @@ -161,20 +161,20 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * | member.clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | * | member.connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | - * | member.lastEvent.name | The most recent [event](/spaces/avatar) emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | + * | member.lastEvent.name | The most recent { @link MembersEventMap | event } emitted by the member. Will be one of {@link MembersEventMap.enter | `enter`}, {@link MembersEventMap.update | `update`}, {@link MembersEventMap.leave | `leave`} or {@link MembersEventMap.remove | `remove`}. | String | * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * | member.profileData | The optional [profile data](/spaces/avatar#profile-data) associated with the member. | Object | + * | member.profileData | The optional {@link Space.updateProfileData | profile data } associated with the member. | Object | * | previousLocation | The previous location of the member. | Object | * | currentLocation | The new location of the member. | Object | * * > **Further reading** * > - * > Member location subscription listeners only trigger on events related to members’ locations. Each event only contains the payload of the member that triggered it. Alternatively, [space state](/spaces/space) can be subscribed to which returns an array of all members with their latest state every time any event is triggered. + * > Member location subscription listeners only trigger on events related to members’ locations. Each event only contains the payload of the member that triggered it. Alternatively, {@link Space.subscribe | space state } can be subscribed to which returns an array of all members with their latest state every time any event is triggered. * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * Listen to events for locations. See {@link EventEmitter} for overloaded usage. * * Available events: * @@ -226,7 +226,7 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * Remove all event listeners, all event listeners for an event, or specific listeners. See {@link EventEmitter} for detailed usage. * * ```ts * space.locations.unsubscribe('update'); diff --git a/src/Locks.ts b/src/Locks.ts index 516edc52..43f0feb5 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -29,7 +29,7 @@ export interface LocksEventMap { * * Once a lock has been acquired by a member, the component that it relates to can be updated in the UI to visually indicate to other members that it is locked and and which member has the lock. The component can then be updated once the editing member has released the lock to indicate that it is now unlocked. * - * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three [states](#states) and may only transition between states in specific circumstances. + * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three { @link LockStatus | states } and may only transition between states in specific circumstances. * * > **Important** * > @@ -37,33 +37,33 @@ export interface LocksEventMap { * * ## Lock states * - * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call [`acquire()`](#acquire) on the lock identifier related to it. Alternatively, you could wait until they receive a `locked` event and display a spinning symbol in the UI until this is received. In either case a subsequent `unlocked` event may invalidate that member’s lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. + * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call {@link acquire | `acquire()` } on the lock identifier related to it. Alternatively, you could wait until they receive a `locked` event and display a spinning symbol in the UI until this is received. In either case a subsequent `unlocked` event may invalidate that member’s lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. * * A lock will be in one of the following states: * * `pending` - * A member has requested a lock by calling [`acquire()`](#acquire). + * A member has requested a lock by calling {@link acquire | `acquire()`}. * * `locked` * The lock is confirmed to be held by the requesting member. * * `unlocked` - * The lock is confirmed to not be locked by the requesting member, or has been [released](#release) by a member previously holding the lock. + * The lock is confirmed to not be locked by the requesting member, or has been {@link release | released } by a member previously holding the lock. * * The following lock state transitions may occur: * - * - None → `pending`: a member calls [`acquire()`](#acquire) to request a lock. + * - None → `pending`: a member calls {@link acquire | `acquire()` } to request a lock. * - `pending` → `locked`: the requesting member holds the lock. * - `pending` → `unlocked`: the requesting member does not hold the lock, since another member already holds it. - * - `locked` → `unlocked`: the lock was either explicitly [released](#release) by the member, or their request was invalidated by a concurrent request which took precedence. + * - `locked` → `unlocked`: the lock was either explicitly {@link release | released} by the member, or their request was invalidated by a concurrent request which took precedence. * - `unlocked` → `locked`: the requesting member reacquired a lock they previously held. * - * Only transitions that result in a `locked` or `unlocked` status will emit a lock event that members can [`subscribe()`](#subscribe) to. + * Only transitions that result in a `locked` or `unlocked` status will emit a lock event that members can {@link subscribe | `subscribe()` } to. * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Provides a mechanism to "lock" a component, reducing the chances of conflict in an application whilst being edited by multiple members. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * Provides a mechanism to "lock" a component, reducing the chances of conflict in an application whilst being edited by multiple members. Inherits from {@link EventEmitter}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export default class Locks extends EventEmitter<LocksEventMap> { @@ -256,7 +256,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L41-L72) --> * Use the `acquire()` method to attempt to acquire a lock with a given unique ID. Additional `attributes` may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using `attributes` is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. * - * A member must have been [entered](/spaces/space#enter) into the space to acquire a lock. + * A member must have been {@link Space.enter | entered } into the space to acquire a lock. * * The following is an example of attempting to acquire a lock: * @@ -282,12 +282,12 @@ export default class Locks extends EventEmitter<LocksEventMap> { * } * } * ``` - * Once a member requests a lock by calling `acquire()`, the lock is temporarily in the [pending state](#states). An event will be emitted based on whether the lock request was successful (a status of `locked`) or invalidated (a status of `unlocked`). This can be [subscribed](#subscribe) to in order for the client to know whether their lock request was successful or not. + * Once a member requests a lock by calling `acquire()`, the lock is temporarily in the {@link LockStatuses.Pending | pending state }. An event will be emitted based on whether the lock request was successful (a status of `locked`) or invalidated (a status of `unlocked`). This can be {@link subscribe | subscribed } to in order for the client to know whether their lock request was successful or not. * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Send a request to acquire a lock. Returns a Promise which resolves once the request has been sent. A resolved Promise holds a `pending` [Lock](#lock). An error will be thrown if a lock request with a status of `pending` or `locked` already exists, returning a rejected promise. + * Send a request to acquire a lock. Returns a Promise which resolves once the request has been sent. A resolved Promise holds a `pending` {@link Lock}. An error will be thrown if a lock request with a status of `pending` or `locked` already exists, returning a rejected promise. * * When a lock acquisition by a member is confirmed with the `locked` status, an `update` event will be emitted. Hence to handle lock acquisition, `acquire()` needs to always be used together with `subscribe()`. * @@ -341,11 +341,11 @@ export default class Locks extends EventEmitter<LocksEventMap> { * ```javascript * await space.locks.release(id); * ``` - * Releasing a lock will emit a lock event with a [lock status](#states) of `unlocked`. + * Releasing a lock will emit a lock event with a {@link LockStatuses | lock status } of `unlocked`. * * > **Note** * > - * > When a member [leaves](/spaces/space#leave) a space, their locks are automatically released. + * > When a member {@link Space.leave | leaves } a space, their locks are automatically released. * * <!-- END WEBSITE DOCUMENTATION --> * @@ -380,7 +380,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L92-L151) --> - * Subscribe to lock events by registering a listener. Lock events are emitted whenever the [lock state](#states) transitions into `locked` or `unlocked`. Use the `subscribe()` method on the `locks` namespace of the space to receive updates. + * Subscribe to lock events by registering a listener. Lock events are emitted whenever the {@link LockStatuses | lock state } transitions into `locked` or `unlocked`. Use the `subscribe()` method on the `locks` namespace of the space to receive updates. * * All lock events are `update` events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. * @@ -429,21 +429,21 @@ export default class Locks extends EventEmitter<LocksEventMap> { * | Property | Description | Type | * |----------------------------|------------------------------------------------------------------------------------------------------------------------------|-----------| * | id | The unique ID of the lock request. | String | - * | status | The lock [status](#states) of the event. Will be one of `locked`, `unlocked` or `pending`. | String | + * | status | The lock {@link LockStatues | status } of the event. Will be one of `locked`, `unlocked` or `pending`. | String | * | timestamp | The timestamp of the lock event. | Number | * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | * | reason | The reason why the `request.status` is `unlocked`. | ErrorInfo | * | member.clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | * | member.connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | - * | member.lastEvent.name | The most recent [event](/spaces/avatar#events) emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | + * | member.lastEvent.name | The most recent { @link Members.subscribe | event } emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * | member.profileData | The optional [profile data](/spaces/avatar#profile-data) associated with the member. | Object | + * | member.profileData | The optional {@link Space.updateProfileData | profile data } associated with the member. | Object | * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Listen to lock events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * Listen to lock events. See {@link EventEmitter} for overloaded usage. * * Available events: * @@ -455,7 +455,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { * space.locks.subscribe('update', (lock: Lock) => {}) * ``` * - * The argument supplied to the callback is a [Lock](#lock), representing the lock request and it's status. + * The argument supplied to the callback is a {@link Lock}, representing the lock request and it's status. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ subscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; @@ -494,7 +494,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * Remove all event listeners, all event listeners for an event, or specific listeners. See {@link EventEmitter} for detailed usage. * * ```ts * space.locks.unsubscribe('update'); diff --git a/src/Members.ts b/src/Members.ts index 55d310a3..797db97e 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -24,23 +24,23 @@ export interface MembersEventMap { * The following four event types are emitted by members: * * `enter` - * A new member has entered the space. The member has either entered explicitly by calling [`space.enter()`](/spaces/space#enter), or has attempted to update their profile data before entering a space, which will instead emit an `enter` event. + * A new member has entered the space. The member has either entered explicitly by calling {@link Space.enter | `space.enter()`}, or has attempted to update their profile data before entering a space, which will instead emit an `enter` event. * * `updateProfile` - * A member has updated their profile data by calling [`space.updateProfileData()`](/spaces/space#update-profile). + * A member has updated their profile data by calling {@link Space.updateProfileData | `space.updateProfileData()`. * * `leave` - * A member has left the space. The member has either left explicitly by calling [`space.leave()`](/spaces/space#leave), or has abruptly disconnected and not re-established a connection within 15 seconds. + * A member has left the space. The member has either left explicitly by calling {@link Space.leave | `space.leave()` }, or has abruptly disconnected and not re-established a connection within 15 seconds. * * `remove` - * A member has been removed from the members list after the [`offlineTimeout`](/spaces/space#options) period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their `leave` and `remove` events. + * A member has been removed from the members list after the {@link SpaceOptions.offlineTimeout | `offlineTimeout` } period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their `leave` and `remove` events. * * `update` * This event is emitted whenever any one of the above events is emitted. * * > **Note** * > - * > Members [enter](/spaces/space#enter), [leave](/spaces/space#leave), and [update](/spaces/space#update-profile) a [space](/spaces/space) directly. The `members` namespace is used to subscribe to these updates. + * > Members {@link Space.enter | enter }, {@link Space.leave | leave }, and {@link Space.updateProfileData | update } a {@link Space | space } directly. The `members` namespace is used to subscribe to these updates. * * <!-- END WEBSITE DOCUMENTATION --> * @@ -49,7 +49,7 @@ export interface MembersEventMap { * * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * Avatar stacks build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). + * Avatar stacks build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. Members are entered into the presence set when they { @link Space.enter | enter the space }. * * <!-- END WEBSITE DOCUMENTATION --> * @@ -100,7 +100,7 @@ class Members extends EventEmitter<MembersEventMap> { * See the documentation for {@link getAll}. * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `null` if the client hasn't entered the space yet. + * Returns a Promise which resolves to the {@link SpaceMember} object relating to the local connection. Will resolve to `null` if the client hasn't entered the space yet. * * Example: * @@ -115,7 +115,7 @@ class Members extends EventEmitter<MembersEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L129-L250) --> - * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member’s own profile data to them, or retrieving a list of all other members to use to [update their profile data](/spaces/space#update-profile). + * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member’s own profile data to them, or retrieving a list of all other members to use to {@link Space.updateProfileData | update their profile data }. * * The following is an example of retrieving a member’s own member object: * @@ -235,7 +235,7 @@ class Members extends EventEmitter<MembersEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, including any who have left and not yet timed out. (_see: [offlineTimeout](#spaceoptions)_) + * Returns a Promise which resolves to an array of all {@link SpaceMember} objects (members) currently in the space, including any who have left and not yet timed out. (_see: {@link SpaceOptions.offlineTimeout | offlineTimeout}_) * * Example: * @@ -255,7 +255,7 @@ class Members extends EventEmitter<MembersEventMap> { * See the documentation for {@link getAll}. * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, excluding your own member object. + * Returns a Promise which resolves to an array of all {@link SpaceMember} objects (members) currently in the space, excluding your own member object. * * Example: * @@ -271,7 +271,7 @@ class Members extends EventEmitter<MembersEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L29-L103) --> - * Subscribe to members’ online status and profile updates by registering a listener. Member events are emitted whenever a member [enters](/spaces/space#enter) or [leaves](/spaces/space#leave) the space, or updates their profile data. Use the `subscribe()` method on the `members` object of a space to receive updates. + * Subscribe to members’ online status and profile updates by registering a listener. Member events are emitted whenever a member {@link Space.enter | enters} or {@link Space.leave | leaves} the space, or updates their profile data. Use the `subscribe()` method on the `members` object of a space to receive updates. * * The following is an example of subscribing to the different member event types: * @@ -310,7 +310,7 @@ class Members extends EventEmitter<MembersEventMap> { * console.log(memberUpdate); * }); * ``` - * The following is an example payload of a member event. The `lastEvent.name` describes which [event type](#events) a payload relates to. + * The following is an example payload of a member event. The `lastEvent.name` describes which {@link SpaceEventMap | event type } a payload relates to. * * ```json * { @@ -335,21 +335,21 @@ class Members extends EventEmitter<MembersEventMap> { * | clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | * | connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | * | isConnected | Whether the member is connected to Ably or not. | Boolean | - * | profileData | The optional [profile data](#profile-data) associated with the member. | Object | - * | location | The current [location](/spaces/locations) of the member. Will be `null` for `enter`, `leave` and `remove` events. | Object | + * | profileData | The optional {@link Space.updateProfileData | profile data } associated with the member. | Object | + * | location | The current {@link Locations | location} of the member. Will be `null` for `enter`, `leave` and `remove` events. | Object | * | lastEvent.name | The most recent event emitted by the member. | String | * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * * > **Further reading** * > - * > Avatar stack subscription listeners only trigger on events related to members’ online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, [space state](/spaces/space) can be subscribed to which returns an array of all members with their latest state every time any event is triggered. + * > Avatar stack subscription listeners only trigger on events related to members’ online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, {@link Space.subscribe | space state } can be subscribed to which returns an array of all members with their latest state every time any event is triggered. * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * Listen to member events for the space. See {@link EventEmitter} for overloaded usage. * - * The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event. + * The argument supplied to the callback is the {@link SpaceMember} object representing the member that triggered the event. * * Example: * @@ -366,7 +366,7 @@ class Members extends EventEmitter<MembersEventMap> { * ```ts * space.members.subscribe('enter', (member: SpaceMember) => {}) * ``` - * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. + * The argument supplied to the callback is a {@link SpaceMember} object representing the member entering the space. * * - ##### **leave** * @@ -376,17 +376,17 @@ class Members extends EventEmitter<MembersEventMap> { * space.members.subscribe('leave', (member: SpaceMember) => {}) * ``` * - * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member leaving the space. + * The argument supplied to the callback is a {@link SpaceMember} object representing the member leaving the space. * * - ##### **remove** * - * Listen to remove events of members. The remove event will be triggered when the [offlineTimeout](#spaceoptions) has passed. + * Listen to remove events of members. The remove event will be triggered when the {@link SpaceOptions.offlineTimeout | offlineTimeout } has passed. * * ```ts * space.members.subscribe('remove', (member: SpaceMember) => {}) * ``` * - * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member removed from the space. + * The argument supplied to the callback is a {@link SpaceMember} object representing the member removed from the space. * * - ##### **updateProfile** * @@ -395,7 +395,7 @@ class Members extends EventEmitter<MembersEventMap> { * ```ts * space.members.subscribe('updateProfile', (member: SpaceMember) => {}) * ``` - * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. + * The argument supplied to the callback is a {@link SpaceMember} object representing the member entering the space. * * - ##### **update** * @@ -405,7 +405,7 @@ class Members extends EventEmitter<MembersEventMap> { * space.members.subscribe('update', (member: SpaceMember) => {}) * ``` * - * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member affected by the change. + * The argument supplied to the callback is a {@link SpaceMember} object representing the member affected by the change. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ subscribe<K extends keyof MembersEventMap>( @@ -452,7 +452,7 @@ class Members extends EventEmitter<MembersEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Remove all the event listeners or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * Remove all the event listeners or specific listeners. See {@link EventEmitter} for detailed usage. * * ```ts * // Unsubscribe from all events diff --git a/src/Space.ts b/src/Space.ts index ce59de3f..7259a02f 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -48,21 +48,21 @@ export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileDat * * The following features can be implemented within a space: * - * - [Avatar stack](/spaces/avatar) - * - [Member location](/spaces/locations) - * - [Live cursors](/spaces/cursors) - * - [Component locking](/spaces/locking) + * - Avatar stack, via the {@link members | `members`} property + * - Member location, via the {@link locations | `locations`} property + * - Live cursors, via the {@link cursors | `cursors`} property + * - Component locking, via the {@link locks | `locks`} property * * The `space` namespace consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member’s location within the application. The position of members’ cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. * - * Space state can be [subscribed](#subscribe) to in the `space` namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. + * Space state can be {@link subscribe | subscribed} to in the `space` namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. * * To subscribe to any events in a space, you first need to create or retrieve a space. * * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * An instance of a Space created using [spaces.get](#get). Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * An instance of a Space created using {@link default.get | spaces.get}. Inherits from {@link EventEmitter}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ class Space extends EventEmitter<SpaceEventMap> { @@ -78,19 +78,19 @@ class Space extends EventEmitter<SpaceEventMap> { readonly options: SpaceOptions; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * An instance of [Locations](#locations). + * An instance of {@link Locations}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ readonly locations: Locations; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * An instance of [Cursors](#cursors). + * An instance of {@link Cursors}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ readonly cursors: Cursors; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * An instance of [Members](#members). + * An instance of {@link Members}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ readonly members: Members; @@ -163,13 +163,13 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L43-L55) --> - * Entering a space will register a client as a member and emit an [`enter`](/spaces/members#events) event to all subscribers. Use the `enter()` method to enter a space. + * Entering a space will register a client as a member and emit an {@link MembersEventMap.enter | `enter` } event to all subscribers. Use the `enter()` method to enter a space. * * Being entered into a space is required for members to: * - * - Update their [profile data](#update-profile). - * - Set their [location](/spaces/locations). - * - Set their [cursor position](/spaces/cursors). + * - { @link updateProfileData | Update their profile data. } + * - { @link Locations.set | Set their location. } + * - { @link Cursors.set | Set their cursor position. } * * The following is an example of entering a space: * @@ -179,7 +179,7 @@ class Space extends EventEmitter<SpaceEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L71-L82) --> - * Profile data can be set when [entering](#enter) a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. + * Profile data can be set when {@link enter | entering} a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. * * Profile data is returned in the payload of all space events. * @@ -194,7 +194,7 @@ class Space extends EventEmitter<SpaceEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the [member object](#spacemember). Returns all current space members. + * Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the {@link SpaceMember | member object }. Returns all current space members. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async enter(profileData: ProfileData = null): Promise<SpaceMember[]> { @@ -221,7 +221,7 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L86-L103) --> - * Profile data can be updated at any point after entering a space by calling `updateProfileData()`. This will emit an `update` event. If a client hasn’t yet entered the space, `updateProfileData()` will instead [enter the space](#enter), with the profile data, and emit an [`enter`](/spaces/members#events) event. + * Profile data can be updated at any point after entering a space by calling `updateProfileData()`. This will emit an `update` event. If a client hasn’t yet entered the space, `updateProfileData()` will instead {@link enter | enter the space }, with the profile data, and emit an { @link MembersEventMap.enter | `enter` } event. * * The following is an example of updating profile data: * @@ -241,7 +241,7 @@ class Space extends EventEmitter<SpaceEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Update `profileData`. This data can be an arbitrary JSON-serializable object which is attached to the [member object](#spacemember). If the connection + * Update `profileData`. This data can be an arbitrary JSON-serializable object which is attached to the {@link SpaceMember | member object }. If the connection * has not entered the space, calling `updateProfileData` will call `enter` instead. * * A function can also be passed in. This function will receive the existing `profileData` and lets you update based on the existing value of `profileData`: @@ -283,7 +283,7 @@ class Space extends EventEmitter<SpaceEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L59-L67) --> - * Leaving a space will emit a [`leave`](/spaces/members#events) event to all subscribers. + * Leaving a space will emit a { @link MembersEventMap.leave | `leave` } event to all subscribers. * * The following is an example of explicitly leaving a space: * @@ -295,7 +295,7 @@ class Space extends EventEmitter<SpaceEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See [offlineTimeout](#spaceoptions). + * Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See {@link SpaceOptions.offlineTimeout | offlineTimeout }. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async leave(profileData: ProfileData = null) { @@ -342,7 +342,7 @@ class Space extends EventEmitter<SpaceEventMap> { * * - A member enters the space * - A member leaves the space - * - A member is removed from the space state [after the offlineTimeout period](#options) has elapsed + * - A member is removed from the space state { @link SpaceOptions.offlineTimeout | after the offlineTimeout period } has elapsed * - A member updates their profile data * - A member sets a new location * @@ -350,7 +350,7 @@ class Space extends EventEmitter<SpaceEventMap> { * * > **Note** * > - * > [Avatar stacks](/spaces/members) and [member location](/spaces/locations) events can be subscribed to on their individual namespaces; `space.members` and `space.locations`. These events are filtered versions of space state events. Only a single [message](https://ably.com/docs/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. + * > Avatar stacks and member location events can be subscribed to on their individual namespaces; {@link Space.members | `space.members` } and {@link Space.locations | `space.locations`}. These events are filtered versions of space state events. Only a single [message](https://ably.com/docs/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. * > * > The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event. * @@ -403,8 +403,8 @@ class Space extends EventEmitter<SpaceEventMap> { * | clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | * | connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | * | isConnected | Whether the member is connected to Ably or not. | Boolean | - * | profileData | The optional [profile data](#profile-data) associated with the member. | Object | - * | location | The current [location](/spaces/locations) of the member. | Object | + * | profileData | The optional {@link updateProfileData | profile data } associated with the member. | Object | + * | location | The current { @link Locations | location } of the member. | Object | * | lastEvent.name | The most recent event emitted by the member. | String | * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * diff --git a/src/Spaces.ts b/src/Spaces.ts index 28e34d6b..c717e2a2 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -18,13 +18,13 @@ class Spaces { private spaces: Record<string, Space> = {}; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Instance of the [ably-js](https://github.com/ably/ably-js) client that was passed to the [constructor](#constructor). + * Instance of the [ably-js](https://github.com/ably/ably-js) client that was passed to the {@link constructor}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ client: Types.RealtimePromise; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Instance of the [ably-js](https://github.com/ably/ably-js) connection, belonging to the client that was passed to the [constructor](#constructor). + * Instance of the [ably-js](https://github.com/ably/ably-js) connection, belonging to the client that was passed to the {@link constructor}. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ connection: Types.ConnectionPromise; @@ -61,7 +61,7 @@ class Spaces { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L26-L39) --> - * A `space` object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the `spaces` collection using the `get()` method. + * A `space` object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the `spaces` collection using the {@link get | `get()`} method. * * The following restrictions apply to space names: * @@ -84,14 +84,14 @@ class Spaces { * * ### Space options * - * An additional set of optional properties may be passed when [creating or retrieving](#create) a space to customize the behavior of different features. + * An additional set of optional properties may be passed when {@link get | creating or retrieving } a space to customize the behavior of different features. * * The following properties can be customized: * * | Property | Description | Type | * |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| * | offlineTimeout | Number of milliseconds after a member loses connection or closes their browser window to wait before they are removed from the member list. The default is 120,000ms (2 minutes). | Number | - * | cursors | A [cursor options](/spaces/cursors#options) object for customizing live cursor behavior. | Object | + * | cursors | A {@link CursorsOptions | cursor options} object for customizing live cursor behavior. | Object | * | cursors.outboundBatchInterval | The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 100ms. | Number | * | cursors.paginationLimit | The number of pages searched from history for the last published cursor position. The default is 5. | Number | * @@ -107,9 +107,9 @@ class Spaces { * * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * A space is created as an Ably [channel](/channels). Members [attach](https://ably.com/docs/channels#attach) to the channel and join its [presence set](https://ably.com/docs/presence-occupancy/presence) when they [enter](#enter) the space. Avatar stacks, member locations and component locking are all handled on this channel. + * A space is created as an Ably [channel](/channels). Members [attach](https://ably.com/docs/channels#attach) to the channel and join its [presence set](https://ably.com/docs/presence-occupancy/presence) when they { @link Space.enter | enter } the space. Avatar stacks, member locations and component locking are all handled on this channel. * - * To manage the state of the space, you can monitor the [state of the underlying channel](https://ably.com/docs/channels#states). The channel object can be accessed through `space.channel`. + * To manage the state of the space, you can monitor the [state of the underlying channel](https://ably.com/docs/channels#states). The channel object can be accessed through {@link Space.channel | `space.channel`}. * * The following is an example of registering a listener to wait for a channel to become attached: * @@ -128,7 +128,7 @@ class Spaces { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L108-L122) --> * ## Cursor options * - * Cursor options are set when creating or retrieving a `Space` instance. They are used to control the behavior of live cursors. + * Cursor options are set when creating or retrieving a {@link Space | `Space` } instance. They are used to control the behavior of live cursors. * * The following cursor options can be set: * @@ -145,7 +145,7 @@ class Spaces { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Get or create a Space instance. Returns a [Space](#space) instance. Configure the space by passing [SpaceOptions](#spaceoptions) as the second argument. + * Get or create a Space instance. Returns a {@link Space} instance. Configure the space by passing {@link SpaceOptions} as the second argument. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ async get(name: string, options?: Subset<SpaceOptions>): Promise<Space> { diff --git a/src/types.ts b/src/types.ts index 6e4e40f9..b6a1bb70 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,7 +53,7 @@ export interface CursorUpdate { export interface SpaceOptions { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> - * Number of milliseconds after a user loses connection or closes their browser window to wait before their [SpaceMember](#spacemember) object is removed from the members list. The default is 120000ms (2 minutes). + * Number of milliseconds after a user loses connection or closes their browser window to wait before their {@link SpaceMember} object is removed from the members list. The default is 120000ms (2 minutes). * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ offlineTimeout: number; From 0024faf0bd34925a034ac8febd2be93ada97a2cb Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 21 Sep 2023 16:05:38 -0300 Subject: [PATCH 126/191] Tweak references to "namespace" in docstrings added in e1b9359 I think it reads a bit better, at least in this context. --- src/Cursors.ts | 2 +- src/Locations.ts | 2 +- src/Locks.ts | 2 +- src/Members.ts | 4 ++-- src/Space.ts | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 788dccec..2acbc69f 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -38,7 +38,7 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * Live cursors build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. * - * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](https://ably.com/docs/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the {@link Space.subscribe | space state } and may only be subscribed to on the `cursors` namespace. + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](https://ably.com/docs/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the {@link Space.subscribe | space state } and may only be subscribed to via the {@link Space.cursors | `cursors` } property. * * The channel is only created when a member calls `space.cursors.set()`. The live cursors channel object can be accessed through `space.cursors.channel`. To monitor the [underlying state of the cursors channel](https://ably.com/docs/channels#states), the channel object can be accessed through `space.cursors.channel`. * diff --git a/src/Locations.ts b/src/Locations.ts index 00bf980d..f2fb54f7 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -108,7 +108,7 @@ export default class Locations extends EventEmitter<LocationsEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L29-L91) --> - * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling {@link set}. Use the `subscribe()` method on the `locations` namespace of the space to receive updates. + * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling {@link set}. * * All location changes are {@link LocationsEventMap.update | `update`} events. When a location update is received, clear the highlight from the UI component of the member’s {@link LocationsEvents.UpdateEvent.previousLocation | `previousLocation`} and add it to {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation`}. * diff --git a/src/Locks.ts b/src/Locks.ts index 43f0feb5..c45a8976 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -380,7 +380,7 @@ export default class Locks extends EventEmitter<LocksEventMap> { /** * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L92-L151) --> - * Subscribe to lock events by registering a listener. Lock events are emitted whenever the {@link LockStatuses | lock state } transitions into `locked` or `unlocked`. Use the `subscribe()` method on the `locks` namespace of the space to receive updates. + * Subscribe to lock events by registering a listener. Lock events are emitted whenever the {@link LockStatuses | lock state } transitions into `locked` or `unlocked`. * * All lock events are `update` events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. * diff --git a/src/Members.ts b/src/Members.ts index 797db97e..b14ec205 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -17,7 +17,7 @@ export interface MembersEventMap { * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L9-L25) --> * Avatar stacks are the most common way of showing the online status of members in an application by displaying an avatar for each member. Events are emitted whenever a member enters or leaves a space, or updates their profile data. Additional information can also be provided, such as a profile picture and email address. * - * Subscribe to the `space.members` namespace in order to keep your avatar stack updated in realtime. + * Subscribe to the space’s { @link Space.members | `members` } property in order to keep your avatar stack updated in realtime. * * ## Event types * @@ -40,7 +40,7 @@ export interface MembersEventMap { * * > **Note** * > - * > Members {@link Space.enter | enter }, {@link Space.leave | leave }, and {@link Space.updateProfileData | update } a {@link Space | space } directly. The `members` namespace is used to subscribe to these updates. + * > Members {@link Space.enter | enter }, {@link Space.leave | leave }, and {@link Space.updateProfileData | update } a {@link Space | space } directly. The space’s { @link Space.members | `members` } property is used to subscribe to these updates. * * <!-- END WEBSITE DOCUMENTATION --> * diff --git a/src/Space.ts b/src/Space.ts index 7259a02f..bfcb6785 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -53,9 +53,9 @@ export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileDat * - Live cursors, via the {@link cursors | `cursors`} property * - Component locking, via the {@link locks | `locks`} property * - * The `space` namespace consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member’s location within the application. The position of members’ cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. + * A `Space` instance consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member’s location within the application. The position of members’ cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. * - * Space state can be {@link subscribe | subscribed} to in the `space` namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. + * Space state can be {@link subscribe | subscribed} to by using a `Space` object. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. * * To subscribe to any events in a space, you first need to create or retrieve a space. * @@ -350,7 +350,7 @@ class Space extends EventEmitter<SpaceEventMap> { * * > **Note** * > - * > Avatar stacks and member location events can be subscribed to on their individual namespaces; {@link Space.members | `space.members` } and {@link Space.locations | `space.locations`}. These events are filtered versions of space state events. Only a single [message](https://ably.com/docs/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. + * > Avatar stacks and member location events can be subscribed to using the space’s {@link members | `members` } and {@link locations | `locations` } properties.0These events are filtered versions of space state events. Only a single [message](https://ably.com/docs/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. * > * > The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event. * From ed931e07e2434d2fc4754d81b8cb27d5113acdac Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 26 Sep 2023 15:19:18 -0300 Subject: [PATCH 127/191] Move parts of docstrings to more appropriate places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I had a quick scan of the docstrings added in e1b9359 and, where I noticed documentation that seemed like it would be more appropriate against another member, I moved it there and added a note explaining that it had been moved (so that the original content would continue to read OK). It’s not comprehensive and I think that a deeper reading of the documentation content would identify more that can be done here. --- src/Cursors.ts | 11 +++----- src/Locations.ts | 21 ++++++++------- src/Locks.ts | 27 +++++-------------- src/Members.ts | 46 ++++++++++++++++++++------------ src/Space.ts | 4 ++- src/types.ts | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 56 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 2acbc69f..b3665668 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -126,14 +126,9 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * ``` * The following are the properties of a cursor event payload: * - * | Property | Description | Type | - * |--------------|----------------------------------------------------------------------------------------------------|--------| - * | connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | - * | clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | - * | position | An object containing the position of a member’s cursor. | Object | - * | position.x | The position of the member’s cursor on the X-axis. | Number | - * | position.y | The position of the member’s cursor on the Y-axis. | Number | - * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | + * > **Moved documentation** + * > + * > This documentation has been moved to {@link CursorUpdate} and {@link CursorPosition}. * * <!-- END WEBSITE DOCUMENTATION --> * diff --git a/src/Locations.ts b/src/Locations.ts index f2fb54f7..0b433746 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -9,7 +9,15 @@ import SpaceUpdate from './SpaceUpdate.js'; export namespace LocationsEvents { export interface UpdateEvent { member: SpaceMember; + /** + * <!-- MOVED FROM Locations.subscribe --> + * The new location of the member. + */ currentLocation: unknown; + /** + * <!-- MOVED FROM Locations.subscribe --> + * The previous location of the member. + */ previousLocation: unknown; } } @@ -156,16 +164,9 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * ``` * The following are the properties of a location event payload: * - * | Property | Description | Type | - * |----------------------------|-----------------------------------------------------------------------------------------------------------------------|---------| - * | member.clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | - * | member.connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | - * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | - * | member.lastEvent.name | The most recent { @link MembersEventMap | event } emitted by the member. Will be one of {@link MembersEventMap.enter | `enter`}, {@link MembersEventMap.update | `update`}, {@link MembersEventMap.leave | `leave`} or {@link MembersEventMap.remove | `remove`}. | String | - * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * | member.profileData | The optional {@link Space.updateProfileData | profile data } associated with the member. | Object | - * | previousLocation | The previous location of the member. | Object | - * | currentLocation | The new location of the member. | Object | + * > **Moved documentation** + * > + * > This documentation has been moved to { @link LocationsEvents.UpdateEvent }. * * > **Further reading** * > diff --git a/src/Locks.ts b/src/Locks.ts index c45a8976..f98b4419 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -41,14 +41,9 @@ export interface LocksEventMap { * * A lock will be in one of the following states: * - * `pending` - * A member has requested a lock by calling {@link acquire | `acquire()`}. - * - * `locked` - * The lock is confirmed to be held by the requesting member. - * - * `unlocked` - * The lock is confirmed to not be locked by the requesting member, or has been {@link release | released } by a member previously holding the lock. + * > **Moved documentation** + * > + * > This documentation has been moved to { @link LockStatuses }. * * The following lock state transitions may occur: * @@ -426,19 +421,9 @@ export default class Locks extends EventEmitter<LocksEventMap> { * ``` * The following are the properties of a lock event payload: * - * | Property | Description | Type | - * |----------------------------|------------------------------------------------------------------------------------------------------------------------------|-----------| - * | id | The unique ID of the lock request. | String | - * | status | The lock {@link LockStatues | status } of the event. Will be one of `locked`, `unlocked` or `pending`. | String | - * | timestamp | The timestamp of the lock event. | Number | - * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | - * | reason | The reason why the `request.status` is `unlocked`. | ErrorInfo | - * | member.clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | - * | member.connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | - * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | - * | member.lastEvent.name | The most recent { @link Members.subscribe | event } emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | - * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * | member.profileData | The optional {@link Space.updateProfileData | profile data } associated with the member. | Object | + * > **Moved documentation** + * > + * > This documentation has been moved to { @link Lock }. * * <!-- END WEBSITE DOCUMENTATION --> * diff --git a/src/Members.ts b/src/Members.ts index b14ec205..116bfcaa 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -6,10 +6,35 @@ import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; export interface MembersEventMap { + /** + * <!-- MOVED WITH EDITING FROM Members --> + * A member has left the space. The member has either left explicitly by calling { @link Space.leave | `space.leave()` }, or has abruptly disconnected and not re-established a connection within 15 seconds. + */ leave: SpaceMember; + /** + * <!-- MOVED WITH EDITING FROM Members --> + * A new member has entered the space. The member has either entered explicitly by calling {@link Space.enter | `space.enter()` }, or has attempted to update their profile data before entering a space, which will instead emit an `enter` event. + */ enter: SpaceMember; + /** + * <!-- MOVED WITH EDITING FROM Members --> + * This event is emitted whenever any one of the following events is emitted: + * + * - {@link enter} + * - {@link leave} + * - {@link remove} + * - {@link updateProfile} + */ update: SpaceMember; + /** + * <!-- MOVED WITH EDITING FROM Members --> + * A member has updated their profile data by calling { @link Space.updateProfileData | `space.updateProfileData()` }. + */ updateProfile: SpaceMember; + /** + * <!-- MOVED WITH EDITING FROM Members --> + * A member has been removed from the members list after the { @link SpaceOptions.offlineTimeout | `offlineTimeout` } period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their `leave` and `remove` events. + */ remove: SpaceMember; } @@ -23,20 +48,9 @@ export interface MembersEventMap { * * The following four event types are emitted by members: * - * `enter` - * A new member has entered the space. The member has either entered explicitly by calling {@link Space.enter | `space.enter()`}, or has attempted to update their profile data before entering a space, which will instead emit an `enter` event. - * - * `updateProfile` - * A member has updated their profile data by calling {@link Space.updateProfileData | `space.updateProfileData()`. - * - * `leave` - * A member has left the space. The member has either left explicitly by calling {@link Space.leave | `space.leave()` }, or has abruptly disconnected and not re-established a connection within 15 seconds. - * - * `remove` - * A member has been removed from the members list after the {@link SpaceOptions.offlineTimeout | `offlineTimeout` } period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their `leave` and `remove` events. - * - * `update` - * This event is emitted whenever any one of the above events is emitted. + * > **Moved documentation** + * > + * > This documentation has been moved to { @link MembersEventMap }. * * > **Note** * > @@ -337,8 +351,8 @@ class Members extends EventEmitter<MembersEventMap> { * | isConnected | Whether the member is connected to Ably or not. | Boolean | * | profileData | The optional {@link Space.updateProfileData | profile data } associated with the member. | Object | * | location | The current {@link Locations | location} of the member. Will be `null` for `enter`, `leave` and `remove` events. | Object | - * | lastEvent.name | The most recent event emitted by the member. | String | - * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | lastEvent.name | This documentation has been moved to {@link SpaceMember}. | String | + * | lastEvent.timestamp | This documentation has been moved to {@link SpaceMember}. | Number | * * > **Further reading** * > diff --git a/src/Space.ts b/src/Space.ts index bfcb6785..13cdec15 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -179,7 +179,9 @@ class Space extends EventEmitter<SpaceEventMap> { * <!-- END WEBSITE DOCUMENTATION --> * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L71-L82) --> - * Profile data can be set when {@link enter | entering} a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. + * > **Moved documentation** + * > + * > This documentation has been moved to { @link ProfileData }. * * Profile data is returned in the payload of all space events. * diff --git a/src/types.ts b/src/types.ts index b6a1bb70..1f0843b0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,7 +22,15 @@ export interface CursorsOptions { * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export interface CursorPosition { + /** + * <!-- MOVED FROM Cursors.set --> + * The position of the member’s cursor on the X-axis. + */ x: number; + /** + * <!-- MOVED FROM Cursors.set --> + * The position of the member’s cursor on the Y-axis. + */ y: number; } @@ -39,9 +47,25 @@ export type CursorData = Record<string, unknown>; * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export interface CursorUpdate { + /** + * <!-- MOVED FROM Cursors.set --> + * The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. + */ clientId: string; + /** + * <!-- MOVED FROM Cursors.set --> + * The unique identifier of the member’s [connection](https://ably.com/docs/connect). + */ connectionId: string; + /** + * <!-- MOVED FROM Cursors.set --> + * An object containing the position of a member’s cursor. + */ position: CursorPosition; + /** + * <!-- MOVED FROM Cursors.set --> + * An optional arbitrary JSON-serializable object containing additional information about the cursor. + */ data?: CursorData; } @@ -65,6 +89,10 @@ export interface SpaceOptions { cursors: CursorsOptions; } +/** + * <!-- MOVED WITH EDITING FROM Space.enter --> + * Profile data can be set when {@link Space.enter | entering } a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. + */ export type ProfileData = Record<string, unknown> | null; /** @@ -109,14 +137,34 @@ export interface SpaceMember { * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ lastEvent: { + /** + * <!-- MOVED FROM Locations.subscribe --> + * The most recent event emitted by the member. + */ name: Types.PresenceAction; + /** + * <!-- MOVED FROM Locations.subscribe --> + * The timestamp of the most recently emitted event. + */ timestamp: number; }; } export namespace LockStatuses { + /** + * <!-- MOVED WITH EDITING FROM Locks --> + * A member has requested a lock by calling { @link Locks.acquire | `acquire()` }. + */ export type Pending = 'pending'; + /** + * <!-- MOVED FROM Locks --> + * The lock is confirmed to be held by the requesting member. + */ export type Locked = 'locked'; + /** + * <!-- MOVED WITH EDITING FROM Locks --> + * The lock is confirmed to not be locked by the requesting member, or has been { @link Locks.release | released } by a member previously holding the lock. + */ export type Unlocked = 'unlocked'; } @@ -133,10 +181,30 @@ export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatus * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ export type Lock = { + /** + * <!-- MOVED FROM Locks.subscribe --> + * The unique ID of the lock request. + */ id: string; + /** + * <!-- MOVED WITH EDITING FROM Locks.subscribe --> + * The lock status of the event. + */ status: LockStatus; member: SpaceMember; + /** + * <!-- MOVED FROM Locks.subscribe --> + * The timestamp of the lock event. + */ timestamp: number; + /** + * <!-- MOVED FROM Locks.subscribe --> + * The optional attributes of the lock, such as the ID of the component it relates to. + */ attributes?: LockAttributes; + /** + * <!-- MOVED FROM Locks.subscribe --> + * The reason why the `request.status` is `unlocked`. + */ reason?: Types.ErrorInfo; }; From 37098e9d91353f5b585d850d0fe7f18416574b9a Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 26 Sep 2023 15:20:29 -0300 Subject: [PATCH 128/191] Write rudimentary documentation where none currently exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is based on my pretty limited knowledge of the SDK and some scanning of the documentation. And I didn’t put a great amount of time into it. There’s plenty of room for improvement. --- src/Cursors.ts | 39 +++++++++++++++++- src/Locations.ts | 42 ++++++++++++++++++++ src/Locks.ts | 43 ++++++++++++++++++++ src/Members.ts | 27 +++++++++++++ src/Space.ts | 75 +++++++++++++++++++++++++++++++++++ src/Spaces.ts | 10 +++++ src/types.ts | 9 +++++ src/utilities/EventEmitter.ts | 31 +++++++++++++++ src/utilities/types.ts | 5 +++ 9 files changed, 280 insertions(+), 1 deletion(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index b3665668..5198470a 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -11,7 +11,13 @@ import type { CursorsOptions, CursorUpdate } from './types.js'; import type { RealtimeMessage } from './utilities/types.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; +/** + * The property names of `CursorsEventMap` are the names of the events emitted by { @link Cursors }. + */ export interface CursorsEventMap { + /** + * A space member moved their cursor. + */ update: CursorUpdate; } @@ -52,9 +58,15 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { private readonly cursorBatching: CursorBatching; private readonly cursorDispensing: CursorDispensing; private readonly cursorHistory: CursorHistory; + /** + * The {@link SpaceOptions.cursors | cursors options} passed to {@link default.get | `Spaces.get()`}, with default values filled in. + */ readonly options: CursorsOptions; private readonly channelName: string; + /** + * The [ably-js](https://github.com/ably/ably-js) realtime channel instance that this `Cursors` instance uses for transmitting and receiving data. + */ public channel?: Types.RealtimeChannelPromise; /** @internal */ @@ -74,7 +86,6 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { /** * Schedules a cursor update event to be sent that will cause the following events to fire * - * @param {CursorUpdate} cursor * @return {void} * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L21-L74) --> @@ -145,6 +156,8 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * }); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param cursor An object describing the cursor update that should be emitted. */ async set(cursor: Pick<CursorUpdate, 'position' | 'data'>) { const self = await this.space.members.getSelf(); @@ -201,6 +214,8 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { }; /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L78-L90) --> * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling `set()`. Use the `subscribe()` method on the `cursors` object of a space to receive updates. * @@ -230,11 +245,21 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * space.cursors.subscribe('update', (cursorUpdate: CursorUpdate) => {}); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to add. + * + * @typeParam K A type which allows one or more names of the properties of the {@link CursorsEventMap} type. */ subscribe<K extends keyof CursorsEventMap>( eventOrEvents: K | K[], listener?: EventListener<CursorsEventMap, K>, ): void; + /** + * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * + * @param listener The listener to add. + */ subscribe(listener?: EventListener<CursorsEventMap, keyof CursorsEventMap>): void; subscribe<K extends keyof CursorsEventMap>( listenerOrEvents?: K | K[] | EventListener<CursorsEventMap, K>, @@ -262,6 +287,8 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/cursors.textile?plain=1#L94-L106) --> * Unsubscribe from cursor events to remove previously registered listeners. * @@ -284,11 +311,21 @@ export default class Cursors extends EventEmitter<CursorsEventMap> { * space.cursors.unsubscribe('update'); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to remove. + * + * @typeParam K A type which allows one or more names of the properties of the {@link CursorsEventMap} type. */ unsubscribe<K extends keyof CursorsEventMap>( eventOrEvents: K | K[], listener?: EventListener<CursorsEventMap, K>, ): void; + /** + * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * + * @param listener The listener to remove. + */ unsubscribe(listener?: EventListener<CursorsEventMap, keyof CursorsEventMap>): void; unsubscribe<K extends keyof CursorsEventMap>( listenerOrEvents?: K | K[] | EventListener<CursorsEventMap, K>, diff --git a/src/Locations.ts b/src/Locations.ts index 0b433746..27a49cf2 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -6,8 +6,17 @@ import type Space from './Space.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; import SpaceUpdate from './SpaceUpdate.js'; +/** + * This namespace contains the types which represent the data attached to an event emitted by a {@link Locations | `Locations`} instance. + */ export namespace LocationsEvents { + /** + * The data attached to an {@link LocationsEventMap.update | `update`} event. + */ export interface UpdateEvent { + /** + * The member whose location changed. + */ member: SpaceMember; /** * <!-- MOVED FROM Locations.subscribe --> @@ -22,7 +31,13 @@ export namespace LocationsEvents { } } +/** + * The property names of `LocationsEventMap` are the names of the events emitted by { @link Locations }. + */ export interface LocationsEventMap { + /** + * A space member changed their location. + */ update: LocationsEvents.UpdateEvent; } @@ -102,6 +117,9 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Set your current location. The `location` argument can be any JSON-serializable object. Emits a `locationUpdate` event to all connected clients in this space. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * <!-- Second sentence copied from above --> + * @param location The new location. Can be any JSON-serializable object, such as a slide number or element ID. */ async set(location: unknown) { const self = await this.space.members.getSelf(); @@ -115,6 +133,8 @@ export default class Locations extends EventEmitter<LocationsEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L29-L91) --> * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling {@link set}. * @@ -187,11 +207,21 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * space.locations.subscribe('update', (locationUpdate: LocationsEvents.UpdateEvent) => {}); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to add. + * + * @typeParam K A type which allows one or more names of the properties of the {@link LocationsEventMap} type. */ subscribe<K extends keyof LocationsEventMap>( eventOrEvents: K | K[], listener?: EventListener<LocationsEventMap, K>, ): void; + /** + * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * + * @param listener The listener to add. + */ subscribe(listener?: EventListener<LocationsEventMap, keyof LocationsEventMap>): void; subscribe<K extends keyof LocationsEventMap>( listenerOrEvents?: K | K[] | EventListener<LocationsEventMap, K>, @@ -211,6 +241,8 @@ export default class Locations extends EventEmitter<LocationsEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locations.textile?plain=1#L95-L107) --> * Unsubscribe from location events to remove previously registered listeners. * @@ -233,11 +265,21 @@ export default class Locations extends EventEmitter<LocationsEventMap> { * space.locations.unsubscribe('update'); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to remove. + * + * @typeParam K A type which allows one or more names of the properties of the {@link LocationsEventMap} type. */ unsubscribe<K extends keyof LocationsEventMap>( eventOrEvents: K | K[], listener?: EventListener<LocationsEventMap, K>, ): void; + /** + * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but unsubscribes from _all_ events. + * + * @param listener The listener to remove. + */ unsubscribe(listener?: EventListener<LocationsEventMap, keyof LocationsEventMap>): void; unsubscribe<K extends keyof LocationsEventMap>( listenerOrEvents?: K | K[] | EventListener<LocationsEventMap, K>, diff --git a/src/Locks.ts b/src/Locks.ts index f98b4419..311e4bbb 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -15,11 +15,23 @@ import SpaceUpdate from './SpaceUpdate.js'; */ export type LockAttributes = Record<string, unknown>; +/** + * Options for customizing the behaviour of {@link Locks.get | `Locks.get()`}. + */ export interface LockOptions { + /** + * Additional metadata to associate with the lock, such as the identifier of a UI component. + */ attributes: LockAttributes; } +/** + * The property names of `LocksEventMap` are the names of the events emitted by { @link Locks }. + */ export interface LocksEventMap { + /** + * A lock’s state transitioned into {@link LockStatuses.Locked} or {@link LockStatuses.Unlocked}. + */ update: Lock; } @@ -110,6 +122,8 @@ export default class Locks extends EventEmitter<LocksEventMap> { * const lock = space.locks.get(id); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param id A unique identifier which specifies the lock to query. */ get(id: string): Lock | undefined { const locks = this.locks.get(id); @@ -293,6 +307,9 @@ export default class Locks extends EventEmitter<LocksEventMap> { * const lockRequest = await space.locks.acquire(id); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param id A unique identifier which specifies the lock to acquire. + * @param opts An object whose {@link LockOptions.attributes | `attributes`} property specifies additional metadata to associate with the lock. */ async acquire(id: string, opts?: LockOptions): Promise<Lock> { const self = await this.space.members.getSelf(); @@ -354,6 +371,8 @@ export default class Locks extends EventEmitter<LocksEventMap> { * await space.locks.release(id); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param id A unique identifier which specifies the lock to release. */ async release(id: string): Promise<void> { const self = await this.space.members.getSelf(); @@ -374,6 +393,8 @@ export default class Locks extends EventEmitter<LocksEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L92-L151) --> * Subscribe to lock events by registering a listener. Lock events are emitted whenever the {@link LockStatuses | lock state } transitions into `locked` or `unlocked`. * @@ -442,8 +463,18 @@ export default class Locks extends EventEmitter<LocksEventMap> { * * The argument supplied to the callback is a {@link Lock}, representing the lock request and it's status. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to add. + * + * @typeParam K A type which allows one or more names of the properties of the {@link LocksEventMap} type. */ subscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; + /** + * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * + * @param listener The listener to add. + */ subscribe(listener?: EventListener<LocksEventMap, keyof LocksEventMap>): void; subscribe<K extends keyof LocksEventMap>( listenerOrEvents?: K | K[] | EventListener<LocksEventMap, K>, @@ -463,6 +494,8 @@ export default class Locks extends EventEmitter<LocksEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/locking.textile?plain=1#L155-L166) --> * Unsubscribe from lock events to remove previously registered listeners. * @@ -485,8 +518,18 @@ export default class Locks extends EventEmitter<LocksEventMap> { * space.locks.unsubscribe('update'); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to remove. + * + * @typeParam K A type which allows one or more names of the properties of the {@link LocksEventMap} type. */ unsubscribe<K extends keyof LocksEventMap>(eventOrEvents: K | K[], listener?: EventListener<LocksEventMap, K>): void; + /** + * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but unsubscribes from _all_ events. + * + * @param listener The listener to remove. + */ unsubscribe(listener?: EventListener<LocksEventMap, keyof LocksEventMap>): void; unsubscribe<K extends keyof LocksEventMap>( listenerOrEvents?: K | K[] | EventListener<LocksEventMap, K>, diff --git a/src/Members.ts b/src/Members.ts index 116bfcaa..40986fa0 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -5,6 +5,9 @@ import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; +/** + * The property names of `MembersEventMap` are the names of the events emitted by { @link Members }. + */ export interface MembersEventMap { /** * <!-- MOVED WITH EDITING FROM Members --> @@ -284,6 +287,8 @@ class Members extends EventEmitter<MembersEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L29-L103) --> * Subscribe to members’ online status and profile updates by registering a listener. Member events are emitted whenever a member {@link Space.enter | enters} or {@link Space.leave | leaves} the space, or updates their profile data. Use the `subscribe()` method on the `members` object of a space to receive updates. * @@ -421,11 +426,21 @@ class Members extends EventEmitter<MembersEventMap> { * * The argument supplied to the callback is a {@link SpaceMember} object representing the member affected by the change. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to add. + * + * @typeParam K A type which allows one or more names of the properties of the {@link MembersEventMap} type. */ subscribe<K extends keyof MembersEventMap>( eventOrEvents: K | K[], listener?: EventListener<MembersEventMap, K>, ): void; + /** + * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * + * @param listener The listener to add. + */ subscribe(listener?: EventListener<MembersEventMap, keyof MembersEventMap>): void; subscribe<K extends keyof MembersEventMap>( listenerOrEvents?: K | K[] | EventListener<MembersEventMap, K>, @@ -445,6 +460,8 @@ class Members extends EventEmitter<MembersEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/avatar.textile?plain=1#L107-L125) --> * Unsubscribe from member events to remove previously registered listeners. * @@ -479,11 +496,21 @@ class Members extends EventEmitter<MembersEventMap> { * space.members.unsubscribe('leave'); * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to remove. + * + * @typeParam K A type which allows one or more names of the properties of the {@link MembersEventMap} type. */ unsubscribe<K extends keyof MembersEventMap>( eventOrEvents: K | K[], listener?: EventListener<MembersEventMap, K>, ): void; + /** + * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but unsubscribes from _all_ events. + * + * @param listener The listener to remove. + */ unsubscribe(listener?: EventListener<MembersEventMap, keyof MembersEventMap>): void; unsubscribe<K extends keyof MembersEventMap>( listenerOrEvents?: K | K[] | EventListener<MembersEventMap, K>, diff --git a/src/Space.ts b/src/Space.ts index 13cdec15..ecc06b5d 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -26,20 +26,47 @@ const SPACE_OPTIONS_DEFAULTS = { }, }; +/** + * This namespace contains the types which represent the data attached to an event emitted by a {@link Space | `Space`} instance. + */ export namespace SpaceEvents { + /** + * The data attached to an {@link SpaceEventMap | `update`} event. + */ export interface UpdateEvent { + /** + * The members currently in the space. + */ members: SpaceMember[]; } } +/** + * The property names of `SpaceEventMap` are the names of the events emitted by { @link Space }. + */ export interface SpaceEventMap { + /** + * The space state was updated. + */ update: SpaceEvents.UpdateEvent; } +/** + * The current state of a {@link Space | `Space`}, as described by {@link Space.getState | `Space.getState()`}. + */ export interface SpaceState { + /** + * <!-- Copied, with edits, from getState documentation --> + * The members currently in the space. This includes members that have recently left the space, but have not yet been removed. + */ members: SpaceMember[]; } +/** + * A function that can be passed to {@link Space.updateProfileData | `Space.updateProfileData()`}. It receives the existing profile data and returns the new profile data. + * + * @param profileData The existing profile data. + */ export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileData; /** @@ -75,6 +102,9 @@ class Space extends EventEmitter<SpaceEventMap> { * @internal */ readonly connectionId: string | undefined; + /** + * The options passed to {@link default.get | `Spaces.get()`}, with default values filled in. + */ readonly options: SpaceOptions; /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -94,8 +124,17 @@ class Space extends EventEmitter<SpaceEventMap> { * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> */ readonly members: Members; + /** + * The [ably-js](https://github.com/ably/ably-js) realtime channel instance that this `Cursors` instance uses for transmitting and receiving data. + */ readonly channel: Types.RealtimeChannelPromise; + /** + * Provides access to the component locking feature for this space. + */ readonly locks: Locks; + /** + * The space name passed to {@link default.get | `Spaces.get()`}. + */ readonly name: string; /** @internal */ @@ -198,6 +237,8 @@ class Space extends EventEmitter<SpaceEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the {@link SpaceMember | member object }. Returns all current space members. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param profileData Data to associate with the member who is entering the space. */ async enter(profileData: ProfileData = null): Promise<SpaceMember[]> { return new Promise((resolve) => { @@ -222,6 +263,8 @@ class Space extends EventEmitter<SpaceEventMap> { } /** + * {@label MAIN_OVERLOAD} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L86-L103) --> * Profile data can be updated at any point after entering a space by calling `updateProfileData()`. This will emit an `update` event. If a client hasn’t yet entered the space, `updateProfileData()` will instead {@link enter | enter the space }, with the profile data, and emit an { @link MembersEventMap.enter | `enter` } event. * @@ -255,8 +298,15 @@ class Space extends EventEmitter<SpaceEventMap> { * }) * ``` * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param profileData The new profile data. */ async updateProfileData(profileData: ProfileData): Promise<void>; + /** + * See the documentation for { @link updateProfileData:MAIN_OVERLOAD | the main overload }. + * + * @param updateFn A function which receives the existing profile data and returns the new profile data. + */ async updateProfileData(updateFn: UpdateProfileDataFunction): Promise<void>; async updateProfileData(profileDataOrUpdateFn: ProfileData | UpdateProfileDataFunction): Promise<void> { const self = await this.members.getSelf(); @@ -299,6 +349,8 @@ class Space extends EventEmitter<SpaceEventMap> { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See {@link SpaceOptions.offlineTimeout | offlineTimeout }. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param profileData If specified, this updated profile data will accompany the { @link MembersEventMap.leave | `leave` } event. */ async leave(profileData: ProfileData = null) { const self = await this.members.getSelf(); @@ -337,6 +389,8 @@ class Space extends EventEmitter<SpaceEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L107-L177) --> * Subscribe to space state updates by registering a listener. Use the `subscribe()` method on the `space` object to receive updates. * @@ -411,8 +465,17 @@ class Space extends EventEmitter<SpaceEventMap> { * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * * <!-- END WEBSITE DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. * @param listener The listener to add. + * + * @typeParam K A type which allows one or more names of the properties of the {@link SpaceEventMap} type. */ subscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap, K>): void; + /** + * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * + * @param listener The listener to add. + */ subscribe(listener?: EventListener<SpaceEventMap, keyof SpaceEventMap>): void; subscribe<K extends keyof SpaceEventMap>( listenerOrEvents?: K | K[] | EventListener<SpaceEventMap, K>, @@ -432,6 +495,8 @@ class Space extends EventEmitter<SpaceEventMap> { } /** + * {@label WITH_EVENTS} + * * <!-- BEGIN WEBSITE DOCUMENTATION (https://github.com/ably/docs/blob/cb5de6a6a40abdcb0d9d5af825928dd62dc1ca64/content/spaces/space.textile?plain=1#L181-L187) --> * Unsubscribe from space events to remove previously registered listeners. * @@ -441,8 +506,18 @@ class Space extends EventEmitter<SpaceEventMap> { * space.unsubscribe('update', listener); * ``` * <!-- END WEBSITE DOCUMENTATION --> + * + * @param eventOrEvents The event name or an array of event names. + * @param listener The listener to remove. + * + * @typeParam K A type which allows one or more names of the properties of the {@link SpaceEventMap} type. */ unsubscribe<K extends keyof SpaceEventMap>(eventOrEvents: K | K[], listener?: EventListener<SpaceEventMap, K>): void; + /** + * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but unsubscribes from _all_ events. + * + * @param listener The listener to remove. + */ unsubscribe(listener?: EventListener<SpaceEventMap, keyof SpaceEventMap>): void; unsubscribe<K extends keyof SpaceEventMap>( listenerOrEvents?: K | K[] | EventListener<SpaceEventMap, K>, diff --git a/src/Spaces.ts b/src/Spaces.ts index c717e2a2..421c08ab 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -14,6 +14,11 @@ export interface ClientWithOptions extends Types.RealtimePromise { }; } +/** + * The `Spaces` class is the entry point to the Spaces SDK. Use its {@link get | `get()`} method to access an individual {@link Space | `Space`}. + * + * To create an instance of `Spaces`, you first need to create an instance of the [ably-js](https://github.com/ably/ably-js) Realtime client. You then pass this instance to the {@link constructor | `Spaces()` constructor}. + */ class Spaces { private spaces: Record<string, Space> = {}; /** @@ -46,6 +51,8 @@ class Spaces { * * Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/setup?lang=javascript) for information on setting up a realtime promise client. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param client An instance of the realtime, promise-based Ably client from [ably-js](https://github.com/ably/ably-js). */ constructor(client: Types.RealtimePromise) { this.client = client; @@ -147,6 +154,9 @@ class Spaces { * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> * Get or create a Space instance. Returns a {@link Space} instance. Configure the space by passing {@link SpaceOptions} as the second argument. * <!-- END CLASS-DEFINITIONS DOCUMENTATION --> + * + * @param name The name of the space to create or retrieve. + * @param options Additional options to customize the behaviour of the space. */ async get(name: string, options?: Subset<SpaceOptions>): Promise<Space> { if (typeof name !== 'string' || name.length === 0) { diff --git a/src/types.ts b/src/types.ts index 1f0843b0..73511128 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,9 @@ import { Types } from 'ably'; import type { LockAttributes } from './Locks.js'; +/** + * Options to configure the behaviour of a {@link Cursors | `Cursors`} instance. + */ export interface CursorsOptions { /** * <!-- BEGIN CLASS-DEFINITIONS DOCUMENTATION --> @@ -150,6 +153,9 @@ export interface SpaceMember { }; } +/** + * The `LockStatuses` namespace describes the possible values of the {@link LockStatus} type. + */ export namespace LockStatuses { /** * <!-- MOVED WITH EDITING FROM Locks --> @@ -191,6 +197,9 @@ export type Lock = { * The lock status of the event. */ status: LockStatus; + /** + * Information about the space member who requested the lock. + */ member: SpaceMember; /** * <!-- MOVED FROM Locks.subscribe --> diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index fa7da99b..df391d2a 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -64,12 +64,34 @@ export class InvalidArgumentError extends Error { } } +/** + * Describes the value of `this` inside the body of an {@link EventListener | `EventListener`} when invoked by an instance of {@link EventEmitter | `EventEmitter`} as the result of an event. + * + * @typeParam K The name of the event that this object corresponds to. + */ export interface EventListenerThis<K> { + /** + * The name of the event. + */ event: K; } +/** + * A listener which {@link EventEmitter | `EventEmitter`} will invoke when a given event (as configured using {@link EventEmitter.on | `on()`} or {@link EventEmitter.once | `once()`}) occurs. + * + * @param this Inside the body of an event listener invoked by `EventEmitter`, the `this` variable provides access to an object containing the name of the event. (This will not be the case if the listener is an arrow function.) + * @param param The data attached to this event. + * + * @typeParam T The type of event data that this listener will receive. + * @typeParam K The name of the event that this listener will listen for. + */ export type EventListener<T, K extends keyof T> = (this: EventListenerThis<K>, param: T[K]) => void; +/** + * `EventEmitter` represents an object which is capable of emitting one or more events, which are identified by their name. It provides methods which allow you to listen for these events. It serves as the base class for all of the event-emitting classes in the Spaces SDK. + * + * @typeParam T An object type, the names of whose properties are the names of the events that an instance of this class can emit. + */ export default class EventEmitter<T> { /** @internal */ any: Array<Function>; @@ -82,6 +104,7 @@ export default class EventEmitter<T> { /** * @internal + * @typeParam T An object type, the names of whose properties are the names of the events that the constructed object can emit. */ constructor() { this.any = []; @@ -95,6 +118,8 @@ export default class EventEmitter<T> { * Add an event listener * @param eventOrEvents the name of the event to listen to or the listener to be called. * @param listener (optional) the listener to be called. + * + * @typeParam K A type which allows one or more names of the properties of {@link T}. */ on<K extends keyof T>(eventOrEvents?: K | K[], listener?: EventListener<T, K>): void; /** @@ -137,6 +162,8 @@ export default class EventEmitter<T> { * Remove one or more event listeners * @param eventOrEvents the name of the event whose listener is to be removed. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. + * + * @typeParam K A type which allows one or more names of the properties of {@link T}. TypeScript will infer this type based on the {@link eventOrEvents} argument. */ off<K extends keyof T>(eventOrEvents?: K | K[], listener?: EventListener<T, K>): void; /** @@ -203,6 +230,8 @@ export default class EventEmitter<T> { * Get the array of listeners for a given event; excludes once events * @param event (optional) the name of the event, or none for 'any' * @return array of events, or null if none + * + * @typeParam K A type which allows a name of the properties of {@link T}. TypeScript will infer this type based on the {@link event} argument. */ listeners<K extends keyof T>(event: K): Function[] | null { if (event) { @@ -259,6 +288,8 @@ export default class EventEmitter<T> { * Listen for a single occurrence of an event * @param event the name of the event to listen to * @param listener (optional) the listener to be called + * + * @typeParam K A type which allows a name of one of the properties of {@link T}. TypeScript will infer this type based on the {@link event} argument. */ once<K extends keyof T>(event: K, listener?: EventListener<T, K>): void | Promise<any>; /** diff --git a/src/utilities/types.ts b/src/utilities/types.ts index 7eadcd05..e3e59768 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -19,6 +19,11 @@ export type PresenceMember = { }; } & Omit<Types.PresenceMessage, 'data'>; +/** + * Given an object type `T`, `Subset<T>` represents an object which has the same shape as `T`, but with some keys (at any level of nesting) potentially absent. + * + * @typeParam T The type from which `Subset` is derived. + */ export type Subset<T> = { [attr in keyof T]?: T[attr] extends object ? Subset<T[attr]> : T[attr]; }; From 2185d3c814751af2fa0d2977ebc69f3866406426 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 18 Sep 2023 10:31:29 -0300 Subject: [PATCH 129/191] Use TypeDoc to generate HTML documentation and upload to sdk.ably.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is largely copied from the way we do things in ably-js (as of its commit 76a5475). I’ve made a couple of tweaks to the config, though: - Added all types (except Project, which I have no idea how to document, and TypeLiteral, which would require us to split out every type literal into its own type, which we don’t want) to requiredToBeDocumented. I took the list from [1]. - Excluded internal and private stuff from documentation (see ccafd10). [1] https://github.com/TypeStrong/typedoc/blob/f2d2abe054feca91b89c00c33e1d726bbda85dcb/src/lib/models/reflections/kind.ts#L7 --- .github/workflows/dev-ci.yml | 28 ++++++ .gitignore | 1 + docs/typedoc/intro.md | 5 ++ package-lock.json | 168 +++++++++++++++++++++++++++++++++++ package.json | 4 +- typedoc.json | 34 +++++++ 6 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 docs/typedoc/intro.md create mode 100644 typedoc.json diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 1d5da735..d78aa820 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -52,3 +52,31 @@ jobs: node-version: 18 - run: npm ci - run: npm run build + docs: + runs-on: ubuntu-latest + permissions: + id-token: write + deployments: write + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 18 + - name: Install Package Dependencies + run: npm ci + - name: Build SDK + run: npm run build + - name: Build Documentation + run: npm run docs + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: eu-west-2 + role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/ably-sdk-builds-spaces + role-session-name: "${{ github.run_id }}-${{ github.run_number }}" + - name: Upload Documentation + uses: ably/sdk-upload-action@v1 + with: + sourcePath: docs/typedoc/generated + githubToken: ${{ secrets.GITHUB_TOKEN }} + artifactName: typedoc diff --git a/.gitignore b/.gitignore index 2a5c1164..d5382a82 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules/ .vscode coverage .env +docs/typedoc/generated/ # Local Netlify folder .netlify diff --git a/docs/typedoc/intro.md b/docs/typedoc/intro.md new file mode 100644 index 00000000..c8703d6d --- /dev/null +++ b/docs/typedoc/intro.md @@ -0,0 +1,5 @@ +# Ably Spaces SDK + +This is the API reference for Ably’s Spaces SDK, generated with [TypeDoc](https://typedoc.org/). + +To find out more about Spaces and how to get started, visit the [Ably documentation website](https://ably.com/docs/spaces). diff --git a/package-lock.json b/package-lock.json index 83da4914..99eebeb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "prettier": "^2.8.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", + "typedoc": "^0.25.2", "typescript": "^4.9.5", "vitest": "^0.34.3" }, @@ -1331,6 +1332,12 @@ "node": ">=8" } }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3462,6 +3469,12 @@ "node": ">=10" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/magic-string": { "version": "0.30.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", @@ -3501,6 +3514,18 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4306,6 +4331,18 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", + "integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -4791,6 +4828,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedoc": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz", + "integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.3", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -5018,6 +5100,18 @@ "node": ">=14.0.0" } }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5996,6 +6090,12 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7593,6 +7693,12 @@ "yallist": "^4.0.0" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "magic-string": { "version": "0.30.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", @@ -7625,6 +7731,12 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8156,6 +8268,18 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "shiki": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", + "integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==", + "dev": true, + "requires": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -8516,6 +8640,38 @@ "is-typed-array": "^1.1.9" } }, + "typedoc": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz", + "integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==", + "dev": true, + "requires": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.3", + "shiki": "^0.14.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -8632,6 +8788,18 @@ } } }, + "vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 7c7b325c..4e87fcc2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "build:mjs": "npx tsc --project tsconfig.mjs.json && cp res/package.mjs.json dist/mjs/package.json", "build:cjs": "npx tsc --project tsconfig.cjs.json && cp res/package.cjs.json dist/cjs/package.json", "build:iife": "rm -rf dist/iife && npx tsc --project tsconfig.iife.json && rollup -c", - "examples:locks": "ts-node ./examples/locks.ts" + "examples:locks": "ts-node ./examples/locks.ts", + "docs": "typedoc" }, "exports": { "import": "./dist/mjs/index.js", @@ -52,6 +53,7 @@ "prettier": "^2.8.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", + "typedoc": "^0.25.2", "typescript": "^4.9.5", "vitest": "^0.34.3" }, diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 00000000..38ca7dac --- /dev/null +++ b/typedoc.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["dist/mjs/index.d.ts"], + "excludeInternal": true, + "excludePrivate": true, + "includeVersion": true, + "out": "docs/typedoc/generated", + "readme": "docs/typedoc/intro.md", + "requiredToBeDocumented": [ + "Accessor", + "CallSignature", + "Class", + "Constructor", + "ConstructorSignature", + "Enum", + "EnumMember", + "Function", + "GetSignature", + "IndexSignature", + "Interface", + "Method", + "Module", + "Namespace", + "Parameter", + "Property", + "Reference", + "SetSignature", + "TypeAlias", + "TypeParameter", + "Variable", + ], + "treatWarningsAsErrors": true, + "validation": true +} From c58418df9829e570b75d5b62863d51152a731538 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 3 Oct 2023 15:31:59 -0300 Subject: [PATCH 130/191] Remove docs/class-definitions.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All of the valuable content from document was copied to TSDoc comments in 95ed746, and I don’t want us to need to maintain two copies of this content. So, replace links to this document with links to the TypeDoc-generated documentation on sdk.ably.com (it’s not ideal that these links always link to the documentation for the `main` branch, regardless of which version of the codebase the user is browsing, but we can think about how to fix that later) and delete it. --- README.md | 2 +- docs/channel-usage.md | 4 +- docs/class-definitions.md | 684 -------------------------------------- docs/usage.md | 4 +- 4 files changed, 5 insertions(+), 689 deletions(-) delete mode 100644 docs/class-definitions.md diff --git a/README.md b/README.md index fac5df4e..8d4eee6a 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Changes to the app state made by users not only need to be synced with your back The Spaces SDK is currently under development. If you are interested in being an early adopter and providing feedback then you can [sign up](https://go.ably.com/spaces-early-access) for early access and are welcome to [provide us with feedback](https://go.ably.com/spaces-feedback). The next section gives you an overview of how to use the SDK. Alternatively, you can jump to: -* [Class definitions](/docs/class-definitions.md) +* [TypeDoc documentation](https://sdk.ably.com/builds/ably/spaces/main/typedoc/index.html) * [Usage instructions](/docs/usage.md) * [Channel behaviors](/docs/channel-behaviors.md) diff --git a/docs/channel-usage.md b/docs/channel-usage.md index d881db82..ee928cc6 100644 --- a/docs/channel-usage.md +++ b/docs/channel-usage.md @@ -6,7 +6,7 @@ The below channels are used by the Spaces library internally. ### Space channel -Each `Space` (as defined by the [`Space` class](/docs/class-definitions.md#space)) creates its own [Ably Channel](https://ably.com/docs/channels). +Each `Space` (as defined by the [`Space` class](https://sdk.ably.com/builds/ably/spaces/main/typedoc/classes/Space.html)) creates its own [Ably Channel](https://ably.com/docs/channels). The channel name is defined by the `name` of the Space and takes the form: `${name}-space`. The full name of a `channel` belonging to a `Space` called 'slides' would therefore be `slides-space`. @@ -18,4 +18,4 @@ The channel name is defined by the name of the Space with the `::$cursors` suffi #### Events published -1. `cursorUpdate` - a batch of cursor updates passed to [`set`](/docs/class-definitions.md#set). +1. `cursorUpdate` - a batch of cursor updates passed to [`set`](https://sdk.ably.com/builds/ably/spaces/main/typedoc/classes/Cursors.html#set). diff --git a/docs/class-definitions.md b/docs/class-definitions.md deleted file mode 100644 index 186f065b..00000000 --- a/docs/class-definitions.md +++ /dev/null @@ -1,684 +0,0 @@ -# Spaces - -## Constructor - -Create a new instance of the Space SDK by passing an instance of the realtime, promise-based [Ably client](https://github.com/ably/ably-js): - -```ts -import { Realtime } from 'ably/promise'; -import Spaces from '@ably/spaces'; - -const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); -const spaces = new Spaces(client); -``` - -Please note that a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascript) is required. - -An API key will required for [basic authentication](https://ably.com/docs/auth/basic?lang=javascript). We strongly recommended that you use [token authentication](https://ably.com/docs/realtime/authentication#token-authentication) in any production environments. - -Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/setup?lang=javascript) for information on setting up a realtime promise client. - -### Properties - -#### client - -Instance of the [ably-js](https://github.com/ably/ably-js) client that was passed to the [constructor](#constructor). - -```ts -type client = Ably.RealtimePromise; -``` - -#### connection - -Instance of the [ably-js](https://github.com/ably/ably-js) connection, belonging to the client that was passed to the [constructor](#constructor). - -```ts -type connection = Ably.ConnectionPromise; -``` - -#### version - -Version of the Spaces library. - -```ts -type version = string; -``` - -### Methods - -#### get() - -Get or create a Space instance. Returns a [Space](#space) instance. Configure the space by passing [SpaceOptions](#spaceoptions) as the second argument. - -```ts -type get = (name: string, options?: SpaceOptions) => Promise<Space>; -``` - -### Related Types - -#### SpaceOptions - -Used to configure a Space instance on creation. - -```ts -type SpaceOptions = { - offlineTimeout?: number; - cursors?: CursorsOptions; -}; -``` - -##### offlineTimeout - -Number of milliseconds after a user loses connection or closes their browser window to wait before their [SpaceMember](#spacemember) object is removed from the members list. The default is 120000ms (2 minutes). - -##### cursors - -Options relating to configuring the cursors API (see below). - -##### CursorsOptions - -```ts -type CursorsOptions = { - outboundBatchInterval?: number; - paginationLimit?: number; -}; -``` - -##### outboundBatchInterval - -The interval in milliseconds at which a batch of cursor positions are published. This is multiplied by the number of members in the space minus 1. The default value is 25ms. - -##### paginationLimit - -The number of pages searched from [history](https://ably.com/docs/storage-history/history) for the last published cursor position. The default is 5. - -## Space - -An instance of a Space created using [spaces.get](#get). Inherits from [EventEmitter](/docs/usage.md#event-emitters). - -### Properties - -#### members - -An instance of [Members](#members). - -```ts -type members = instanceof Members; -``` - -#### cursors - -An instance of [Cursors](#cursors). - -```ts -type cursors = instanceof Cursors; -``` - -#### locations - -An instance of [Locations](#locations). - -```ts -type locations = instanceof Locations; -``` - -### Methods - -#### enter() - -Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the [member object](#spacemember). Returns all current space members. - -```ts -type enter = (profileData?: Record<string, unknown>) => Promise<SpaceMember[]>; -``` - -#### leave() - -Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See [offlineTimeout](#spaceoptions). - -```ts -type leave = (profileData?: Record<string, unknown>) => Promise<void>; -``` - -#### updateProfileData() - -Update `profileData`. This data can be an arbitrary JSON-serializable object which is attached to the [member object](#spacemember). If the connection -has not entered the space, calling `updateProfileData` will call `enter` instead. - -```ts -type updateProfileData = (profileDataOrUpdateFn?: unknown| (unknown) => unknown) => Promise<void>; -``` - -A function can also be passed in. This function will receive the existing `profileData` and lets you update based on the existing value of `profileData`: - -```ts -await space.updateProfileData((oldProfileData) => { - const newProfileData = getNewProfileData(); - return { ...oldProfileData, ...newProfileData }; -}) -``` - -## Space Members - -Handles members within a space. - -### Methods - -#### subscribe() - -Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. - -The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event. - -Example: - -```ts -space.members.subscribe((member: SpaceMember) => {}); -``` - -Available events: - -- ##### **enter** - - Listen to enter events of members. - - ```ts - space.members.subscribe('enter', (member: SpaceMember) => {}) - ``` - The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. - -- ##### **leave** - - Listen to leave events of members. The leave event will be issued when a member calls `space.leave()` or is disconnected. - - ```ts - space.members.subscribe('leave', (member: SpaceMember) => {}) - ``` - - The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member leaving the space. - -- ##### **remove** - - Listen to remove events of members. The remove event will be triggered when the [offlineTimeout](#spaceoptions) has passed. - - ```ts - space.members.subscribe('remove', (member: SpaceMember) => {}) - ``` - - The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member removed from the space. - -- ##### **updateProfile** - - Listen to profile update events of members. - - ```ts - space.members.subscribe('updateProfile', (member: SpaceMember) => {}) - ``` - The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. - -- ##### **update** - - Listen to `enter`, `leave`, `updateProfile` and `remove` events. - - ```ts - space.members.subscribe('update', (member: SpaceMember) => {}) - ``` - - The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member affected by the change. - - -#### unsubscribe() - -Remove all the event listeners or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. - -```ts -// Unsubscribe from all events -space.members.unsubscribe(); - -// Unsubscribe from enter events -space.members.unsubscribe('enter'); - -// Unsubscribe from leave events -space.members.unsubscribe('leave'); -``` - -#### getSelf() - -Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `null` if the client hasn't entered the space yet. - -```ts -type getSelf = () => Promise<SpaceMember | null>; -``` - -Example: - -```ts -const myMember = await space.members.getSelf(); -``` - -#### getAll() - -Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, including any who have left and not yet timed out. (_see: [offlineTimeout](#spaceoptions)_) - -```ts -type getAll = () => Promise<SpaceMember[]>; -``` - -Example: - -```ts -const allMembers = await space.members.getAll(); -``` - -#### getOthers() - -Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, excluding your own member object. - -```ts -type getSelf = () => Promise<SpaceMember[]>; -``` - -Example: - -```ts -const otherMembers = await space.members.getOthers(); -``` - -### Related Types - -#### SpaceMember - -A SpaceMember represents a member within a Space instance. Each new connection that enters will create a new member, even if they have the same [`clientId`](https://ably.com/docs/auth/identified-clients?lang=javascript). - -```ts -type SpaceMember = { - clientId: string; - connectionId: string; - isConnected: boolean; - profileData: Record<string, unknown>; - location: unknown; - lastEvent: { - name: Types.PresenceAction; - timestamp: number; - }; -}; -``` - -##### clientId - -The client identifier for the user, provided to the ably client instance. - -##### connectionId - -Identifier for the connection used by the user. This is a unique identifier. - -##### isConnected - -Whether the user is connected to Ably. - -##### profileData - -Optional user data that can be attached to a user, such as a username or image to display in an avatar stack. - -##### location - -The current location of the user within the space. - -##### lastEvent - -The most recent event emitted by [presence](https://ably.com/docs/presence-occupancy/presence?lang=javascript) and its timestamp. Events will be either `enter`, `leave`, `update` or `present`. - -## Member Locations - -Handles the tracking of member locations within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). - -### Methods - -#### subscribe() - -Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. - -Available events: - -- ##### **update** - - Fires when a member updates their location. The argument supplied to the event listener is an UpdateEvent. - - ```ts - space.locations.subscribe('update', (locationUpdate: LocationsEvents.UpdateEvent) => {}); - ``` - -#### set() - -Set your current location. The `location` argument can be any JSON-serializable object. Emits a `locationUpdate` event to all connected clients in this space. - -```ts -type set = (location: unknown) => Promise<void>; -``` - -#### unsubscribe() - -Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. - -```ts -space.locations.unsubscribe('update'); -``` - -#### getSelf() - -Get location for self. - -```ts -type getSelf = () => Promise<unknown>; -``` - -Example: - -```ts -const myLocation = await space.locations.getSelf(); -``` - -#### getAll() - -Get location for all members. - -```ts -type getAll = () => Promise<Record<ConnectionId, unknown>>; -``` - -Example: - -```ts -const allLocations = await space.locations.getAll(); -``` - -#### getOthers() - -Get location for other members - -```ts -type getOthers = () => Promise<Record<ConnectionId, unknown>>; -``` - -Example: - -```ts -const otherLocations = await space.locations.getOthers() -``` - -### Related types - -## Live Cursors - -Handles tracking of member cursors within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). - -### Methods - -#### subscribe() - -Listen to `CursorUpdate` events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. - -Available events: - -- ##### **update** - - Emits an event when a new cursor position is set. The argument supplied to the event listener is a [CursorUpdate](#cursorupdate). - - ```ts - space.cursors.subscribe('update', (cursorUpdate: CursorUpdate) => {}); - ``` - -#### set() - -Set the position of a cursor. If a member has not yet entered the space, this method will error. - -A event payload returned contains an object with 2 properties. `position` is an object with 2 required properties, `x` and `y`. These represent the position of the cursor on a 2D plane. A second optional property, `data` can also be passed. This is an object of any shape and is meant for data associated with the cursor movement (like drag or hover calculation results): - -```ts -type set = (update: { position: CursorPosition, data?: CursorData }) => void; -``` - -Example usage: - -```ts -window.addEventListener('mousemove', ({ clientX, clientY }) => { - space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: "red" } }); -}); -``` - -#### unsubscribe() - -Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. - -```ts -space.cursors.unsubscribe('update'); -``` - -#### getSelf() - -Get the last `CursorUpdate` object for self. - -```ts -type getSelf = () => Promise<CursorUpdate | null>; -``` - -Example: - -```ts -const selfPosition = await space.cursors.getSelf(); -``` - -#### getAll() - -Get the last `CursorUpdate` object for all the members. - -```ts -type getAll = () => Promise<Record<string, CursorUpdate | null>>; -``` - -Example: - -```ts -const allLatestPositions = await space.cursors.getAll(); -``` - -#### getOthers() - -Get the last `CursorUpdate` object for everyone else but yourself. - -```ts -type getOthers = () => Promise<Record<string, CursorUpdate | null>>; -``` - -Example: - -```ts -const otherPositions = await space.cursors.getOthers(); -``` - -### Related types - -#### CursorUpdate - -Represents an update to a cursor. - -```ts -type CursorUpdate = { - name: string; - clientId: string; - connectionId: string; - position: CursorPosition; - data?: CursorData; -}; -``` - -#### CursorPosition - -Represents a cursors position. - -```ts -type CursorPosition = { - x: number; - y: number; -}; -``` - -#### CursorData - -Represent data that can be associated with a cursor update. - -```ts -type CursorData = Record<string, unknown>; -``` - -## Component Locking - -Provides a mechanism to "lock" a component, reducing the chances of conflict in an application whilst being edited by multiple members. Inherits from [EventEmitter](/docs/usage.md#event-emitters). - -### Methods - -#### acquire() - -Send a request to acquire a lock. Returns a Promise which resolves once the request has been sent. A resolved Promise holds a `pending` [Lock](#lock). An error will be thrown if a lock request with a status of `pending` or `locked` already exists, returning a rejected promise. - -When a lock acquisition by a member is confirmed with the `locked` status, an `update` event will be emitted. Hence to handle lock acquisition, `acquire()` needs to always be used together with `subscribe()`. - -```ts -type acquire = (lockId: string) => Promise<Lock>; -``` - -Example: - -```ts -const id = "/slide/1/element/3"; -const lockRequest = await space.locks.acquire(id); -``` - -#### release() - -Releases a previously requested lock. - -```ts -type release = (lockId: string) => Promise<void>; -``` - -Example: - -```ts -const id = "/slide/1/element/3"; -await space.locks.release(id); -``` - -#### subscribe() - -Listen to lock events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. - -Available events: - -- ##### **update** - - Listen to changes to locks. - - ```ts - space.locks.subscribe('update', (lock: Lock) => {}) - ``` - - The argument supplied to the callback is a [Lock](#lock), representing the lock request and it's status. - -#### unsubscribe() - -Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. - -```ts -space.locks.unsubscribe('update'); -``` - -#### get() - -Get a lock by its id. - -```ts -type get = (lockId: string) => Lock | undefined -``` - -Example: - -```ts -const id = "/slide/1/element/3"; -const lock = space.locks.get(id); -``` - -#### getSelf() - -Get all locks belonging to self that have the `locked` status. - -```ts -type getSelf = () => Promise<Lock[]> -``` - -Example: - -```ts -const locks = await space.locks.getSelf(); -``` - -#### getOthers() - -Get all locks belonging to all members except self that have the `locked` status. - -```ts -type getOthers = () => Promise<Lock[]> -``` - -Example: - -```ts -const locks = await space.locks.getOthers(); -``` - -#### getAll() - -Get all locks that have the `locked` status. - -```ts -type getAll = () => Promise<Lock[]> -``` - -Example: - -```ts -const locks = await space.locks.getAll(); -``` - -### Related types - -#### Lock - -Represents a Lock. - -```ts -type Lock = { - id: string; - status: LockStatus; - member: SpaceMember; - timestamp: number; - attributes?: LockAttributes; - reason?: Types.ErrorInfo; -}; -``` - -#### LockStatus - -Represents a status of a lock. - -```ts -type LockStatus = 'pending' | 'locked' | 'unlocked'; -``` - -#### LockAttributes - -Additional attributes that can be set when acquiring a lock. - -```ts -type LockAttributes = Record<string, unknown>; -``` \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md index 16a6fe66..87425bfe 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -65,7 +65,7 @@ const space = await spaces.get('demoSlideshow'); ### Options -A set of `spaceOptions` can be passed to space when creating or retrieving it. See the [class definitions](/docs/class-definitions.md#spaceoptions) for details on what options are available. +A set of `spaceOptions` can be passed to space when creating or retrieving it. See the [class definitions](https://sdk.ably.com/builds/ably/spaces/main/typedoc/interfaces/SpaceOptions.html) for details on what options are available. The following is an example of setting `offlineTimeout` to 3 minutes and a `paginationLimit` of 10: @@ -161,7 +161,7 @@ When you enter a space, you become a `member`. On the client, your own membershi } ``` -See [SpaceMember](/docs/class-definitions.md#spacemember) for details on properties. +See [SpaceMember](https://sdk.ably.com/builds/ably/spaces/main/typedoc/interfaces/SpaceMember.html) for details on properties. ### Member events From 86b9a0a7104cd539e38dc8a4fe4e76ad8eca1705 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 19 Oct 2023 09:57:37 -0300 Subject: [PATCH 131/191] Remove mocking of presence enter completion in space.enter() test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit space.enter() doesn’t await the result of the presence enter operation (whether it should or not is another matter), and the other tests for space.enter() don’t mock its completion, so let’s be consistent with them. --- src/Space.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Space.test.ts b/src/Space.test.ts index 99e268f9..ea33a7ef 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -61,7 +61,7 @@ describe('Space', () => { describe('enter', () => { it<SpaceTestContext>('enter a space successfully', async ({ space, presence }) => { - const spy = vi.spyOn(presence, 'enter').mockResolvedValueOnce(); + const spy = vi.spyOn(presence, 'enter'); await space.enter({ name: 'Betty' }); expect(spy).toHaveBeenNthCalledWith(1, createProfileUpdate({ current: { name: 'Betty' } })); }); From 822a3fd99cdc48ed69c2d3e88435077202b73be3 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 18 Oct 2023 10:11:44 -0300 Subject: [PATCH 132/191] Format auto-mocks --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4e87fcc2..a3b988a3 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write --ignore-path .gitignore src demo", - "format:check": "prettier --check --ignore-path .gitignore src demo", + "format": "prettier --write --ignore-path .gitignore src demo __mocks__", + "format:check": "prettier --check --ignore-path .gitignore src demo __mocks__", "test": "vitest run", "test:watch": "vitest watch", "coverage": "vitest run --coverage", From efc3989fa8a41f97f6681129a8f9702e89afcd65 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 18 Oct 2023 10:10:10 -0300 Subject: [PATCH 133/191] Make each test context create new mock objects This removes the possibility of one test case affecting another due to shared mock objects. --- __mocks__/ably/promises/index.ts | 83 +++++++++++++++++++------------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/__mocks__/ably/promises/index.ts b/__mocks__/ably/promises/index.ts index e664bcf2..f16a7a4a 100644 --- a/__mocks__/ably/promises/index.ts +++ b/__mocks__/ably/promises/index.ts @@ -5,46 +5,58 @@ const MOCK_CLIENT_ID = 'MOCK_CLIENT_ID'; const mockPromisify = <T>(expectedReturnValue): Promise<T> => new Promise((resolve) => resolve(expectedReturnValue)); const methodReturningVoidPromise = () => mockPromisify<void>((() => {})()); -const mockPresence = { - get: () => mockPromisify<Types.PresenceMessage[]>([]), - update: () => mockPromisify<void>(undefined), - enter: methodReturningVoidPromise, - leave: methodReturningVoidPromise, - subscriptions: { - once: (_: unknown, fn: Function) => { - fn(); +function createMockPresence() { + return { + get: () => mockPromisify<Types.PresenceMessage[]>([]), + update: () => mockPromisify<void>(undefined), + enter: methodReturningVoidPromise, + leave: methodReturningVoidPromise, + subscriptions: { + once: (_: unknown, fn: Function) => { + fn(); + }, }, - }, - subscribe: () => {}, -}; + subscribe: () => {}, + }; +} -const mockHistory = { - items: [], - first: () => mockPromisify(mockHistory), - next: () => mockPromisify(mockHistory), - current: () => mockPromisify(mockHistory), - hasNext: () => false, - isLast: () => true, -}; +function createMockHistory() { + const mockHistory = { + items: [], + first: () => mockPromisify(mockHistory), + next: () => mockPromisify(mockHistory), + current: () => mockPromisify(mockHistory), + hasNext: () => false, + isLast: () => true, + }; + return mockHistory; +} -const mockEmitter = { - any: [], - events: {}, - anyOnce: [], - eventsOnce: {}, -}; +function createMockEmitter() { + return { + any: [], + events: {}, + anyOnce: [], + eventsOnce: {}, + }; +} -const mockChannel = { - presence: mockPresence, - history: () => mockHistory, - subscribe: () => {}, - publish: () => {}, - subscriptions: mockEmitter, -}; +function createMockChannel() { + return { + presence: createMockPresence(), + history: (() => { + const mockHistory = createMockHistory(); + return () => mockHistory; + })(), + subscribe: () => {}, + publish: () => {}, + subscriptions: createMockEmitter(), + }; +} class MockRealtime { public channels: { - get: () => typeof mockChannel; + get: () => ReturnType<typeof createMockChannel>; }; public auth: { clientId: string; @@ -58,7 +70,10 @@ class MockRealtime { constructor() { this.channels = { - get: () => mockChannel, + get: (() => { + const mockChannel = createMockChannel(); + return () => mockChannel; + })(), }; this.auth = { clientId: MOCK_CLIENT_ID, From 7dae5fc47f214188d7f3cf9d03e52fb7120f97fc Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Thu, 19 Oct 2023 09:19:19 -0300 Subject: [PATCH 134/191] Remove unnecessary space.enter() from some tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These calls presumably exist because the method that they are testing requires that the client has entered the space. But calling `space.enter()` in the context of these tests does nothing useful — the underlying Realtime object is a mock, and the `presenceMap` objects in the tests’ contexts have already been configured such that a member is present for the space’s connection ID. --- src/Locations.test.ts | 1 - src/Locks.test.ts | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/src/Locations.test.ts b/src/Locations.test.ts index f78ea4c2..b6c488d3 100644 --- a/src/Locations.test.ts +++ b/src/Locations.test.ts @@ -49,7 +49,6 @@ describe('Locations', () => { it<SpaceTestContext>('sends a presence update on location set', async ({ space, presence }) => { const spy = vi.spyOn(presence, 'update'); - await space.enter(); await space.locations.set('location1'); expect(spy).toHaveBeenCalledWith(createLocationUpdate({ current: 'location1' })); }); diff --git a/src/Locks.test.ts b/src/Locks.test.ts index 2f1a6216..5441799a 100644 --- a/src/Locks.test.ts +++ b/src/Locks.test.ts @@ -49,8 +49,6 @@ describe('Locks', () => { space, presence, }) => { - await space.enter(); - const presenceUpdate = vi.spyOn(presence, 'update'); const lockID = 'test'; @@ -71,8 +69,6 @@ describe('Locks', () => { }); it<SpaceTestContext>('includes attributes in the lock request when provided', async ({ space, presence }) => { - await space.enter(); - const presenceUpdate = vi.spyOn(presence, 'update'); const lockID = 'test'; @@ -89,8 +85,6 @@ describe('Locks', () => { }); it<SpaceTestContext>('errors if a PENDING request already exists', async ({ space }) => { - await space.enter(); - const lockID = 'test'; await space.locks.acquire(lockID); expect(space.locks.acquire(lockID)).rejects.toThrowError(); @@ -108,7 +102,6 @@ describe('Locks', () => { }); it<SpaceTestContext>('sets a PENDING request to LOCKED', async ({ space }) => { - await space.enter(); const member = (await space.members.getSelf())!; const emitSpy = vi.spyOn(space.locks, 'emit'); @@ -173,8 +166,6 @@ describe('Locks', () => { }, ])('$name', ({ desc, otherConnId, otherTimestamp, expectedSelfStatus, expectedOtherStatus }) => { it<SpaceTestContext>(desc, async ({ client, space }) => { - await space.enter(); - // process a PENDING request for the other member, which should // transition to LOCKED let msg = Realtime.PresenceMessage.fromValues({ @@ -230,7 +221,6 @@ describe('Locks', () => { }); it<SpaceTestContext>('sets a released request to UNLOCKED', async ({ space }) => { - await space.enter(); const member = (await space.members.getSelf())!; let msg = Realtime.PresenceMessage.fromValues({ @@ -263,7 +253,6 @@ describe('Locks', () => { }); it<SpaceTestContext>('sets all locks to UNLOCKED when a member leaves', async ({ space }) => { - await space.enter(); const member = (await space.members.getSelf())!; let msg = Realtime.PresenceMessage.fromValues({ @@ -311,7 +300,6 @@ describe('Locks', () => { }); it<SpaceTestContext>('removes the identified lock request from presence extras', async ({ space, presence }) => { - await space.enter(); const member = (await space.members.getSelf())!; const lockID = 'test'; From 1a5cffb0387169b4bdb4dca43761615722219877 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 17 Oct 2023 16:25:05 -0300 Subject: [PATCH 135/191] Remove usage of private presence API Instead of using the private `subscriptions` property, implement a `once`-like behaviour by passing `subscribe` a listener that removes itself once triggered. --- __mocks__/ably/promises/index.ts | 1 + src/Space.test.ts | 8 ++++++++ src/Space.ts | 10 +++++----- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/__mocks__/ably/promises/index.ts b/__mocks__/ably/promises/index.ts index f16a7a4a..ec352472 100644 --- a/__mocks__/ably/promises/index.ts +++ b/__mocks__/ably/promises/index.ts @@ -17,6 +17,7 @@ function createMockPresence() { }, }, subscribe: () => {}, + unsubscribe: () => {}, }; } diff --git a/src/Space.test.ts b/src/Space.test.ts index ea33a7ef..e0c1de87 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -60,6 +60,14 @@ describe('Space', () => { }); describe('enter', () => { + beforeEach<SpaceTestContext>(({ presence }) => { + vi.spyOn(presence, 'subscribe').mockImplementation( + async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => { + listener!(createPresenceMessage('enter' /* arbitrarily chosen */)); + }, + ); + }); + it<SpaceTestContext>('enter a space successfully', async ({ space, presence }) => { const spy = vi.spyOn(presence, 'enter'); await space.enter({ name: 'Betty' }); diff --git a/src/Space.ts b/src/Space.ts index ecc06b5d..f6fb22cf 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -244,18 +244,18 @@ class Space extends EventEmitter<SpaceEventMap> { return new Promise((resolve) => { const presence = this.channel.presence; - interface PresenceWithSubscriptions extends Types.RealtimePresencePromise { - subscriptions: EventEmitter<{ enter: [unknown] }>; - } + const presenceListener = async () => { + presence.unsubscribe(presenceListener); - (presence as PresenceWithSubscriptions).subscriptions.once('enter', async () => { const presenceMessages = await presence.get(); presenceMessages.forEach((msg) => this.locks.processPresenceMessage(msg)); const members = await this.members.getAll(); resolve(members); - }); + }; + + presence.subscribe('enter', presenceListener); const update = new SpaceUpdate({ self: null, extras: null }); this.presenceEnter(update.updateProfileData(profileData)); From 4080f31f486cff77c4e34247ffc584b2c98bdd54 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Wed, 18 Oct 2023 13:59:13 -0300 Subject: [PATCH 136/191] Check sender of ENTER presence message in space.enter() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It shouldn’t return as a result of somebody else entering the space. I’m not sure whether this is the best way of predicating whether or not to return — perhaps checking for some unique ID on the received ENTER presence message would be better. But it seems like an OK low-thought solution at least for the time being. --- src/Space.test.ts | 41 ++++++++++++++++++++++++++++++++++++++++- src/Space.ts | 11 ++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/Space.test.ts b/src/Space.test.ts index e0c1de87..fbef923f 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -63,7 +63,9 @@ describe('Space', () => { beforeEach<SpaceTestContext>(({ presence }) => { vi.spyOn(presence, 'subscribe').mockImplementation( async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => { - listener!(createPresenceMessage('enter' /* arbitrarily chosen */)); + listener!( + createPresenceMessage('enter' /* arbitrarily chosen */, { clientId: 'MOCK_CLIENT_ID', connectionId: '1' }), + ); }, ); }); @@ -74,6 +76,43 @@ describe('Space', () => { expect(spy).toHaveBeenNthCalledWith(1, createProfileUpdate({ current: { name: 'Betty' } })); }); + describe.each([ + { + scenario: 'when it receives a presence message from a client ID and connection ID that matches ours', + presenceMessageData: { clientId: 'MOCK_CLIENT_ID', connectionId: '1' }, + shouldComplete: true, + }, + { + scenario: 'when it receives a presence message from a client ID that isn’t ours', + presenceMessageData: { clientId: 'OTHER_MOCK_CLIENT_ID', connectionId: '1' }, + shouldComplete: false, + }, + { + scenario: 'when it receives a presence message from a connection ID that isn’t ours', + presenceMessageData: { clientId: 'MOCK_CLIENT_ID', connectionId: '2' }, + shouldComplete: false, + }, + ])('$scenario', ({ presenceMessageData, shouldComplete }) => { + it<SpaceTestContext>(shouldComplete ? 'completes' : 'does not complete', async ({ space, presence }) => { + const unsubscribeSpy = vi.spyOn(presence, 'unsubscribe'); + + vi.spyOn(presence, 'subscribe').mockImplementation( + async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => { + listener!(createPresenceMessage('enter' /* arbitrarily chosen */, presenceMessageData)); + }, + ); + + space.enter(); + + // Note: There’s no nice way (i.e. without timeouts) to actually check that space.enter() didn’t complete, so we use "did it remove its presence listener?" as a proxy for "did it complete?" + if (shouldComplete) { + expect(unsubscribeSpy).toHaveBeenCalled(); + } else { + expect(unsubscribeSpy).not.toHaveBeenCalled(); + } + }); + }); + it<SpaceTestContext>('returns current space members', async ({ presenceMap, space }) => { presenceMap.set('1', createPresenceMessage('enter')); presenceMap.set('2', createPresenceMessage('update', { clientId: '2', connectionId: '2' })); diff --git a/src/Space.ts b/src/Space.ts index f6fb22cf..7761b381 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -244,7 +244,16 @@ class Space extends EventEmitter<SpaceEventMap> { return new Promise((resolve) => { const presence = this.channel.presence; - const presenceListener = async () => { + const presenceListener = async (presenceMessage: Types.PresenceMessage) => { + if ( + !( + presenceMessage.clientId == this.client.auth.clientId && + presenceMessage.connectionId == this.client.connection.id + ) + ) { + return; + } + presence.unsubscribe(presenceListener); const presenceMessages = await presence.get(); From 6e779413c43f0e7ac83a3edaa34a2d6db4c30ec6 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 17 Oct 2023 13:29:36 -0300 Subject: [PATCH 137/191] =?UTF-8?q?Fix=20bug=20where=20space.enter()=20som?= =?UTF-8?q?etimes=20wouldn=E2=80=99t=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling presence.enter() does not necessarily result in the presence object emitting an ENTER event. This could happen, for example, if the channel does not become attached quickly enough, or if a transport upgrade is happening at roughly the same time as the presence enter call (note that the latter possibility means that we wouldn’t gain much by trying to work around the former by making sure the channel becomes attached before performing the presence enter). In both these cases, the only visible side effect of the presence enter call will be a PRESENT event emitted as a result of a presence SYNC. So, we update space.enter such that it also will return if it receives a PRESENT event for the current member. Resolves COL-335. --- src/Space.test.ts | 6 ++++-- src/Space.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Space.test.ts b/src/Space.test.ts index fbef923f..d8f7a011 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -71,9 +71,11 @@ describe('Space', () => { }); it<SpaceTestContext>('enter a space successfully', async ({ space, presence }) => { - const spy = vi.spyOn(presence, 'enter'); + const presenceEnterSpy = vi.spyOn(presence, 'enter'); + const presenceSubscribeSpy = vi.spyOn(presence, 'subscribe'); await space.enter({ name: 'Betty' }); - expect(spy).toHaveBeenNthCalledWith(1, createProfileUpdate({ current: { name: 'Betty' } })); + expect(presenceEnterSpy).toHaveBeenNthCalledWith(1, createProfileUpdate({ current: { name: 'Betty' } })); + expect(presenceSubscribeSpy).toHaveBeenCalledWith(['enter', 'present'], expect.any(Function)); }); describe.each([ diff --git a/src/Space.ts b/src/Space.ts index 7761b381..ada781fd 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -264,7 +264,7 @@ class Space extends EventEmitter<SpaceEventMap> { resolve(members); }; - presence.subscribe('enter', presenceListener); + presence.subscribe(['enter', 'present'], presenceListener); const update = new SpaceUpdate({ self: null, extras: null }); this.presenceEnter(update.updateProfileData(profileData)); From 944109dea1bd16dd3bfc2c0352efff25f137c66b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 10 Oct 2023 12:10:26 -0300 Subject: [PATCH 138/191] Fix CDN instructions in README and usage docs When used via the CDN, ably-js exposes itself through a global `Ably` object. --- README.md | 8 +++++++- docs/usage.md | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d4eee6a..4d2ecc33 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,13 @@ You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): <script src="https://cdn.ably.com/lib/ably.min-1.js"></script> <script src="https://cdn.ably.com/spaces/0.1.2/iife/index.bundle.js"></script> ``` -After this, instantiate the SDK in the same way as in the NPM option above. + +After this, instantiate the SDK in the same way as in the NPM option above: + +```ts +const client = new Ably.Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" }); +const spaces = new Spaces(client); +``` ## Creating a new Space diff --git a/docs/usage.md b/docs/usage.md index 87425bfe..02c47d95 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -37,6 +37,10 @@ You can also use Spaces with a CDN like [unpkg](https://www.unpkg.com/): <script src="https://unpkg.com/@ably/spaces@0.1.2/dist/iife/index.bundle.js"></script> ``` +> **Note** +> +> If you do this, then replace the call to `new Realtime.Promise` in the next section with `new Ably.Realtime.Promise`. + ## Authentication and instantiation Spaces use an [Ably promise-based realtime client](https://github.com/ably/ably-js#using-the-async-api-style). You can pass an Ably client directly to the spaces constructor. From 60a8a0b0c3d86c214ca53a11939bec83a0809a18 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Tue, 10 Oct 2023 16:34:38 -0300 Subject: [PATCH 139/191] Update Prettier to 3.0.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I am going to introduce a submodule and want to make use of a .prettierignore file to tell Prettier to ignore it. However, we are already overriding the --ignore-path flag to tell it to ignore the files described by .gitignore. Prettier 3 ignores the files described by .gitignore by default. This means that we can now remove our usage of --ignore-path and it’ll ignore the files described by .gitignore _and_ .prettierignore. --- .prettierrc.json | 1 - demo/index.html | 2 +- package-lock.json | 18 +++++++++--------- package.json | 6 +++--- src/Locations.ts | 5 ++++- src/Locks.ts | 5 ++++- 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index f8ea5495..6aedd184 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,5 +1,4 @@ { - "trailingComma": "all", "tabWidth": 2, "semi": true, "singleQuote": true, diff --git a/demo/index.html b/demo/index.html index adac4a53..171f162b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,4 +1,4 @@ -<!DOCTYPE html> +<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> diff --git a/package-lock.json b/package-lock.json index 99eebeb9..70d623e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", - "prettier": "^2.8.3", + "prettier": "^3.0.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", @@ -3964,15 +3964,15 @@ } }, "node_modules/prettier": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz", - "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -8036,9 +8036,9 @@ "dev": true }, "prettier": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz", - "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true }, "pretty-format": { diff --git a/package.json b/package.json index a3b988a3..221ccc10 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write --ignore-path .gitignore src demo __mocks__", - "format:check": "prettier --check --ignore-path .gitignore src demo __mocks__", + "format": "prettier --write src demo __mocks__", + "format:check": "prettier --check src demo __mocks__", "test": "vitest run", "test:watch": "vitest watch", "coverage": "vitest run --coverage", @@ -50,7 +50,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", - "prettier": "^2.8.3", + "prettier": "^3.0.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", diff --git a/src/Locations.ts b/src/Locations.ts index 27a49cf2..b1483d12 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -66,7 +66,10 @@ export default class Locations extends EventEmitter<LocationsEventMap> { private lastLocationUpdate: Record<string, PresenceMember['data']['locationUpdate']['id']> = {}; /** @internal */ - constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { + constructor( + private space: Space, + private presenceUpdate: Space['presenceUpdate'], + ) { super(); } diff --git a/src/Locks.ts b/src/Locks.ts index 311e4bbb..ec0638e9 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -83,7 +83,10 @@ export default class Locks extends EventEmitter<LocksEventMap> { private locks: Map<string, Map<string, Lock>>; /** @internal */ - constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { + constructor( + private space: Space, + private presenceUpdate: Space['presenceUpdate'], + ) { super(); this.locks = new Map(); } From fa95f9f16e38fc0e025297aa30c6f147936e6abc Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian <lawrence@forooghian.com> Date: Mon, 9 Oct 2023 17:11:34 -0300 Subject: [PATCH 140/191] Add a script to test the bundle that will be uploaded to CDN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `test:cdn_bundle` NPM script and run it in CI. This script tests the bundle that the `.github/workflows/cdn.yml` workflow will upload to the CDN, by checking that, when this bundle is imported by a web page, it gives access to a working Spaces client. The approach (using Playwright and a local web server powered by Express) is based heavily on that used by the `test:playwright` NPM script in ably-js at commit 83b4516. I then incorporated Dom’s suggestion of using Playwright’s built-in test-running functionality, taking code from his draft PR #218. --- .eslintrc.js | 2 +- .github/workflows/cdn.yml | 1 + .github/workflows/dev-ci.yml | 13 + .gitignore | 1 + .gitmodules | 3 + .prettierignore | 1 + CONTRIBUTING.md | 9 + package-lock.json | 1284 ++++++++++++++++++++++++ package.json | 9 +- test/ably-common | 1 + test/cdn-bundle/lib/ablySandbox.ts | 61 ++ test/cdn-bundle/lib/webServer.ts | 30 + test/cdn-bundle/playwright.config.js | 12 + test/cdn-bundle/resources/test.html | 42 + test/cdn-bundle/server.ts | 5 + test/cdn-bundle/test/cdnBundle.test.ts | 30 + test/cdn-bundle/tsconfig.json | 9 + vitest.config.ts | 4 +- 18 files changed, 1513 insertions(+), 4 deletions(-) create mode 100644 .gitmodules create mode 100644 .prettierignore create mode 160000 test/ably-common create mode 100644 test/cdn-bundle/lib/ablySandbox.ts create mode 100644 test/cdn-bundle/lib/webServer.ts create mode 100644 test/cdn-bundle/playwright.config.js create mode 100644 test/cdn-bundle/resources/test.html create mode 100644 test/cdn-bundle/server.ts create mode 100644 test/cdn-bundle/test/cdnBundle.test.ts create mode 100644 test/cdn-bundle/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index a540ebff..1c321c08 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,7 +42,7 @@ module.exports = { extends: ['plugin:jsdoc/recommended'], }, ], - ignorePatterns: ['dist', 'build', 'examples'], + ignorePatterns: ['dist', 'build', 'examples', 'test/ably-common'], settings: { jsdoc: { tagNamePreference: { diff --git a/.github/workflows/cdn.yml b/.github/workflows/cdn.yml index cfc9613e..17e26e5a 100644 --- a/.github/workflows/cdn.yml +++ b/.github/workflows/cdn.yml @@ -45,5 +45,6 @@ jobs: run: | npm ci npm run build + # Note: If you modify what we upload to the CDN, you must make sure you keep the `test:cdn-bundle` NPM script in sync with your changes. - run: | aws s3 cp ./dist/iife/index.bundle.js s3://${{ github.event.inputs.bucket }}/spaces/${{ github.event.inputs.version }}/iife/index.bundle.js diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index d78aa820..66ac38fe 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -80,3 +80,16 @@ jobs: sourcePath: docs/typedoc/generated githubToken: ${{ secrets.GITHUB_TOKEN }} artifactName: typedoc + test-cdn-bundle: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: actions/setup-node@v1 + with: + node-version: 18 + - run: npm ci + - run: npx playwright install chromium + - run: npm run build + - run: npm run test:cdn-bundle diff --git a/.gitignore b/.gitignore index d5382a82..61efec7f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules/ coverage .env docs/typedoc/generated/ +test-results/ # Local Netlify folder .netlify diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..b4b20984 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test/ably-common"] + path = test/ably-common + url = https://github.com/ably/ably-common.git diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..7b117036 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +test/ably-common/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 698ed8d8..3be2f6b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,3 +34,12 @@ To run the Jest tests, simply run the following command: ```bash npm test ``` + +### CDN bundle test + +To test the bundle that we upload to the CDN: + +1. Initialize submodules: `git submodule update --init` +2. Install browser for Playwright to use: `npx run playwright install chromium` +3. Build the bundle: `npm run build` +4. Run the test: `npm run test:cdn-bundle` diff --git a/package-lock.json b/package-lock.json index 70d623e7..20cae84d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,10 @@ "nanoid": "^5.0.2" }, "devDependencies": { + "@playwright/test": "^1.39.0", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-terser": "^0.4.3", + "@types/express": "^4.17.18", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", @@ -22,6 +24,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", + "express": "^4.18.2", "prettier": "^3.0.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", @@ -661,6 +664,21 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "dev": true, + "dependencies": { + "playwright": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.2.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", @@ -784,6 +802,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/body-parser": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", + "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -811,18 +839,57 @@ "@types/chai": "*" } }, + "node_modules/@types/connect": { + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.19.tgz", + "integrity": "sha512-UtOfBtzN9OvpZPPbnnYunfjM7XCI4jyk1NvnFhTVz5krYAnW4o5DCoIekvms+8ApqhB4+9wSge1kBijdfTSmfg==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.37", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", + "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", "peer": true }, + "node_modules/@types/http-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", + "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -850,11 +917,29 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", + "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", + "dev": true + }, "node_modules/@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "node_modules/@types/qs": { + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", + "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", + "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", + "dev": true + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -876,6 +961,27 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "node_modules/@types/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", + "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", + "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.51.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz", @@ -1277,6 +1383,19 @@ } } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1387,6 +1506,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, "node_modules/array-includes": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", @@ -1532,6 +1657,45 @@ "node": ">= 0.4" } }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/bops": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", @@ -1582,6 +1746,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/c8": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", @@ -1771,12 +1944,48 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1893,6 +2102,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -1926,12 +2154,27 @@ "node": ">=6.0.0" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -2080,6 +2323,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2445,6 +2694,72 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2524,6 +2839,39 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2581,6 +2929,24 @@ "node": ">=8.0.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2922,6 +3288,22 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "peer": true }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -2935,6 +3317,18 @@ "node": ">=10.19.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2999,6 +3393,15 @@ "node": ">= 0.4" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3526,6 +3929,21 @@ "node": ">= 12" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3535,6 +3953,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -3548,6 +3975,39 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -3625,6 +4085,15 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -3719,6 +4188,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3822,6 +4303,15 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3855,6 +4345,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3908,6 +4404,36 @@ "pathe": "^1.1.0" } }, + "node_modules/playwright": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "dev": true, + "dependencies": { + "playwright-core": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4004,6 +4530,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4023,6 +4562,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4064,6 +4618,30 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -4286,6 +4864,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4301,6 +4885,51 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/serialize-javascript": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", @@ -4310,6 +4939,27 @@ "randombytes": "^2.1.0" } }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4440,6 +5090,15 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", @@ -4645,6 +5304,15 @@ "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==", "peer": true }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -4763,6 +5431,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", @@ -4907,6 +5588,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4916,6 +5606,15 @@ "punycode": "^2.1.0" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -4936,6 +5635,15 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "4.4.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", @@ -5642,6 +6350,15 @@ "fastq": "^1.6.0" } }, + "@playwright/test": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "dev": true, + "requires": { + "playwright": "1.39.0" + } + }, "@rollup/plugin-node-resolve": { "version": "15.2.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", @@ -5723,6 +6440,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "@types/body-parser": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", + "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -5750,18 +6477,57 @@ "@types/chai": "*" } }, + "@types/connect": { + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "@types/express": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.19.tgz", + "integrity": "sha512-UtOfBtzN9OvpZPPbnnYunfjM7XCI4jyk1NvnFhTVz5krYAnW4o5DCoIekvms+8ApqhB4+9wSge1kBijdfTSmfg==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.37", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", + "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", "peer": true }, + "@types/http-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", + "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -5789,11 +6555,29 @@ "@types/node": "*" } }, + "@types/mime": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", + "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", + "dev": true + }, "@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "@types/qs": { + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", + "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", + "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", + "dev": true + }, "@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -5815,6 +6599,27 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "@types/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", + "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", + "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", + "dev": true, + "requires": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "5.51.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz", @@ -6053,6 +6858,16 @@ "ws": "^5.1" } }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -6133,6 +6948,12 @@ "is-array-buffer": "^3.0.1" } }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, "array-includes": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", @@ -6233,6 +7054,43 @@ "integrity": "sha512-ZXBDPMt/v/8fsIqn+Z5VwrhdR6jVka0bYobHdGia0Nxi7BJ9i/Uvml3AocHIBtIIBhZjBw5MR0aR4ROs/8+SNg==", "peer": true }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "bops": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", @@ -6274,6 +7132,12 @@ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, "c8": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/c8/-/c8-7.14.0.tgz", @@ -6421,12 +7285,39 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, "convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -6507,6 +7398,18 @@ "object-keys": "^1.1.1" } }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -6531,12 +7434,24 @@ "esutils": "^2.0.2" } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -6660,6 +7575,12 @@ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6944,6 +7865,68 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7013,6 +7996,38 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7058,6 +8073,18 @@ "signal-exit": "^3.0.2" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7296,6 +8323,19 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "peer": true }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, "http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -7306,6 +8346,15 @@ "resolve-alpn": "^1.0.0" } }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -7355,6 +8404,12 @@ "side-channel": "^1.0.4" } }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, "is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -7737,12 +8792,30 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, "micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -7753,6 +8826,27 @@ "picomatch": "^2.3.1" } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -7809,6 +8903,12 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, "normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -7873,6 +8973,15 @@ "es-abstract": "^1.20.4" } }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7945,6 +9054,12 @@ "callsites": "^3.0.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7969,6 +9084,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -8010,6 +9131,22 @@ "pathe": "^1.1.0" } }, + "playwright": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.39.0" + } + }, + "playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true + }, "postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -8060,6 +9197,16 @@ } } }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -8076,6 +9223,15 @@ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8097,6 +9253,24 @@ "safe-buffer": "^5.1.0" } }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -8235,6 +9409,12 @@ "is-regex": "^1.1.4" } }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -8244,6 +9424,52 @@ "lru-cache": "^6.0.0" } }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, "serialize-javascript": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", @@ -8253,6 +9479,24 @@ "randombytes": "^2.1.0" } }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8365,6 +9609,12 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, "std-env": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", @@ -8516,6 +9766,12 @@ "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==", "peer": true }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, "ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -8593,6 +9849,16 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typed-array-buffer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", @@ -8696,6 +9962,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -8705,6 +9977,12 @@ "punycode": "^2.1.0" } }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8722,6 +10000,12 @@ "convert-source-map": "^1.6.0" } }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, "vite": { "version": "4.4.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", diff --git a/package.json b/package.json index 221ccc10..eb8e1430 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,12 @@ "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write src demo __mocks__", - "format:check": "prettier --check src demo __mocks__", + "format": "prettier --write src demo __mocks__ test", + "format:check": "prettier --check src demo __mocks__ test", "test": "vitest run", "test:watch": "vitest watch", + "test:cdn-bundle": "npx playwright test -c test/cdn-bundle/playwright.config.js", + "test-support:cdn-server": "ts-node test/cdn-bundle/server.ts", "coverage": "vitest run --coverage", "build": "npm run build:mjs && npm run build:cjs && npm run build:iife", "build:mjs": "npx tsc --project tsconfig.mjs.json && cp res/package.mjs.json dist/mjs/package.json", @@ -40,8 +42,10 @@ }, "keywords": [], "devDependencies": { + "@playwright/test": "^1.39.0", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-terser": "^0.4.3", + "@types/express": "^4.17.18", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", @@ -50,6 +54,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", + "express": "^4.18.2", "prettier": "^3.0.3", "rollup": "^3.28.0", "ts-node": "^10.9.1", diff --git a/test/ably-common b/test/ably-common new file mode 160000 index 00000000..91e7ea81 --- /dev/null +++ b/test/ably-common @@ -0,0 +1 @@ +Subproject commit 91e7ea8112f878e909680afc6629e4599a3c31a0 diff --git a/test/cdn-bundle/lib/ablySandbox.ts b/test/cdn-bundle/lib/ablySandbox.ts new file mode 100644 index 00000000..1ec7a72b --- /dev/null +++ b/test/cdn-bundle/lib/ablySandbox.ts @@ -0,0 +1,61 @@ +import https from 'node:https'; +import testAppSetup from '../../ably-common/test-resources/test-app-setup.json'; + +export interface TestApp { + keys: TestAppKey[]; +} + +export interface TestAppKey { + id: string; + value: string; + keyName: string; + keySecret: string; + keyStr: string; + capability: string; + expires: number; +} + +export async function createSandboxAblyAPIKey() { + const data = await new Promise<Buffer>((resolve, reject) => { + const request = https.request( + { + hostname: 'sandbox-rest.ably.io', + path: '/apps', + port: 443, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + (incomingMessage) => { + if (!(incomingMessage.statusCode && incomingMessage.statusCode >= 200 && incomingMessage.statusCode < 300)) { + throw new Error(`Unexpected status code ${incomingMessage.statusCode}`); + } + + let data: Buffer | null; + incomingMessage.on('error', reject); + incomingMessage.on('data', (dataChunk: Buffer) => { + if (data) { + data = Buffer.concat([data, dataChunk]); + } else { + data = dataChunk; + } + }); + incomingMessage.on('end', () => { + resolve(data!); + }); + }, + ); + + request.on('error', reject); + + request.write(JSON.stringify(testAppSetup.post_apps), (error) => { + if (error) { + reject(error); + } + request.end(); + }); + }); + + const testApp = JSON.parse(data.toString()) as TestApp; + + return testApp.keys[0].keyStr; +} diff --git a/test/cdn-bundle/lib/webServer.ts b/test/cdn-bundle/lib/webServer.ts new file mode 100644 index 00000000..c931dc8c --- /dev/null +++ b/test/cdn-bundle/lib/webServer.ts @@ -0,0 +1,30 @@ +import express from 'express'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import os from 'node:os'; + +async function createFakeCDNDirectory() { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ably-spaces-test-cdn-bundle')); + await fs.cp( + path.join(__dirname, '..', '..', '..', 'dist', 'iife', 'index.bundle.js'), + path.join(tmpDir, 'ably-spaces-cdn-bundle.js'), + ); + + return tmpDir; +} + +export async function startWebServer(listenPort: number) { + const server = express(); + server.use(express.static(__dirname + '/../resources')); + + server.get('/', function (_req, res) { + res.redirect('/test.html'); + }); + + server.use('/node_modules', express.static(__dirname + '/../../../node_modules')); + + const fakeCDNDirectory = await createFakeCDNDirectory(); + server.use('/fake-cdn', express.static(fakeCDNDirectory)); + + server.listen(listenPort); +} diff --git a/test/cdn-bundle/playwright.config.js b/test/cdn-bundle/playwright.config.js new file mode 100644 index 00000000..9a4ac4c6 --- /dev/null +++ b/test/cdn-bundle/playwright.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: 'test', + webServer: { + command: 'npm run test-support:cdn-server', + url: 'http://localhost:4567', + }, + use: { + baseURL: 'http://localhost:4567', + }, +}); diff --git a/test/cdn-bundle/resources/test.html b/test/cdn-bundle/resources/test.html new file mode 100644 index 00000000..a2526d08 --- /dev/null +++ b/test/cdn-bundle/resources/test.html @@ -0,0 +1,42 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <title>Spaces CDN build test + + + + + + + diff --git a/test/cdn-bundle/server.ts b/test/cdn-bundle/server.ts new file mode 100644 index 00000000..8ac91d54 --- /dev/null +++ b/test/cdn-bundle/server.ts @@ -0,0 +1,5 @@ +import { startWebServer } from './lib/webServer.js'; + +(async () => { + await startWebServer(4567); +})(); diff --git a/test/cdn-bundle/test/cdnBundle.test.ts b/test/cdn-bundle/test/cdnBundle.test.ts new file mode 100644 index 00000000..3a81467d --- /dev/null +++ b/test/cdn-bundle/test/cdnBundle.test.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { createSandboxAblyAPIKey } from '../lib/ablySandbox.js'; + +test.describe('CDN bundle', () => { + /** + * Tests the bundle that the .github/workflows/cdn.yml workflow will upload to the CDN. + * + * It does this by checking that a webpage which imports this bundle is able to create and use a Spaces client. + */ + test('browser can import and use the CDN bundle', async ({ page }) => { + const pageResultPromise = new Promise((resolve, reject) => { + page.exposeFunction('onResult', (error: Error | null) => { + if (error) { + reject(error); + } else { + resolve('Success'); + } + }); + + page.exposeFunction('createSandboxAblyAPIKey', createSandboxAblyAPIKey); + }); + + await page.goto('/'); + + await expect(async () => { + const result = await pageResultPromise; + expect(result).toEqual('Success'); + }).toPass(); + }); +}); diff --git a/test/cdn-bundle/tsconfig.json b/test/cdn-bundle/tsconfig.json new file mode 100644 index 00000000..9daa1760 --- /dev/null +++ b/test/cdn-bundle/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "resolveJsonModule": true + }, + "ts-node": { + "experimentalResolver": true + } +} diff --git a/vitest.config.ts b/vitest.config.ts index b951a942..4eb92da0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,11 @@ -import { defineConfig } from 'vitest/config'; +import { configDefaults, defineConfig } from 'vitest/config'; + export default defineConfig({ test: { coverage: { reporter: ['text', 'json', 'html'], }, + exclude: [...configDefaults.exclude, 'test/ably-common', 'test/cdn-bundle'] }, }); From 05b671087ed829ee1519a164b05872ba88107a7f Mon Sep 17 00:00:00 2001 From: Evgeny Khokhlov Date: Wed, 25 Oct 2023 10:59:55 +0100 Subject: [PATCH 141/191] feat: React hooks for Spaces (#233) * feat: add `SpacesProvider` and `useSpaces` (#198) * feat: add `useLock` and `useLocks` hooks (#211) * feat: add `useLocations` hook (#206) * feat: `useMembers` implementation (#203) * feat: add `useCursors` hook (#210) * feat: add tests (#217) * feat: slide deck demo with hooks (#219) * fix: `useLocations`,`useMembers` hook overload (#232) * docs: react hook for spaces (#228) --------- Co-authored-by: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> --- .eslintrc.js | 2 + README.md | 4 + __mocks__/ably/promises/index.ts | 3 + demo/package-lock.json | 105 +- demo/package.json | 5 +- demo/src/App.tsx | 25 +- demo/src/components/CurrentSlide.tsx | 3 +- demo/src/components/Cursors.tsx | 76 +- demo/src/components/Image.tsx | 3 +- demo/src/components/Modal.tsx | 9 +- demo/src/components/SlidePreview.tsx | 13 +- demo/src/components/SpacesContext.tsx | 54 - demo/src/components/index.ts | 1 - demo/src/hooks/index.ts | 2 - demo/src/hooks/useElementSelect.ts | 12 +- demo/src/hooks/useLock.ts | 56 - demo/src/hooks/useMembers.ts | 64 - demo/src/hooks/useTextComponentLock.ts | 11 +- demo/src/hooks/useTrackCursor.ts | 17 +- demo/src/main.tsx | 34 +- docs/react.md | 211 +++ package-lock.json | 1764 +++++++++++++++++++++++- package.json | 22 +- react/package.json | 8 + src/react/contexts/SpaceContext.tsx | 48 + src/react/contexts/SpacesContext.tsx | 12 + src/react/index.ts | 9 + src/react/types.ts | 12 + src/react/useChannelState.ts | 38 + src/react/useConnectionState.ts | 35 + src/react/useCursors.test.tsx | 125 ++ src/react/useCursors.ts | 102 ++ src/react/useEventListener.ts | 43 + src/react/useLocations.test.tsx | 81 ++ src/react/useLocations.ts | 77 ++ src/react/useLock.ts | 55 + src/react/useLocks.test.tsx | 85 ++ src/react/useLocks.ts | 65 + src/react/useMembers.test.tsx | 116 ++ src/react/useMembers.ts | 122 ++ src/react/useSpace.test.tsx | 48 + src/react/useSpace.ts | 48 + src/react/useSpaces.ts | 6 + tsconfig.base.json | 2 + tsconfig.iife.json | 4 +- 45 files changed, 3296 insertions(+), 341 deletions(-) delete mode 100644 demo/src/components/SpacesContext.tsx delete mode 100644 demo/src/hooks/useLock.ts delete mode 100644 demo/src/hooks/useMembers.ts create mode 100644 docs/react.md create mode 100644 react/package.json create mode 100644 src/react/contexts/SpaceContext.tsx create mode 100644 src/react/contexts/SpacesContext.tsx create mode 100644 src/react/index.ts create mode 100644 src/react/types.ts create mode 100644 src/react/useChannelState.ts create mode 100644 src/react/useConnectionState.ts create mode 100644 src/react/useCursors.test.tsx create mode 100644 src/react/useCursors.ts create mode 100644 src/react/useEventListener.ts create mode 100644 src/react/useLocations.test.tsx create mode 100644 src/react/useLocations.ts create mode 100644 src/react/useLock.ts create mode 100644 src/react/useLocks.test.tsx create mode 100644 src/react/useLocks.ts create mode 100644 src/react/useMembers.test.tsx create mode 100644 src/react/useMembers.ts create mode 100644 src/react/useSpace.test.tsx create mode 100644 src/react/useSpace.ts create mode 100644 src/react/useSpaces.ts diff --git a/.eslintrc.js b/.eslintrc.js index 1c321c08..0a832a43 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,8 @@ module.exports = { // security/detect-object-injection just gives a lot of false positives // see https://github.com/nodesecurity/eslint-plugin-security/issues/21 'security/detect-object-injection': 'off', + // the code problem checked by this ESLint rule is automatically checked by the TypeScript compiler + 'no-redeclare': 'off', }, overrides: [ { diff --git a/README.md b/README.md index 4d2ecc33..54dfbb00 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,10 @@ const client = new Ably.Realtime.Promise({ key: "", clientId: " mockHistory; })(), subscribe: () => {}, + unsubscribe: () => {}, + on: () => {}, + off: () => {}, publish: () => {}, subscriptions: createMockEmitter(), }; diff --git a/demo/package-lock.json b/demo/package-lock.json index 59db66d1..cf8846eb 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -8,9 +8,8 @@ "name": "demo", "version": "1.0.0", "dependencies": { - "@ably-labs/react-hooks": "^3.0.0-canary.1", - "@ably/spaces": "0.1.3", - "ably": "^1.2.44", + "@ably/spaces": "file:..", + "ably": "^1.2.45", "classnames": "^2.3.2", "dayjs": "^1.11.9", "nanoid": "^4.0.2", @@ -40,6 +39,49 @@ "vite": "^4.4.5" } }, + "..": { + "name": "@ably/spaces", + "version": "0.1.3", + "license": "ISC", + "dependencies": { + "nanoid": "^4.0.2" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.3", + "@testing-library/react": "^14.0.0", + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "@vitest/coverage-c8": "^0.33.0", + "eslint": "^8.33.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsdoc": "^46.7.0", + "eslint-plugin-security": "^1.7.1", + "jsdom": "^22.1.0", + "prettier": "^2.8.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^3.28.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "vitest": "^0.34.3" + }, + "peerDependencies": { + "ably": "^1.2.45", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -49,18 +91,6 @@ "node": ">=0.10.0" } }, - "node_modules/@ably-labs/react-hooks": { - "version": "3.0.0-canary.1", - "resolved": "https://registry.npmjs.org/@ably-labs/react-hooks/-/react-hooks-3.0.0-canary.1.tgz", - "integrity": "sha512-ln2XHNwiZof3xU0jZBd1tb4wzsntmbfonqX9M40V2nlNKW7OkzSSW0e2CbAyy8LtsZPEm6vsHZHVgTcdrBxKBg==", - "dependencies": { - "ably": "^1.2.27" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -70,15 +100,8 @@ } }, "node_modules/@ably/spaces": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.3.tgz", - "integrity": "sha512-8egeUAvl+L6wrBuIIVx17BdQH+bl9rP0VRqEuYpx8+lRt8pQS+t/gyfjE3XQbg5OIaILbeiDbhgkNNDD7OOlRw==", - "dependencies": { - "nanoid": "^4.0.2" - }, - "peerDependencies": { - "ably": "^1.2.43" - } + "resolved": "..", + "link": true }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", @@ -20404,14 +20427,6 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, - "@ably-labs/react-hooks": { - "version": "3.0.0-canary.1", - "resolved": "https://registry.npmjs.org/@ably-labs/react-hooks/-/react-hooks-3.0.0-canary.1.tgz", - "integrity": "sha512-ln2XHNwiZof3xU0jZBd1tb4wzsntmbfonqX9M40V2nlNKW7OkzSSW0e2CbAyy8LtsZPEm6vsHZHVgTcdrBxKBg==", - "requires": { - "ably": "^1.2.27" - } - }, "@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -20421,11 +20436,29 @@ } }, "@ably/spaces": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@ably/spaces/-/spaces-0.1.3.tgz", - "integrity": "sha512-8egeUAvl+L6wrBuIIVx17BdQH+bl9rP0VRqEuYpx8+lRt8pQS+t/gyfjE3XQbg5OIaILbeiDbhgkNNDD7OOlRw==", + "version": "file:..", "requires": { - "nanoid": "^4.0.2" + "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.3", + "@testing-library/react": "^14.0.0", + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "@vitest/coverage-c8": "^0.33.0", + "eslint": "^8.33.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsdoc": "^46.7.0", + "eslint-plugin-security": "^1.7.1", + "jsdom": "^22.1.0", + "nanoid": "^4.0.2", + "prettier": "^2.8.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^3.28.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5", + "vitest": "^0.34.3" } }, "@alloc/quick-lru": { diff --git a/demo/package.json b/demo/package.json index 98bae423..848019ec 100644 --- a/demo/package.json +++ b/demo/package.json @@ -12,9 +12,8 @@ "deploy:production": "npm run build && netlify deploy --prod" }, "dependencies": { - "@ably-labs/react-hooks": "^3.0.0-canary.1", - "@ably/spaces": "0.1.3", - "ably": "^1.2.44", + "@ably/spaces": "file:..", + "ably": "^1.2.45", "classnames": "^2.3.2", "dayjs": "^1.11.9", "nanoid": "^4.0.2", diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 9c79e7b8..cfd7dce7 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,33 +1,36 @@ -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useMembers, useSpace, useLocations } from '@ably/spaces/react'; -import { Header, SlideMenu, SpacesContext, CurrentSlide, AblySvg, slides, Modal } from './components'; +import { Header, SlideMenu, CurrentSlide, AblySvg, slides, Modal } from './components'; import { getRandomName, getRandomColor } from './utils'; -import { useMembers } from './hooks'; import { PreviewProvider } from './components/PreviewContext.tsx'; +import { type Member } from './utils/types'; + const App = () => { - const space = useContext(SpacesContext); + const { space, enter } = useSpace(); const { self, others } = useMembers(); + const { update } = useLocations(); const [isModalVisible, setModalIsVisible] = useState(false); useEffect(() => { if (!space || self?.profileData.name) return; - const enter = async () => { + const init = async () => { const name = getRandomName(); - await space.enter({ name, color: getRandomColor() }); - await space.locations.set({ slide: `${0}`, element: null }); + await enter({ name, color: getRandomColor() }); + await update({ slide: `${0}`, element: null }); setModalIsVisible(true); }; - enter(); + init(); }, [space, self?.profileData.name]); return (
@@ -50,7 +53,7 @@ const App = () => {
diff --git a/demo/src/components/CurrentSlide.tsx b/demo/src/components/CurrentSlide.tsx index ecda595b..7a146a91 100644 --- a/demo/src/components/CurrentSlide.tsx +++ b/demo/src/components/CurrentSlide.tsx @@ -1,7 +1,8 @@ import { useRef } from 'react'; import { Cursors } from '.'; -import { useMembers, useTrackCursor } from '../hooks'; +import { useTrackCursor } from '../hooks'; import { SlidePreviewProps } from './SlidePreview'; +import { useMembers } from '@ably/spaces/react'; interface Props { slides: Omit[]; diff --git a/demo/src/components/Cursors.tsx b/demo/src/components/Cursors.tsx index 0f726c4a..c64d2adc 100644 --- a/demo/src/components/Cursors.tsx +++ b/demo/src/components/Cursors.tsx @@ -1,65 +1,27 @@ -import { useContext, useEffect, useState } from 'react'; -import type { CursorUpdate as _CursorUpdate } from '@ably/spaces'; - +import { useCursors } from '@ably/spaces/react'; import cn from 'classnames'; -import { CursorSvg, SpacesContext } from '.'; -import { useMembers, CURSOR_ENTER, CURSOR_LEAVE, CURSOR_MOVE } from '../hooks'; - -type state = typeof CURSOR_ENTER | typeof CURSOR_LEAVE | typeof CURSOR_MOVE; -type CursorUpdate = Omit<_CursorUpdate, 'data'> & { data: { state: state } }; +import { CursorSvg } from '.'; +import { CURSOR_LEAVE } from '../hooks'; export const Cursors = () => { - const space = useContext(SpacesContext); - const { self, others } = useMembers(); - const [cursors, setCursors] = useState<{ - [connectionId: string]: { position: CursorUpdate['position']; state: CursorUpdate['data']['state'] }; - }>({}); - - useEffect(() => { - if (!space || !others) return; - - space.cursors.subscribe('update', (cursorUpdate) => { - const { connectionId, position, data } = cursorUpdate as CursorUpdate; - - if (cursorUpdate.connectionId === self?.connectionId) return; - - setCursors((currentCursors) => ({ - ...currentCursors, - [connectionId]: { position, state: data.state }, - })); + const { space, cursors } = useCursors({ returnCursors: true }); + + const activeCursors = Object.keys(cursors) + .filter((connectionId) => { + const { member, cursorUpdate } = cursors[connectionId]!!; + return ( + member.connectionId !== space.connectionId && member.isConnected && cursorUpdate.data.state !== CURSOR_LEAVE + ); + }) + .map((connectionId) => { + const { member, cursorUpdate } = cursors[connectionId]!!; + return { + connectionId: member.connectionId, + profileData: member.profileData, + position: cursorUpdate.position, + }; }); - return () => { - space.cursors.unsubscribe('update'); - }; - }, [space, others, self?.connectionId]); - - useEffect(() => { - const handler = async (member: { connectionId: string }) => { - setCursors((currentCursors) => ({ - ...currentCursors, - [member.connectionId]: { position: { x: 0, y: 0 }, state: CURSOR_LEAVE }, - })); - }; - - space?.members.subscribe('leave', handler); - - return () => { - space?.members.unsubscribe('leave', handler); - }; - }, [space]); - - const activeCursors = others - .filter( - (member) => - member.isConnected && cursors[member.connectionId] && cursors[member.connectionId].state !== CURSOR_LEAVE, - ) - .map((member) => ({ - connectionId: member.connectionId, - profileData: member.profileData, - position: cursors[member.connectionId].position, - })); - return (
{activeCursors.map((cursor) => { diff --git a/demo/src/components/Image.tsx b/demo/src/components/Image.tsx index 95d36be0..7be5868c 100644 --- a/demo/src/components/Image.tsx +++ b/demo/src/components/Image.tsx @@ -1,7 +1,8 @@ import cn from 'classnames'; -import { useClickOutside, useElementSelect, useMembers } from '../hooks'; +import { useClickOutside, useElementSelect } from '../hooks'; import { findActiveMembers, getMemberFirstName, getOutlineClasses } from '../utils'; import { useRef } from 'react'; +import { useMembers } from '@ably/spaces/react'; import { usePreview } from './PreviewContext.tsx'; interface Props extends React.HTMLAttributes { diff --git a/demo/src/components/Modal.tsx b/demo/src/components/Modal.tsx index f6a56c3f..2a369aac 100644 --- a/demo/src/components/Modal.tsx +++ b/demo/src/components/Modal.tsx @@ -1,7 +1,6 @@ -import { FormEvent, useContext, useRef } from 'react'; +import { FormEvent, useRef } from 'react'; import cn from 'classnames'; - -import { SpacesContext } from '.'; +import { useSpace } from '@ably/spaces/react'; import { Member } from '../utils/types'; interface Props { @@ -11,7 +10,7 @@ interface Props { } export const Modal = ({ isVisible = false, setIsVisible, self }: Props) => { - const space = useContext(SpacesContext); + const { space, updateProfileData } = useSpace(); const inputRef = useRef(null); const handleSubmit = (e: FormEvent) => { @@ -19,7 +18,7 @@ export const Modal = ({ isVisible = false, setIsVisible, self }: Props) => { if (!space || !setIsVisible) return; - space.updateProfileData((profileData) => ({ ...profileData, name: inputRef.current?.value })); + updateProfileData((profileData) => ({ ...profileData, name: inputRef.current?.value })); setIsVisible(false); }; diff --git a/demo/src/components/SlidePreview.tsx b/demo/src/components/SlidePreview.tsx index b1af292e..7256a11b 100644 --- a/demo/src/components/SlidePreview.tsx +++ b/demo/src/components/SlidePreview.tsx @@ -1,8 +1,7 @@ import cn from 'classnames'; import { AvatarStack, CurrentSelectorSvg } from '.'; -import { useContext } from 'react'; -import { SpacesContext } from '.'; -import { useMembers } from '../hooks'; +import { useLocations, useMembers } from '@ably/spaces/react'; +import { Member } from '../utils/types'; export interface SlidePreviewProps { children: React.ReactNode; @@ -10,14 +9,14 @@ export interface SlidePreviewProps { } export const SlidePreview = ({ children, index }: SlidePreviewProps) => { - const space = useContext(SpacesContext); - const { self, members } = useMembers(); + const { space, self, members } = useMembers(); + const { update } = useLocations(); const membersOnASlide = (members || []).filter(({ location }) => location?.slide === `${index}`); const isActive = self?.location?.slide === `${index}`; const handleSlideClick = async () => { if (!space || !self) return; - space.locations.set({ slide: `${index}`, element: null }); + update({ slide: `${index}`, element: null }); }; return ( @@ -49,7 +48,7 @@ export const SlidePreview = ({ children, index }: SlidePreviewProps) => {
); diff --git a/demo/src/components/SpacesContext.tsx b/demo/src/components/SpacesContext.tsx deleted file mode 100644 index 1ec91831..00000000 --- a/demo/src/components/SpacesContext.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react'; -import { AblyProvider } from '@ably-labs/react-hooks'; - -import Spaces, { type Space } from '@ably/spaces'; -import { Realtime } from 'ably'; -import { nanoid } from 'nanoid'; - -import { getParamValueFromUrl, generateSpaceName } from '../utils'; - -export const SpacesContext = React.createContext(undefined); - -const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [space, setSpace] = React.useState(undefined); - const spaceName = getParamValueFromUrl('space', generateSpaceName); - - const [spaces, ably] = React.useMemo(() => { - const clientId = nanoid(); - - const ably = new Realtime.Promise({ - authUrl: `/api/ably-token-request?clientId=${clientId}`, - clientId, - }); - - return [new Spaces(ably), ably]; - }, []); - - React.useEffect(() => { - let ignore = false; - - const init = async () => { - const spaceInstance = await spaces.get(getParamValueFromUrl('space', generateSpaceName), { - offlineTimeout: 10_000, - }); - - if (spaceInstance && !space && !ignore) { - setSpace(spaceInstance); - } - }; - - init(); - - return () => { - ignore = true; - }; - }, [spaceName, spaces]); - - return ( - - {children}{' '} - - ); -}; - -export { SpaceContextProvider }; diff --git a/demo/src/components/index.ts b/demo/src/components/index.ts index 28b16ad4..d5c8529b 100644 --- a/demo/src/components/index.ts +++ b/demo/src/components/index.ts @@ -10,6 +10,5 @@ export * from './Paragraph'; export * from './SlideMenu'; export * from './SlidePreview'; export * from './slides'; -export * from './SpacesContext'; export * from './svg'; export * from './Title'; diff --git a/demo/src/hooks/index.ts b/demo/src/hooks/index.ts index b2ab5dab..e4644a39 100644 --- a/demo/src/hooks/index.ts +++ b/demo/src/hooks/index.ts @@ -1,4 +1,2 @@ -export * from './useMembers'; export * from './useElementSelect'; export * from './useTrackCursor'; -export * from './useLock'; diff --git a/demo/src/hooks/useElementSelect.ts b/demo/src/hooks/useElementSelect.ts index d29adbf3..2ffc8375 100644 --- a/demo/src/hooks/useElementSelect.ts +++ b/demo/src/hooks/useElementSelect.ts @@ -1,13 +1,11 @@ -import { MutableRefObject, useContext, useEffect } from 'react'; -import { SpacesContext } from '../components'; -import { useMembers } from './useMembers'; +import { MutableRefObject, useEffect } from 'react'; import { buildLockId, releaseMyLocks } from '../utils/locking'; import { Member } from '../utils/types'; +import { useMembers, useSpace } from '@ably/spaces/react'; export const useElementSelect = (element?: string, lockable: boolean = true) => { - const space = useContext(SpacesContext); - const { self } = useMembers(); + const { space, self } = useMembers(); const handleSelect = async () => { if (!space || !self) return; @@ -32,7 +30,7 @@ export const useElementSelect = (element?: string, lockable: boolean = true) => }; export const useClickOutside = (ref: MutableRefObject, self?: Member, enabled?: boolean) => { - const space = useContext(SpacesContext); + const { space } = useSpace(); useEffect(() => { if (!enabled) return; @@ -55,7 +53,7 @@ export const useClickOutside = (ref: MutableRefObject, self? }; export const useClearOnFailedLock = (lockConflict: boolean, self?: Member) => { - const space = useContext(SpacesContext); + const { space } = useSpace(); useEffect(() => { if (lockConflict) { diff --git a/demo/src/hooks/useLock.ts b/demo/src/hooks/useLock.ts deleted file mode 100644 index 7a3d2181..00000000 --- a/demo/src/hooks/useLock.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useContext, useEffect, useState } from 'react'; - -import { type Lock, LockStatus } from '@ably/spaces'; - -import { SpacesContext } from '../components'; -import { buildLockId } from '../utils/locking'; -import { isMember } from '../hooks'; - -import { type Member } from '../utils/types'; - -export const useLock = (slide: string, id: string): { status?: string; member?: Member } => { - const space = useContext(SpacesContext); - const locationLockId = buildLockId(slide, id); - const [status, setStatus] = useState(undefined); - const [member, setMember] = useState(undefined); - - useEffect(() => { - if (!space) return; - - const handler = (lock: Lock) => { - if (lock.id !== locationLockId) return; - - setStatus(lock.status); - - if (isMember(lock.member)) { - setMember(lock.member); - } - }; - - space.locks.subscribe('update', handler); - - return () => { - space?.locks.unsubscribe('update', handler); - }; - }, [space, slide, id]); - - useEffect(() => { - if (status !== undefined) return; - const lock = space?.locks.get(locationLockId); - if (lock) { - setMember(lock.member as any); - setStatus(lock.status); - } - }, [status]); - - return { status, member }; -}; - -export const useLockStatus = (slide: string, id: string, selfConnectionId?: string) => { - const { member, status } = useLock(slide, id); - - const locked = status === 'locked'; - const lockedByYou = locked && member?.connectionId === selfConnectionId; - - return { locked, lockedByYou }; -}; diff --git a/demo/src/hooks/useMembers.ts b/demo/src/hooks/useMembers.ts deleted file mode 100644 index 47905d24..00000000 --- a/demo/src/hooks/useMembers.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect, useState, useContext } from 'react'; -import { type SpaceMember } from '@ably/spaces'; -import { SpacesContext } from '../components'; - -import { type Member } from '../utils/types'; - -export const isMember = (obj: unknown): obj is Member => { - return !!(obj as Member)?.profileData?.name && !!(obj as Member)?.profileData?.color; -}; - -const areMembers = (arr: unknown): arr is Member[] => { - return (arr as Member[]).every((m) => isMember(m)); -}; - -const membersToOthers = (members: Member[] = [], self: SpaceMember | null): Member[] => - members.filter((m) => m.connectionId !== self?.connectionId); - -export const useMembers: () => { self?: Member; others: Member[]; members: Member[] } = () => { - const space = useContext(SpacesContext); - const [members, setMembers] = useState([]); - const [others, setOthers] = useState([]); - const [self, setSelf] = useState(undefined); - - useEffect(() => { - if (!space) return; - - const handler = ({ members }: { members: SpaceMember[] }) => - (async () => { - const self = await space.members.getSelf(); - - if (isMember(self)) { - setSelf(self); - } - - if (areMembers(members)) { - setMembers([...members]); - setOthers(membersToOthers([...members], self)); - } - })(); - - const init = async () => { - const initSelf = await space.members.getSelf(); - const initMembers = await space.members.getAll(); - - if (isMember(initSelf)) { - setSelf(initSelf); - } - - if (areMembers(initMembers)) { - setMembers(initMembers); - setOthers(membersToOthers(initMembers, initSelf)); - } - space.subscribe('update', handler); - }; - - init(); - - return () => { - space.unsubscribe('update', handler); - }; - }, [space]); - - return { members, self, others }; -}; diff --git a/demo/src/hooks/useTextComponentLock.ts b/demo/src/hooks/useTextComponentLock.ts index cd837dff..c175d67a 100644 --- a/demo/src/hooks/useTextComponentLock.ts +++ b/demo/src/hooks/useTextComponentLock.ts @@ -1,13 +1,12 @@ import { MutableRefObject, useCallback } from 'react'; -import { useChannel } from '@ably-labs/react-hooks'; +import { useChannel } from 'ably/react'; +import { useMembers, useLock } from '@ably/spaces/react'; +import sanitize from 'sanitize-html'; import { findActiveMember, generateSpaceName, getParamValueFromUrl } from '../utils'; import { buildLockId } from '../utils/locking.ts'; import { usePreview } from '../components/PreviewContext.tsx'; -import { useMembers } from './useMembers.ts'; import { useClearOnFailedLock, useClickOutside, useElementSelect } from './useElementSelect.ts'; -import { useLockStatus } from './useLock.ts'; import { useSlideElementContent } from './useSlideElementContent.ts'; -import sanitize from 'sanitize-html'; interface UseTextComponentLockArgs { id: string; @@ -20,8 +19,10 @@ export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: U const spaceName = getParamValueFromUrl('space', generateSpaceName); const { members, self } = useMembers(); const activeMember = findActiveMember(id, slide, members); - const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId); const lockId = buildLockId(slide, id); + const { status, member } = useLock(lockId); + const locked = status === 'locked'; + const lockedByYou = locked && self?.connectionId === member?.connectionId; const channelName = `[?rewind=1]${spaceName}${lockId}`; const [content, updateContent] = useSlideElementContent(lockId, defaultText); const preview = usePreview(); diff --git a/demo/src/hooks/useTrackCursor.ts b/demo/src/hooks/useTrackCursor.ts index 01b277ba..56e26d57 100644 --- a/demo/src/hooks/useTrackCursor.ts +++ b/demo/src/hooks/useTrackCursor.ts @@ -1,15 +1,15 @@ -import { useContext, RefObject, useEffect } from 'react'; -import { SpacesContext } from '../components'; +import { RefObject, useEffect } from 'react'; +import { useCursors } from '@ably/spaces/react'; export const CURSOR_MOVE = 'move'; export const CURSOR_ENTER = 'enter'; export const CURSOR_LEAVE = 'leave'; export const useTrackCursor = (containerRef: RefObject, selfConnectionId?: string) => { - const space = useContext(SpacesContext); + const { set } = useCursors(); useEffect(() => { - if (!containerRef.current || !space) return; + if (!containerRef.current || !set) return; const { current: cursorContainer } = containerRef; @@ -17,7 +17,7 @@ export const useTrackCursor = (containerRef: RefObject, selfConn enter: (event: MouseEvent) => { if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); - space.cursors.set({ + set({ position: { x: event.clientX - left, y: event.clientY - top }, data: { state: CURSOR_ENTER }, }); @@ -25,7 +25,7 @@ export const useTrackCursor = (containerRef: RefObject, selfConn move: (event: MouseEvent) => { if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); - space.cursors.set({ + set({ position: { x: event.clientX - left, y: event.clientY - top }, data: { state: CURSOR_MOVE }, }); @@ -33,7 +33,7 @@ export const useTrackCursor = (containerRef: RefObject, selfConn leave: (event: MouseEvent) => { if (!selfConnectionId) return; const { top, left } = cursorContainer.getBoundingClientRect(); - space.cursors.set({ + set({ position: { x: event.clientX - left, y: event.clientY - top }, data: { state: CURSOR_LEAVE }, }); @@ -45,10 +45,9 @@ export const useTrackCursor = (containerRef: RefObject, selfConn cursorContainer.addEventListener('mouseleave', cursorHandlers.leave); return () => { - space.cursors.unsubscribe(); cursorContainer.removeEventListener('mouseenter', cursorHandlers.enter); cursorContainer.removeEventListener('mousemove', cursorHandlers.move); cursorContainer.removeEventListener('mouseleave', cursorHandlers.leave); }; - }, [space, containerRef, selfConnectionId]); + }, [set, containerRef, selfConnectionId]); }; diff --git a/demo/src/main.tsx b/demo/src/main.tsx index c264e519..743cbbff 100644 --- a/demo/src/main.tsx +++ b/demo/src/main.tsx @@ -3,17 +3,39 @@ import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; -import { SpaceContextProvider } from './components'; import { SlidesStateContextProvider } from './components/SlidesStateContext.tsx'; +import Spaces from '@ably/spaces'; +import { SpacesProvider, SpaceProvider } from '@ably/spaces/react'; +import { Realtime } from 'ably'; +import { nanoid } from 'nanoid'; +import { generateSpaceName, getParamValueFromUrl } from './utils'; +import { AblyProvider } from 'ably/react'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +const clientId = nanoid(); + +const client = new Realtime.Promise({ + authUrl: `/api/ably-token-request?clientId=${clientId}`, + clientId, +}); + +const spaces = new Spaces(client); +const spaceName = getParamValueFromUrl('space', generateSpaceName); + root.render( - - - - - + + + + + + + + + , ); diff --git a/docs/react.md b/docs/react.md new file mode 100644 index 00000000..69b320ee --- /dev/null +++ b/docs/react.md @@ -0,0 +1,211 @@ +# React Hooks + +> [!NOTE] +> For more information about React Hooks for Spaces, please see the official [Spaces documentation](https://ably.com/docs/spaces/react). + +Incorporate Spaces into your React application with idiomatic and user-friendly React Hooks. + +Using this module you can: + +- Interact with [Ably Spaces](https://ably.com/docs/spaces) using a React Hook. +- Subscribe to events in a space +- Retrieve the membership of a space +- Set the location of space members +- Acquire locks on components within a space +- Set the position of members' cursors in a space + +--- + +- [Compatible React Versions](#compatible-react-versions) +- [Usage](#usage) + + [useSpace](#usespace) + + [useMembers](#usemembers) + + [useLocation](#uselocation) + + [useLocks](#uselocks) + + [useLock](#uselock) + + [useCursors](#usecursors) + + [Error Handling](#error-handling) + +--- + +## Compatible React Versions + +The hooks are compatible with all versions of React above 16.8.0 + +## Usage + +Start by connecting your app to Ably using the `SpacesProvider` component. + +The `SpacesProvider` should wrap every component that needs to access Spaces. + +```jsx +import { Realtime } from "ably"; +import Spaces from "@ably/spaces"; +import { SpacesProvider, SpaceProvider } from "@ably/spaces/react"; + +const ably = new Realtime.Promise({ key: "your-ably-api-key", clientId: 'me' }); +const spaces = new Spaces(ably); + +root.render( + + + + + +) +``` + +Once you've done this, you can use the `hooks` in your code. The simplest example is as follows: + +```javascript +const { self, others } = useMembers(); +``` + +Our react hooks are designed to run on the client-side, so if you are using server-side rendering, make sure that your components which use Spaces react hooks are only rendered on the client side. + +--- + +### useSpace + +The `useSpace` hook lets you subscribe to the current Space and receive Space state events and get the current Space instance. + +```javascript +const { space } = useSpace((update) => { + console.log(update); +}); +``` + +### useMembers + +The `useMembers` hook is useful in building avatar stacks. By using the `useMembers` hook you can retrieve members of the space. +This includes members that have recently left the space, but have not yet been removed. + +```javascript +const { self, others, members } = useMembers(); +``` + +* `self` - a member’s own member object +* `others` - an array of member objects for all members other than the member themselves +* `members` - an array of all member objects, including the member themselves + +It also lets you subscribe to members entering, leaving, being +removed from the Space (after a timeout) or updating their profile information. + +```javascript +// Subscribe to all member events in a space +useMembers((memberUpdate) => { + console.log(memberUpdate); +}); + +// Subscribe to member enter events only +useMembers('enter', (memberJoined) => { + console.log(memberJoined); +}); + +// Subscribe to member leave events only +useMembers('leave', (memberLeft) => { + console.log(memberLeft); +}); + +// Subscribe to member remove events only +useMembers('remove', (memberRemoved) => { + console.log(memberRemoved); +}); + +// Subscribe to profile updates on members only +useMembers('updateProfile', (memberProfileUpdated) => { + console.log(memberProfileUpdated); +}); + +// Subscribe to all updates to members +useMembers('update', (memberUpdate) => { + console.log(memberUpdate); +}); +``` + +### useLocation + +The `useLocation` hook lets you subscribe to location events. +Location events are emitted whenever a member changes location. + +```javascript +useLocation((locationUpdate) => { + console.log(locationUpdate); +}); +``` + +`useLocation` also enables you to update current member location by using `update` method provided by hook. For example: + +```javascript +const { update } = useLocation((locationUpdate) => { + console.log(locationUpdate); +}); +``` + +### useLocks + +`useLocks` enables you to subscribe to lock events by registering a listener. Lock events are emitted whenever a lock transitions into the `locked` or `unlocked` state. + +```javascript +useLocks((lockUpdate) => { + console.log(lockUpdate); +}); +``` + +### useLock + +`useLock` returns the status of a lock and, if the lock has been acquired, the member holding that lock. + +```javascript +const { status, member } = useLock('my-lock'); +``` + +### useCursors + +`useCursors` enables you to track a member's cursor position and provide a view of all members' cursors within a space. For example: + +```javascript +// Subscribe to events published on "mousemove" by all members +const { set } = useCursors((cursorUpdate) => { + console.log(cursorUpdate); +}); + +useEffect(() => { + // Publish a your cursor position on "mousemove" including optional data + const eventListener = ({ clientX, clientY }) => { + set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); + } + + window.addEventListener('mousemove', eventListener); + + return () => { + window.removeEventListener('mousemove', eventListener); + }; +}); +``` + +If you provide `{ returnCursors: true }` as an option you can get active members cursors: + +```javascript +const { cursors } = useCursors((cursorUpdate) => { + console.log(cursorUpdate); +}, { returnCursors: true }); +``` + +--- + +### Error Handling + +`useSpace`, `useMembers`, `useLocks` and `useCursors` return connection and channel errors you may encounter, so that you can handle then within your components. This may include when a client doesn't have permission to attach to a channel, or if it loses its connection to Ably. + +```jsx +const { connectionError, channelError } = useMembers(); + +if (connectionError) { + // TODO: handle connection errors +} else if (channelError) { + // TODO: handle channel errors +} else { + return +} +``` diff --git a/package-lock.json b/package-lock.json index 20cae84d..d86e1d56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,10 @@ "@playwright/test": "^1.39.0", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-terser": "^0.4.3", + "@testing-library/react": "^14.0.0", "@types/express": "^4.17.18", + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", @@ -25,7 +28,10 @@ "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", "express": "^4.18.2", + "jsdom": "^22.1.0", "prettier": "^3.0.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", "rollup": "^3.28.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", @@ -33,7 +39,17 @@ "vitest": "^0.34.3" }, "peerDependencies": { - "ably": "^1.2.45" + "ably": "^1.2.45", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -67,6 +83,196 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -778,6 +984,84 @@ "node": ">=10" } }, + "node_modules/@testing-library/dom": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", + "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -802,6 +1086,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/aria-query": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.2.tgz", + "integrity": "sha512-PHKZuMN+K5qgKIWhBodXzQslTo5P+K/6LqeKXS6O/4liIDdZqaX5RXrCK++LAw+y/nptN48YmUMFiQHRSWYwtQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", @@ -928,6 +1218,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "node_modules/@types/prop-types": { + "version": "15.7.7", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.7.tgz", + "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==", + "dev": true + }, "node_modules/@types/qs": { "version": "6.9.8", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", @@ -940,6 +1236,26 @@ "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", "dev": true }, + "node_modules/@types/react": { + "version": "18.2.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.23.tgz", + "integrity": "sha512-qHLW6n1q2+7KyBEYnrZpcsAmU/iiCh9WGCKgXvMxx89+TYdJWRjZohVIo9XTcoLhfX3+/hP0Pbulu3bCZQ9PSA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", + "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -955,6 +1271,12 @@ "@types/node": "*" } }, + "node_modules/@types/scheduler": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -1357,6 +1679,12 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, "node_modules/ably": { "version": "1.2.45", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", @@ -1426,6 +1754,18 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1493,6 +1833,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -1630,6 +1979,12 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "peer": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1690,6 +2045,18 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1923,6 +2290,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2006,6 +2385,38 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2023,6 +2434,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -2062,6 +2479,35 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2102,6 +2548,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2154,6 +2609,24 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2184,6 +2657,18 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", @@ -2237,6 +2722,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -2929,6 +3434,20 @@ "node": ">=8.0.0" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3276,6 +3795,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3304,6 +3835,20 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -3317,13 +3862,26 @@ "node": ">=10.19.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -3402,6 +3960,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3528,6 +4102,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -3579,6 +4162,12 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -3595,6 +4184,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -3656,6 +4254,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -3668,6 +4275,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -3730,6 +4350,12 @@ "node": ">=8" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "devOptional": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3751,6 +4377,69 @@ "node": ">=12.0.0" } }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3842,6 +4531,18 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "devOptional": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -3878,6 +4579,15 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", @@ -4106,6 +4816,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -4115,6 +4831,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -4303,6 +5035,18 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4543,6 +5287,12 @@ "node": ">= 0.10" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -4577,6 +5327,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4642,12 +5398,55 @@ "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "devOptional": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "devOptional": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, "node_modules/regexp-tree": { "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", @@ -4695,6 +5494,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -4780,6 +5585,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4870,6 +5681,27 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "devOptional": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -5105,6 +5937,18 @@ "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", "dev": true }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5233,6 +6077,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/terser": { "version": "5.17.7", "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", @@ -5313,6 +6163,33 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -5588,6 +6465,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5606,6 +6492,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5820,6 +6716,61 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", "dev": true }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5851,6 +6802,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", @@ -5917,6 +6883,21 @@ "async-limiter": "~1.0.0" } }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6007,6 +6988,158 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/runtime": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, "@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -6413,9 +7546,67 @@ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "peer": true, "requires": { - "defer-to-connect": "^2.0.0" + "defer-to-connect": "^2.0.0" + } + }, + "@testing-library/dom": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + } + } + }, + "@testing-library/react": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", + "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" } }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -6440,6 +7631,12 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "@types/aria-query": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.2.tgz", + "integrity": "sha512-PHKZuMN+K5qgKIWhBodXzQslTo5P+K/6LqeKXS6O/4liIDdZqaX5RXrCK++LAw+y/nptN48YmUMFiQHRSWYwtQ==", + "dev": true + }, "@types/body-parser": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", @@ -6566,6 +7763,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "@types/prop-types": { + "version": "15.7.7", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.7.tgz", + "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==", + "dev": true + }, "@types/qs": { "version": "6.9.8", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", @@ -6578,6 +7781,26 @@ "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", "dev": true }, + "@types/react": { + "version": "18.2.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.23.tgz", + "integrity": "sha512-qHLW6n1q2+7KyBEYnrZpcsAmU/iiCh9WGCKgXvMxx89+TYdJWRjZohVIo9XTcoLhfX3+/hP0Pbulu3bCZQ9PSA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", + "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -6593,6 +7816,12 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==", + "dev": true + }, "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -6847,6 +8076,12 @@ "pretty-format": "^29.5.0" } }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, "ably": { "version": "1.2.45", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", @@ -6887,6 +8122,15 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -6938,6 +8182,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "requires": { + "deep-equal": "^2.0.5" + } + }, "array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -7036,6 +8289,12 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "peer": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -7083,6 +8342,15 @@ "ms": "2.0.0" } }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -7267,6 +8535,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -7335,6 +8612,32 @@ "which": "^2.0.1" } }, + "cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "requires": { + "rrweb-cssom": "^0.6.0" + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7344,6 +8647,12 @@ "ms": "2.1.2" } }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -7370,6 +8679,32 @@ "type-detect": "^4.0.0" } }, + "deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + } + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -7398,6 +8733,12 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7434,6 +8775,21 @@ "esutils": "^2.0.2" } }, + "dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "requires": { + "webidl-conversions": "^7.0.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7461,6 +8817,12 @@ "once": "^1.4.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "es-abstract": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", @@ -7508,6 +8870,23 @@ "which-typed-array": "^1.1.10" } }, + "es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + } + }, "es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -8073,6 +9452,17 @@ "signal-exit": "^3.0.2" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8311,6 +9701,15 @@ "has-symbols": "^1.0.2" } }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8336,6 +9735,17 @@ "toidentifier": "1.0.1" } }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, "http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -8346,13 +9756,23 @@ "resolve-alpn": "^1.0.0" } }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "ignore": { @@ -8410,6 +9830,16 @@ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, "is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -8494,6 +9924,12 @@ "is-extglob": "^2.1.1" } }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true + }, "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -8527,6 +9963,12 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8537,6 +9979,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true + }, "is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -8577,6 +10025,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, "is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -8586,6 +10040,16 @@ "call-bind": "^1.0.2" } }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -8636,6 +10100,12 @@ "istanbul-lib-report": "^3.0.0" } }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "devOptional": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -8651,6 +10121,46 @@ "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, + "jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "requires": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "dependencies": { + "ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "requires": {} + } + } + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -8724,6 +10234,15 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "devOptional": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -8754,6 +10273,12 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true + }, "magic-string": { "version": "0.30.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", @@ -8915,12 +10440,28 @@ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "peer": true }, + "nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "dev": true }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -9054,6 +10595,15 @@ "callsites": "^3.0.0" } }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -9207,6 +10757,12 @@ "ipaddr.js": "1.9.1" } }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -9232,6 +10788,12 @@ "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9269,6 +10831,36 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "devOptional": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "devOptional": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" } }, "react-is": { @@ -9277,6 +10869,12 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, "regexp-tree": { "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", @@ -9306,6 +10904,12 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -9362,6 +10966,12 @@ "fsevents": "~2.3.2" } }, + "rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9415,6 +11025,24 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "devOptional": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -9621,6 +11249,15 @@ "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", "dev": true }, + "stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "requires": { + "internal-slot": "^1.0.4" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -9710,6 +11347,12 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "terser": { "version": "5.17.7", "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", @@ -9772,6 +11415,27 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "requires": { + "punycode": "^2.3.0" + } + }, "ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -9962,6 +11626,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -9977,6 +11647,16 @@ "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -10084,6 +11764,46 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", "dev": true }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true + }, + "whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10106,6 +11826,18 @@ "is-symbol": "^1.0.3" } }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, "which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", @@ -10154,6 +11886,18 @@ "async-limiter": "~1.0.0" } }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index eb8e1430..66381f88 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,6 @@ "examples:locks": "ts-node ./examples/locks.ts", "docs": "typedoc" }, - "exports": { - "import": "./dist/mjs/index.js", - "require": "./dist/cjs/index.js" - }, "repository": { "type": "git", "url": "git+https://github.com/ably/spaces.git" @@ -45,7 +41,10 @@ "@playwright/test": "^1.39.0", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-terser": "^0.4.3", + "@testing-library/react": "^14.0.0", "@types/express": "^4.17.18", + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", @@ -55,7 +54,10 @@ "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", "express": "^4.18.2", + "jsdom": "^22.1.0", "prettier": "^3.0.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", "rollup": "^3.28.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", @@ -66,6 +68,16 @@ "nanoid": "^5.0.2" }, "peerDependencies": { - "ably": "^1.2.45" + "ably": "^1.2.45", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } } diff --git a/react/package.json b/react/package.json new file mode 100644 index 00000000..fb51e68a --- /dev/null +++ b/react/package.json @@ -0,0 +1,8 @@ +{ + "name": "@ably/spaces/react", + "type": "module", + "main": "../dist/cjs/react/index.js", + "module": "../dist/mjs/react/index.js", + "types": "../dist/mjs/react/index.d.ts", + "sideEffects": false +} diff --git a/src/react/contexts/SpaceContext.tsx b/src/react/contexts/SpaceContext.tsx new file mode 100644 index 00000000..1277b003 --- /dev/null +++ b/src/react/contexts/SpaceContext.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useSpaces } from '../useSpaces.js'; + +import type { Space, SpaceOptions } from '../../'; + +export const SpaceContext = React.createContext(undefined); + +interface SpaceProviderProps { + name: string; + options?: Partial; + children?: React.ReactNode | React.ReactNode[] | null; +} +export const SpaceProvider: React.FC = ({ name, options, children }) => { + const [space, setSpace] = React.useState(undefined); + const spaces = useSpaces(); + const optionsRef = React.useRef(options); + + React.useEffect(() => { + optionsRef.current = options; + }, [options]); + + React.useEffect(() => { + let ignore: boolean = false; + + const init = async () => { + if (!spaces) { + throw new Error( + 'Could not find spaces client in context. ' + + 'Make sure your spaces hooks are called inside an ', + ); + } + + const spaceInstance = await spaces.get(name, optionsRef.current); + + if (spaceInstance && !space && !ignore) { + setSpace(spaceInstance); + } + }; + + init(); + + return () => { + ignore = true; + }; + }, [name, spaces]); + + return {children}; +}; diff --git a/src/react/contexts/SpacesContext.tsx b/src/react/contexts/SpacesContext.tsx new file mode 100644 index 00000000..3f685932 --- /dev/null +++ b/src/react/contexts/SpacesContext.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import type Spaces from '../../'; + +export const SpacesContext = React.createContext(undefined); + +interface SpacesProviderProps { + client: Spaces; + children?: React.ReactNode | React.ReactNode[] | null; +} +export const SpacesProvider: React.FC = ({ client: spaces, children }) => { + return {children}; +}; diff --git a/src/react/index.ts b/src/react/index.ts new file mode 100644 index 00000000..d7768f26 --- /dev/null +++ b/src/react/index.ts @@ -0,0 +1,9 @@ +export { SpacesProvider } from './contexts/SpacesContext.js'; +export { SpaceProvider } from './contexts/SpaceContext.js'; +export { useSpaces } from './useSpaces.js'; +export { useSpace } from './useSpace.js'; +export { useMembers } from './useMembers.js'; +export { useLocations } from './useLocations.js'; +export { useLocks } from './useLocks.js'; +export { useLock } from './useLock.js'; +export { useCursors } from './useCursors.js'; diff --git a/src/react/types.ts b/src/react/types.ts new file mode 100644 index 00000000..90a16e00 --- /dev/null +++ b/src/react/types.ts @@ -0,0 +1,12 @@ +import type { SpaceMember } from '..'; + +export interface UseSpaceOptions { + /** + * Skip parameter makes the hook skip execution - + * this is useful in order to conditionally register a subscription to + * an EventListener (needed because it's not possible to conditionally call a hook in react) + */ + skip?: boolean; +} + +export type UseSpaceCallback = (params: { members: SpaceMember[] }) => void; diff --git a/src/react/useChannelState.ts b/src/react/useChannelState.ts new file mode 100644 index 00000000..a3a39b12 --- /dev/null +++ b/src/react/useChannelState.ts @@ -0,0 +1,38 @@ +import { useState } from 'react'; +import { useEventListener } from './useEventListener.js'; + +import type { Types } from 'ably'; + +type ChannelStateListener = (stateChange: Types.ChannelStateChange) => void; + +const failedStateEvents: Types.ChannelState[] = ['suspended', 'failed', 'detached']; +const successStateEvents: Types.ChannelState[] = ['attached']; + +/** + * todo use `ably/react` hooks instead + */ +export const useChannelState = ( + emitter?: Types.EventEmitter, +) => { + const [channelError, setChannelError] = useState(null); + + useEventListener( + emitter, + (stateChange) => { + if (stateChange.reason) { + setChannelError(stateChange.reason); + } + }, + failedStateEvents, + ); + + useEventListener( + emitter, + () => { + setChannelError(null); + }, + successStateEvents, + ); + + return channelError; +}; diff --git a/src/react/useConnectionState.ts b/src/react/useConnectionState.ts new file mode 100644 index 00000000..ff8271f6 --- /dev/null +++ b/src/react/useConnectionState.ts @@ -0,0 +1,35 @@ +import { useState } from 'react'; +import { useEventListener } from './useEventListener.js'; + +import type { Types } from 'ably'; + +type ConnectionStateListener = (stateChange: Types.ConnectionStateChange) => void; + +const failedStateEvents: Types.ConnectionState[] = ['suspended', 'failed', 'disconnected']; +const successStateEvents: Types.ConnectionState[] = ['connected', 'closed']; + +export const useConnectionState = ( + emitter?: Types.EventEmitter, +) => { + const [connectionError, setConnectionError] = useState(null); + + useEventListener( + emitter, + (stateChange) => { + if (stateChange.reason) { + setConnectionError(stateChange.reason); + } + }, + failedStateEvents, + ); + + useEventListener( + emitter, + () => { + setConnectionError(null); + }, + successStateEvents, + ); + + return connectionError; +}; diff --git a/src/react/useCursors.test.tsx b/src/react/useCursors.test.tsx new file mode 100644 index 00000000..b7020e33 --- /dev/null +++ b/src/react/useCursors.test.tsx @@ -0,0 +1,125 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import Spaces from '../index.js'; +import Space from '../Space.js'; +import { createPresenceEvent } from '../utilities/test/fakes.js'; +import { useCursors } from './useCursors.js'; +import type { Types } from 'ably'; + +interface SpaceTestContext { + spaces: Spaces; + space: Space; + presenceMap: Map; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useCursors', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + context.presenceMap = new Map(); + + const space = new Space('test', client); + const presence = space.channel.presence; + + context.space = space; + + vi.spyOn(context.spaces, 'get').mockImplementation(async () => space); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(context.presenceMap.values()); + }); + }); + + it('invokes callback on cursor set', async ({ space, spaces, presenceMap }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useCursors(callbackSpy), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + + const dispensing = space.cursors.cursorDispensing; + + const fakeMessage = { + connectionId: '1', + clientId: '1', + encoding: 'encoding', + extras: null, + id: '1', + name: 'fake', + timestamp: 1, + data: [{ cursor: { position: { x: 1, y: 1 } } }], + }; + + dispensing.processBatch(fakeMessage); + + await waitFor(() => { + expect(callbackSpy).toHaveBeenCalledWith({ + position: { x: 1, y: 1 }, + data: undefined, + clientId: '1', + connectionId: '1', + }); + }); + }); + + it('returns cursors', async ({ space, spaces, presenceMap }) => { + // @ts-ignore + const { result } = renderHook(() => useCursors({ returnCursors: true }), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + await createPresenceEvent(space, presenceMap, 'enter', { clientId: '2', connectionId: '2' }); + const [member] = await space.members.getOthers(); + + const dispensing = space.cursors.cursorDispensing; + + const fakeMessage = { + connectionId: '2', + clientId: '2', + encoding: 'encoding', + extras: null, + id: '1', + name: 'fake', + timestamp: 1, + data: [{ cursor: { position: { x: 1, y: 1 } } }], + }; + + dispensing.processBatch(fakeMessage); + + await waitFor(() => { + expect(result.current.cursors).toEqual({ + '2': { member, cursorUpdate: { clientId: '2', connectionId: '2', position: { x: 1, y: 1 } } }, + }); + }); + }); +}); diff --git a/src/react/useCursors.ts b/src/react/useCursors.ts new file mode 100644 index 00000000..72fd9b28 --- /dev/null +++ b/src/react/useCursors.ts @@ -0,0 +1,102 @@ +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { SpaceContext } from './contexts/SpaceContext.js'; +import { useMembers } from './useMembers.js'; +import { useChannelState } from './useChannelState.js'; +import { useConnectionState } from './useConnectionState.js'; +import { isFunction } from '../utilities/is.js'; + +import type { CursorUpdate, SpaceMember } from '../types.js'; +import type { ErrorInfo } from 'ably'; +import type Cursors from '../Cursors.js'; +import type { UseSpaceOptions } from './types.js'; +import type { Space } from '..'; + +interface UseCursorsOptions extends UseSpaceOptions { + /** + * Whether to return the cursors object described in UseCursorsResult, defaults to false + */ + returnCursors?: boolean; +} + +interface UseCursorsResult { + space?: Space; + connectionError: ErrorInfo | null; + channelError: ErrorInfo | null; + set?: Cursors['set']; + /** + * if UseCursorsOptions.returnCursors is truthy; a map from connectionId to associated space member and their cursor update + */ + cursors: Record; +} + +type UseCursorsCallback = (params: CursorUpdate) => void; + +/** + * Registers a subscription on the `Space.cursors` object + */ +function useCursors(options?: UseCursorsOptions): UseCursorsResult; +function useCursors(callback: UseCursorsCallback, options?: UseCursorsOptions): UseCursorsResult; +function useCursors( + callbackOrOptions?: UseCursorsCallback | UseCursorsOptions, + optionsOrNothing?: UseCursorsOptions, +): UseCursorsResult { + const space = useContext(SpaceContext); + const [cursors, setCursors] = useState>({}); + const { members } = useMembers(); + const channelError = useChannelState(space?.cursors.channel); + const connectionError = useConnectionState(); + + const connectionIdToMember: Record = useMemo(() => { + return members.reduce( + (acc, member) => { + acc[member.connectionId] = member; + return acc; + }, + {} as Record, + ); + }, [members]); + + const callback = isFunction(callbackOrOptions) ? callbackOrOptions : undefined; + const options = isFunction(callbackOrOptions) ? optionsOrNothing : callbackOrOptions; + + const callbackRef = useRef(callback); + const optionsRef = useRef(options); + + useEffect(() => { + callbackRef.current = callback; + optionsRef.current = options; + }, [callback, options]); + + useEffect(() => { + if (!space || !connectionIdToMember) return; + + const listener: UseCursorsCallback = (cursorUpdate) => { + if (!optionsRef.current?.skip) callbackRef.current?.(cursorUpdate); + + const { connectionId } = cursorUpdate; + + if (connectionId === space?.connectionId || !optionsRef.current?.returnCursors) return; + + setCursors((currentCursors) => ({ + ...currentCursors, + [connectionId]: { member: connectionIdToMember[connectionId], cursorUpdate }, + })); + }; + + space.cursors.subscribe('update', listener); + + return () => { + space.cursors.unsubscribe('update', listener); + }; + }, [space, connectionIdToMember]); + + return { + space, + connectionError, + channelError, + set: space?.cursors.set.bind(space?.cursors), + cursors, + }; +} + +export { useCursors }; diff --git a/src/react/useEventListener.ts b/src/react/useEventListener.ts new file mode 100644 index 00000000..4b69665b --- /dev/null +++ b/src/react/useEventListener.ts @@ -0,0 +1,43 @@ +import { useEffect, useRef } from 'react'; + +import type { Types } from 'ably'; + +type EventListener = (stateChange: T) => void; + +/** + * todo use `ably/react` hooks instead + */ +export const useEventListener = < + S extends Types.ConnectionState | Types.ChannelState, + C extends Types.ConnectionStateChange | Types.ChannelStateChange, +>( + emitter?: Types.EventEmitter, C, S>, + listener?: EventListener, + event?: S | S[], +) => { + const listenerRef = useRef(listener); + + useEffect(() => { + listenerRef.current = listener; + }, [listener]); + + useEffect(() => { + const callback: EventListener = (stateChange) => { + listenerRef.current?.(stateChange); + }; + + if (event) { + emitter?.on(event as S, callback); + } else { + emitter?.on(callback); + } + + return () => { + if (event) { + emitter?.off(event as S, callback); + } else { + emitter?.off(callback); + } + }; + }, [emitter, event]); +}; diff --git a/src/react/useLocations.test.tsx b/src/react/useLocations.test.tsx new file mode 100644 index 00000000..a569379c --- /dev/null +++ b/src/react/useLocations.test.tsx @@ -0,0 +1,81 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import type { Types } from 'ably'; +import Spaces from '../index.js'; +import { createLocationUpdate, createPresenceEvent } from '../utilities/test/fakes.js'; +import Space from '../Space.js'; +import { useLocations } from './useLocations.js'; + +interface SpaceTestContext { + spaces: Spaces; + space: Space; + presenceMap: Map; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useLocations', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + context.presenceMap = new Map(); + + const space = new Space('test', client); + const presence = space.channel.presence; + + context.space = space; + + vi.spyOn(context.spaces, 'get').mockImplementation(async () => space); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(context.presenceMap.values()); + }); + }); + + it('invokes callback with new location', async ({ space, spaces, presenceMap }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useLocations(callbackSpy), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + const member = await space.members.getSelf(); + await createPresenceEvent(space, presenceMap, 'update', { + data: createLocationUpdate({ current: 'location1' }), + }); + + await waitFor(() => { + expect(callbackSpy).toHaveBeenCalledWith( + expect.objectContaining({ + member: { + ...member, + lastEvent: { + name: 'update', + timestamp: 1, + }, + location: 'location1', + }, + previousLocation: null, + }), + ); + }); + }); +}); diff --git a/src/react/useLocations.ts b/src/react/useLocations.ts new file mode 100644 index 00000000..bbce2c5c --- /dev/null +++ b/src/react/useLocations.ts @@ -0,0 +1,77 @@ +import { useContext, useEffect, useRef } from 'react'; +import { SpaceContext } from './contexts/SpaceContext.js'; +import { isArray, isFunction, isString } from '../utilities/is.js'; + +import type Locations from '../Locations.js'; +import type { SpaceMember } from '../types.js'; +import type { UseSpaceOptions } from './types.js'; +import type { Space } from '../'; + +interface UseLocationsResult { + space?: Space; + update?: Locations['set']; +} + +type UseLocationCallback = (locationUpdate: { member: SpaceMember }) => void; + +export type LocationsEvent = 'update'; + +function useLocations(callback?: UseLocationCallback, options?: UseSpaceOptions): UseLocationsResult; +function useLocations( + event: LocationsEvent | LocationsEvent[], + callback: UseLocationCallback, + options?: UseSpaceOptions, +): UseLocationsResult; + +/* + * Registers a subscription on the `Space.locations` object + */ +function useLocations( + eventOrCallback?: LocationsEvent | LocationsEvent[] | UseLocationCallback, + callbackOrOptions?: UseLocationCallback | UseSpaceOptions, + optionsOrNothing?: UseSpaceOptions, +): UseLocationsResult { + const space = useContext(SpaceContext); + const locations = space?.locations; + + const callback = + isString(eventOrCallback) || isArray(eventOrCallback) + ? (callbackOrOptions as UseLocationCallback) + : eventOrCallback; + + const options = isFunction(callbackOrOptions) ? optionsOrNothing : callbackOrOptions; + + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (callbackRef.current && locations && !options?.skip) { + const listener: UseLocationCallback = (params) => { + callbackRef.current?.(params); + }; + if (!isFunction(eventOrCallback) && eventOrCallback) { + locations.subscribe(eventOrCallback, listener); + } else { + locations.subscribe(listener); + } + + return () => { + if (!isFunction(eventOrCallback) && eventOrCallback) { + locations.unsubscribe(eventOrCallback, listener); + } else { + locations.unsubscribe(listener); + } + }; + } + }, [locations, options?.skip]); + + return { + space, + update: locations?.set.bind(locations), + }; +} + +export { useLocations }; diff --git a/src/react/useLock.ts b/src/react/useLock.ts new file mode 100644 index 00000000..0701f1d5 --- /dev/null +++ b/src/react/useLock.ts @@ -0,0 +1,55 @@ +import { useContext, useEffect, useState } from 'react'; +import { SpaceContext } from './contexts/SpaceContext.js'; + +import type { LockStatus, SpaceMember, Lock } from '../types.js'; + +interface UseLockResult { + status: LockStatus | null; + member: SpaceMember | null; +} + +/* + * Returns the status of a lock and, if it has been acquired, the member holding the lock + */ +export function useLock(lockId: string): UseLockResult { + const space = useContext(SpaceContext); + const [status, setStatus] = useState(null); + const [member, setMember] = useState(null); + + const initialized = status !== null; + + useEffect(() => { + if (!space) return; + + const handler = (lock: Lock) => { + if (lock.id !== lockId) return; + + if (lock.status === 'unlocked') { + setStatus(null); + setMember(null); + } else { + setStatus(lock.status); + setMember(lock.member); + } + }; + + space.locks.subscribe('update', handler); + + return () => { + space?.locks.unsubscribe('update', handler); + }; + }, [space, lockId]); + + useEffect(() => { + if (initialized || !space) return; + + const lock = space?.locks.get(lockId); + + if (lock) { + setMember(lock.member); + setStatus(lock.status); + } + }, [initialized, space]); + + return { status, member }; +} diff --git a/src/react/useLocks.test.tsx b/src/react/useLocks.test.tsx new file mode 100644 index 00000000..8ad080d7 --- /dev/null +++ b/src/react/useLocks.test.tsx @@ -0,0 +1,85 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import type { Types } from 'ably'; +import Spaces from '../index.js'; +import { createPresenceEvent } from '../utilities/test/fakes.js'; +import Space from '../Space.js'; +import { useLocks } from './useLocks.js'; + +interface SpaceTestContext { + spaces: Spaces; + space: Space; + presenceMap: Map; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useLocks', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + context.presenceMap = new Map(); + + const space = new Space('test', client); + const presence = space.channel.presence; + + context.space = space; + + vi.spyOn(context.spaces, 'get').mockImplementation(async () => space); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(context.presenceMap.values()); + }); + }); + + it('invokes callback on lock', async ({ space, spaces, presenceMap }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useLocks(callbackSpy), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + const member = await space.members.getSelf(); + + const msg = Realtime.PresenceMessage.fromValues({ + action: 'update', + connectionId: member.connectionId, + extras: { + locks: [ + { + id: 'lockID', + status: 'pending', + timestamp: Date.now(), + }, + ], + }, + }); + await space.locks.processPresenceMessage(msg); + + expect(callbackSpy).toHaveBeenCalledWith( + expect.objectContaining({ + member: member, + id: 'lockID', + status: 'locked', + }), + ); + }); +}); diff --git a/src/react/useLocks.ts b/src/react/useLocks.ts new file mode 100644 index 00000000..88da80ad --- /dev/null +++ b/src/react/useLocks.ts @@ -0,0 +1,65 @@ +import { useEffect, useRef } from 'react'; +import { isArray, isFunction, isString } from '../utilities/is.js'; +import { useSpace, type UseSpaceResult } from './useSpace.js'; + +import type { UseSpaceOptions } from './types.js'; +import type { LocksEventMap } from '../Locks.js'; +import type { Lock } from '../types.js'; + +type LocksEvent = keyof LocksEventMap; + +type UseLocksCallback = (params: Lock) => void; + +/* + * Registers a subscription on the `Space.locks` object + */ +function useLocks(callback?: UseLocksCallback, options?: UseSpaceOptions): UseSpaceResult; +function useLocks( + event: LocksEvent | LocksEvent[], + callback: UseLocksCallback, + options?: UseSpaceOptions, +): UseSpaceResult; +function useLocks( + eventOrCallback?: LocksEvent | LocksEvent[] | UseLocksCallback, + callbackOrOptions?: UseLocksCallback | UseSpaceOptions, + optionsOrNothing?: UseSpaceOptions, +): UseSpaceResult { + const spaceContext = useSpace(); + const { space } = spaceContext; + + const callback = + isString(eventOrCallback) || isArray(eventOrCallback) ? (callbackOrOptions as UseLocksCallback) : eventOrCallback; + + const options = isFunction(callbackOrOptions) ? optionsOrNothing : callbackOrOptions; + + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (callbackRef.current && space?.locks && !options?.skip) { + const listener: UseLocksCallback = (params) => { + callbackRef.current?.(params); + }; + if (!isFunction(eventOrCallback) && eventOrCallback) { + space?.locks.subscribe(eventOrCallback, listener); + } else { + space?.locks.subscribe(listener); + } + + return () => { + if (!isFunction(eventOrCallback) && eventOrCallback) { + space?.locks.unsubscribe(eventOrCallback, listener); + } else { + space?.locks.unsubscribe(listener); + } + }; + } + }, [space?.locks, options?.skip]); + + return spaceContext; +} + +export { useLocks }; diff --git a/src/react/useMembers.test.tsx b/src/react/useMembers.test.tsx new file mode 100644 index 00000000..00653f4e --- /dev/null +++ b/src/react/useMembers.test.tsx @@ -0,0 +1,116 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import type { Types } from 'ably'; +import Spaces from '../index.js'; +import { useMembers } from './useMembers.js'; +import { createLocationUpdate, createPresenceEvent } from '../utilities/test/fakes.js'; +import Space from '../Space.js'; + +interface SpaceTestContext { + spaces: Spaces; + space: Space; + presenceMap: Map; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useMembers', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + context.presenceMap = new Map(); + + const space = new Space('test', client); + const presence = space.channel.presence; + + context.space = space; + + vi.spyOn(context.spaces, 'get').mockImplementation(async () => space); + + vi.spyOn(presence, 'get').mockImplementation(async () => { + return Array.from(context.presenceMap.values()); + }); + }); + + it('invokes callback with enter and update on enter presence events', async ({ + space, + spaces, + presenceMap, + }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useMembers(['enter', 'leave'], callbackSpy), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + const member = await space.members.getSelf(); + + await waitFor(() => { + expect(callbackSpy).toHaveBeenCalledTimes(1); + expect(result.current.members).toEqual([member]); + expect(result.current.others).toEqual([]); + expect(result.current.self).toEqual(member); + expect(result.current.self.location).toBe(null); + }); + + await createPresenceEvent(space, presenceMap, 'update', { + data: createLocationUpdate({ current: 'location1' }), + }); + + await waitFor(() => { + expect(result.current.self.location).toBe('location1'); + // callback hasn't been invoked + expect(callbackSpy).toHaveBeenCalledTimes(1); + }); + + await createPresenceEvent(space, presenceMap, 'leave'); + await waitFor(() => { + // callback has invoked + expect(callbackSpy).toHaveBeenCalledTimes(2); + }); + }); + + it('skips callback if skip option provided', async ({ space, spaces, presenceMap }) => { + const callbackSpy = vi.fn(); + // @ts-ignore + const { result } = renderHook(() => useMembers(callbackSpy, { skip: true }), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + + await createPresenceEvent(space, presenceMap, 'enter'); + const member = await space.members.getSelf(); + + await waitFor(() => { + expect(result.current.members).toEqual([member]); + expect(result.current.others).toEqual([]); + expect(result.current.self).toEqual(member); + expect(callbackSpy).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/src/react/useMembers.ts b/src/react/useMembers.ts new file mode 100644 index 00000000..43da8b34 --- /dev/null +++ b/src/react/useMembers.ts @@ -0,0 +1,122 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSpace } from './useSpace.js'; +import { isArray, isFunction, isString } from '../utilities/is.js'; + +import type { ErrorInfo } from 'ably'; +import type { Space, SpaceMember } from '..'; +import type { UseSpaceOptions } from './types.js'; +import type { MembersEventMap } from '../Members.js'; + +interface UseMembersResult { + space?: Space; + /** + * All members present in the space + */ + members: SpaceMember[]; + /** + * All members present in the space excluding the member associated with the spaces client + */ + others: SpaceMember[]; + /** + * The member associated with the spaces client + */ + self: SpaceMember | null; + channelError: ErrorInfo | null; + connectionError: ErrorInfo | null; +} + +type UseMembersCallback = (params: SpaceMember) => void; + +type MembersEvent = keyof MembersEventMap; + +function useMembers(callback?: UseMembersCallback, options?: UseSpaceOptions): UseMembersResult; +function useMembers( + event: MembersEvent | MembersEvent[], + callback: UseMembersCallback, + options?: UseSpaceOptions, +): UseMembersResult; + +function useMembers( + eventOrCallback?: MembersEvent | MembersEvent[] | UseMembersCallback, + callbackOrOptions?: UseMembersCallback | UseSpaceOptions, + optionsOrNothing?: UseSpaceOptions, +): UseMembersResult { + const { space, connectionError, channelError } = useSpace(); + const [members, setMembers] = useState([]); + const [others, setOthers] = useState([]); + const [self, setSelf] = useState(null); + + const callback = + isString(eventOrCallback) || isArray(eventOrCallback) ? (callbackOrOptions as UseMembersCallback) : eventOrCallback; + + const options = isFunction(callbackOrOptions) ? optionsOrNothing : callbackOrOptions; + + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (callbackRef.current && space?.members && !options?.skip) { + const listener: UseMembersCallback = (params) => { + callbackRef.current?.(params); + }; + if (!isFunction(eventOrCallback) && eventOrCallback) { + space?.members.subscribe(eventOrCallback, listener); + } else { + space?.members.subscribe(listener); + } + + return () => { + if (!isFunction(eventOrCallback) && eventOrCallback) { + space?.members.unsubscribe(eventOrCallback, listener); + } else { + space?.members.unsubscribe(listener); + } + }; + } + }, [space?.members, options?.skip]); + + useEffect(() => { + if (!space) return; + let ignore: boolean = false; + + const updateState = (updatedSelf: SpaceMember | null, updatedMembers: SpaceMember[]) => { + if (ignore) return; + setSelf(updatedSelf); + setMembers([...updatedMembers]); + setOthers(updatedMembers.filter((member) => member.connectionId !== updatedSelf?.connectionId)); + }; + + const handler = async ({ members: updatedMembers }: { members: SpaceMember[] }) => { + const updatedSelf = await space.members.getSelf(); + updateState(updatedSelf, updatedMembers); + }; + + const init = async () => { + const initSelf = await space.members.getSelf(); + const initMembers = await space.members.getAll(); + updateState(initSelf, initMembers); + space.subscribe('update', handler); + }; + + init(); + + return () => { + ignore = true; + space.unsubscribe('update', handler); + }; + }, [space]); + + return { + space, + members, + others, + self, + connectionError, + channelError, + }; +} + +export { useMembers }; diff --git a/src/react/useSpace.test.tsx b/src/react/useSpace.test.tsx new file mode 100644 index 00000000..af6baf93 --- /dev/null +++ b/src/react/useSpace.test.tsx @@ -0,0 +1,48 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { Realtime } from 'ably/promises'; +import { it, beforeEach, describe, expect, vi } from 'vitest'; +import { waitFor, renderHook } from '@testing-library/react'; +import { SpacesProvider } from './contexts/SpacesContext.js'; +import { SpaceProvider } from './contexts/SpaceContext.js'; +import Spaces from '../index.js'; +import { useSpace } from './useSpace.js'; + +interface SpaceTestContext { + spaces: Spaces; +} + +vi.mock('ably/promises'); +vi.mock('nanoid'); + +describe('useSpace', () => { + beforeEach((context) => { + const client = new Realtime({ key: 'spaces-test' }); + context.spaces = new Spaces(client); + }); + + it('creates and retrieves space successfully', async ({ spaces }) => { + const spy = vi.spyOn(spaces, 'get'); + + // @ts-ignore + const { result } = renderHook(() => useSpace(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('spaces-test', undefined); + + const space = await spaces.get('spaces-test'); + + await waitFor(() => { + expect(result.current.space).toBe(space); + }); + }); +}); diff --git a/src/react/useSpace.ts b/src/react/useSpace.ts new file mode 100644 index 00000000..edc34135 --- /dev/null +++ b/src/react/useSpace.ts @@ -0,0 +1,48 @@ +import { useContext, useEffect, useRef } from 'react'; +import { SpaceContext } from './contexts/SpaceContext.js'; +import { useChannelState } from './useChannelState.js'; +import { useConnectionState } from './useConnectionState.js'; + +import type { ErrorInfo } from 'ably'; +import type { Space } from '..'; +import type { UseSpaceCallback, UseSpaceOptions } from './types.js'; + +export interface UseSpaceResult { + space?: Space; + enter?: Space['enter']; + leave?: Space['leave']; + updateProfileData?: Space['updateProfileData']; + connectionError: ErrorInfo | null; + channelError: ErrorInfo | null; +} + +export const useSpace = (callback?: UseSpaceCallback, options?: UseSpaceOptions): UseSpaceResult => { + const space = useContext(SpaceContext); + const callbackRef = useRef(callback); + + const channelError = useChannelState(space?.channel); + const connectionError = useConnectionState(); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (callbackRef.current && space && !options?.skip) { + const listener: UseSpaceCallback = (params) => { + callbackRef.current?.(params); + }; + space.subscribe('update', listener); + return () => space.unsubscribe('update', listener); + } + }, [space, options?.skip]); + + return { + space, + enter: space?.enter.bind(space), + leave: space?.leave.bind(space), + updateProfileData: space?.updateProfileData.bind(space), + connectionError, + channelError, + }; +}; diff --git a/src/react/useSpaces.ts b/src/react/useSpaces.ts new file mode 100644 index 00000000..8a65b915 --- /dev/null +++ b/src/react/useSpaces.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { SpacesContext } from './contexts/SpacesContext.js'; + +export const useSpaces = () => { + return useContext(SpacesContext); +}; diff --git a/tsconfig.base.json b/tsconfig.base.json index 13c51ac7..ed980f40 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -2,6 +2,7 @@ "include": ["./src/**/*.ts"], "exclude": ["./src/**/*.test.tsx", "./src/**/*.test.ts", "./src/fakes/**/*.ts"], "compilerOptions": { + "jsx": "react", "target": "es6", "rootDir": "./src", "sourceMap": true, @@ -13,6 +14,7 @@ "moduleResolution": "node", "skipLibCheck": true, "allowJs": true, + "allowSyntheticDefaultImports": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], "types": [] } diff --git a/tsconfig.iife.json b/tsconfig.iife.json index 05994307..f18f24f8 100644 --- a/tsconfig.iife.json +++ b/tsconfig.iife.json @@ -13,5 +13,7 @@ "outDir": "dist/iife" }, "include": ["src/**/*"], - "exclude": ["dist", "node_modules", "src/utilities/test", "./src/**/*.test.ts", "./src/fakes/**/*.ts"] + "exclude": ["dist", "node_modules", "src/utilities/test", "./src/**/*.test.ts", "./src/fakes/**/*.ts", + "src/react" + ] } From ab6179a5ef15fd189961d4593d5c56e94de1bc6a Mon Sep 17 00:00:00 2001 From: Evgeny Khokhlov Date: Wed, 25 Oct 2023 14:14:53 +0100 Subject: [PATCH 142/191] Bump to 0.2.0 (#234) --- CHANGELOG.md | 22 +++++++++++++++++++++- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5257ece..dfb6beae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # CHANGELOG +## v0.2.0 + +In this release, we introduce React hooks for Spaces [\#233](https://github.com/ably/spaces/pull/233). See the [React Hooks documentation](/docs/react.md) for further details. + +Breaking changes in this release: + +* \[MMB-317\], \[MMB-318\] — Remove the `LockAttributes` type [\#214](https://github.com/ably/spaces/pull/214) ([lawrence-forooghian](https://github.com/lawrence-forooghian)) +* Remove ability to pass array of event names to `EventEmitter.prototype.once` [\#196](https://github.com/ably/spaces/pull/196) ([lawrence-forooghian](https://github.com/lawrence-forooghian)) + +Other notable changes: + +* \[COL-335\] Fix bug where `space.enter()` sometimes hangs. [\#227](https://github.com/ably/spaces/pull/227) ([lawrence-forooghian](https://github.com/lawrence-forooghian)) +* Add agent param [\#220](https://github.com/ably/spaces/pull/220) ([dpiatek](https://github.com/dpiatek)) +* Add script to test CDN bundle [\#216](https://github.com/ably/spaces/pull/216) ([lawrence-forooghian](https://github.com/lawrence-forooghian)) +* Publish to new CDN bucket only [\#205](https://github.com/ably/spaces/pull/205) ([surminus](https://github.com/surminus)) +* \[MMB-156\] Add documentation comments and generate HTML documentation [\#204](https://github.com/ably/spaces/pull/204) ([lawrence-forooghian](https://github.com/lawrence-forooghian)) +* Demo updates [\#195](https://github.com/ably/spaces/pull/195) ([dpiatek](https://github.com/dpiatek)) + +**Full Changelog**: https://github.com/ably/spaces/compare/0.1.3...0.2.0 + ## v0.1.3 Breaking changes in this release: @@ -32,7 +52,7 @@ No breaking changes were introduced in this release. ## v0.1.0 -In this release, we're advancing Spaces from alpha to beta. Along with introducing this library to a wider audience, we've decided to move it to the `ably` organisation as Spaces is no longer an experiment, it's something we see as an excellent supplement to our core SDKs to help developers build collaborative environments in their apps. We are committed to grow and officially maintain it. +In this release, we're advancing Spaces from alpha to beta. Along with introducing this library to a wider audience, we've decided to move it to the `ably` organisation as Spaces is no longer an experiment, it's something we see as an excellent supplement to our core SDKs to help developers build collaborative environments in their apps. We are committed to grow and officially maintain it. If you are one of our early adopters, this means you need to update your `package.json` from `@ably-labs/spaces` to `@ably/spaces`. diff --git a/package-lock.json b/package-lock.json index d86e1d56..f7136f5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ably/spaces", - "version": "0.1.3", + "version": "0.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ably/spaces", - "version": "0.1.3", + "version": "0.2.0", "license": "ISC", "dependencies": { "nanoid": "^5.0.2" diff --git a/package.json b/package.json index 66381f88..b850b729 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/spaces", - "version": "0.1.3", + "version": "0.2.0", "description": "", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", diff --git a/src/version.ts b/src/version.ts index 2df250dc..95f51588 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ // Manually update when bumping version -const VERSION = '0.1.3'; +const VERSION = '0.2.0'; export { VERSION }; From 093ac0966799f202e171e1a3950434aafe5ee487 Mon Sep 17 00:00:00 2001 From: Dominik Piatek Date: Wed, 18 Oct 2023 17:32:53 +0100 Subject: [PATCH 143/191] Return value of a lock not it's reference When a lock is retrieved by using locks.get, a reference to an object is returned. By mutating this reference, a developer, could in theory, mutate the lock held by Spaces. This could lead to subtle and hard to debug issues. This commit changes the .get and .getAll methods to return copies of objects instead. This breaks some of the internal mutations, so they are replaced by the already present .setLock method which will override the lock with a new object, severing any dependecies on references. --- src/Locks.test.ts | 23 +++++++++++++++++++++++ src/Locks.ts | 23 ++++++++++++----------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/Locks.test.ts b/src/Locks.test.ts index 5441799a..6cfaf670 100644 --- a/src/Locks.test.ts +++ b/src/Locks.test.ts @@ -380,6 +380,22 @@ describe('Locks', () => { expect(lock3).toBeDefined(); }); + it('returns the lock by value, not reference', ({ space }) => { + const lockGet1 = space.locks.get('lock1'); + const lockGet2 = space.locks.get('lock1'); + expect(lockGet1).toEqual(expect.objectContaining({ status: 'locked' })); + // Note we are using toBe here on purpose, to check the reference, not value + expect(lockGet1).not.toBe(lockGet2); + }); + + it('returns locks by value, not reference', async ({ space }) => { + const lockGetAll1 = await space.locks.getAll(); + const lockGetAll2 = await space.locks.getAll(); + expect(lockGetAll1).toEqual(expect.arrayContaining([expect.objectContaining({ status: 'locked' })])); + // Note we are using toBe here on purpose, to check the reference, not value + expect(lockGetAll1[0]).not.toBe(lockGetAll2[0]); + }); + describe('getSelf', () => { it('returns all locks in the LOCKED state that belong to self', async ({ space }) => { const member1 = await space.members.getByConnectionId('1')!; @@ -398,6 +414,13 @@ describe('Locks', () => { } } }); + + it('returns locks by value not reference', async ({ space }) => { + const locksGetSelf1 = await space.locks.getSelf(); + const locksGetSelf2 = await space.locks.getSelf(); + // Note we are using toBe here on purpose, to check the reference, not value + expect(locksGetSelf1[0]).not.toBe(locksGetSelf2[0]); + }); }); describe('getOthers', () => { diff --git a/src/Locks.ts b/src/Locks.ts index ec0638e9..2b4e1c32 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -133,7 +133,8 @@ export default class Locks extends EventEmitter { if (!locks) return; for (const lock of locks.values()) { if (lock.status === 'locked') { - return lock; + // Return a copy instead of a reference to prevent mutations + return { ...lock }; } } } @@ -211,7 +212,8 @@ export default class Locks extends EventEmitter { for (const locks of this.locks.values()) { for (const lock of locks.values()) { if (lock.status === 'locked') { - allLocks.push(lock); + // Return a copy instead of a reference to prevent mutations + allLocks.push({ ...lock }); } } } @@ -387,8 +389,7 @@ export default class Locks extends EventEmitter { const lock = this.getLock(id, self.connectionId); if (!lock) return; - lock.status = 'unlocked'; - lock.reason = undefined; + this.setLock({ ...lock, status: 'unlocked', reason: undefined }); // Send presence update with the updated lock, but delete afterwards so when the // message is processed an update event is fired this.updatePresence(self); @@ -563,9 +564,9 @@ export default class Locks extends EventEmitter { const lock = locks.get(member.connectionId); if (lock) { - lock.status = 'unlocked'; - lock.reason = undefined; - this.emit('update', lock); + const updatedLock = { ...lock, status: 'unlocked' as const, reason: undefined }; + this.setLock(updatedLock); + this.emit('update', updatedLock); locks.delete(member.connectionId); } } @@ -638,9 +639,9 @@ export default class Locks extends EventEmitter { (pendingLock.timestamp == lock.timestamp && member.connectionId < lock.member.connectionId) ) { pendingLock.status = 'locked'; - lock.status = 'unlocked'; - lock.reason = ERR_LOCK_INVALIDATED(); - this.emit('update', lock); + const updatedLock = { ...lock, status: 'unlocked' as const, reason: ERR_LOCK_INVALIDATED() }; + this.setLock(updatedLock); + this.emit('update', updatedLock); return; } @@ -684,7 +685,7 @@ export default class Locks extends EventEmitter { const lock = locks.get(connectionId); if (lock) { - requests.push(lock); + requests.push({ ...lock }); } } From 0c79b0b70825fc04ccea8fecc33800e175395e1e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 25 Oct 2023 09:30:54 -0300 Subject: [PATCH 144/191] Upgrade Ably to 1.2.46 Resolves COL-533 (malformed LEAVE presence message is sent when space.leave() called whilst holding a lock). --- package-lock.json | 94 +++++++++++++++-------------------------------- package.json | 2 +- 2 files changed, 31 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index f7136f5d..e972621b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "vitest": "^0.34.3" }, "peerDependencies": { - "ably": "^1.2.45", + "ably": "^1.2.46", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, @@ -1686,14 +1686,14 @@ "dev": true }, "node_modules/ably": { - "version": "1.2.45", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", - "integrity": "sha512-8Jk8XT0dSPcLcZ8zSAF+mRtwMzaQR5dWHqGdx9Y859ZLhXiqf4gQb7P+4Elqy1l1IIRoDWgyt0fkyfo7ngUdMA==", + "version": "1.2.46", + "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.46.tgz", + "integrity": "sha512-N+t2C3bWc8P7dL9sJ/WbfspdshSfdJFhkvlqQKBJjPZCjT0WeyW6PW4nQnJK7TfzsiESWZovZ+Aku+/kgZW2OQ==", "peer": true, "dependencies": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", - "ws": "^5.1" + "ws": "^8.14.2" }, "engines": { "node": ">=5.10.x" @@ -1973,12 +1973,6 @@ "node": "*" } }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "peer": true - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4419,27 +4413,6 @@ } } }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6875,12 +6848,23 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", - "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", - "peer": true, - "dependencies": { - "async-limiter": "~1.0.0" + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/xml-name-validator": { @@ -8083,14 +8067,14 @@ "dev": true }, "ably": { - "version": "1.2.45", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", - "integrity": "sha512-8Jk8XT0dSPcLcZ8zSAF+mRtwMzaQR5dWHqGdx9Y859ZLhXiqf4gQb7P+4Elqy1l1IIRoDWgyt0fkyfo7ngUdMA==", + "version": "1.2.46", + "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.46.tgz", + "integrity": "sha512-N+t2C3bWc8P7dL9sJ/WbfspdshSfdJFhkvlqQKBJjPZCjT0WeyW6PW4nQnJK7TfzsiESWZovZ+Aku+/kgZW2OQ==", "peer": true, "requires": { "@ably/msgpack-js": "^0.4.0", "got": "^11.8.5", - "ws": "^5.1" + "ws": "^8.14.2" } }, "accepts": { @@ -8283,12 +8267,6 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "peer": true - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10150,15 +10128,6 @@ "whatwg-url": "^12.0.1", "ws": "^8.13.0", "xml-name-validator": "^4.0.0" - }, - "dependencies": { - "ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "dev": true, - "requires": {} - } } }, "json-buffer": { @@ -11878,13 +11847,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", - "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", - "peer": true, - "requires": { - "async-limiter": "~1.0.0" - } + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "requires": {} }, "xml-name-validator": { "version": "4.0.0", diff --git a/package.json b/package.json index b850b729..f18eddd5 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "nanoid": "^5.0.2" }, "peerDependencies": { - "ably": "^1.2.45", + "ably": "^1.2.46", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, From dac823f6dbb738e030cc09fed15ed85549670770 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 10 Oct 2023 16:20:23 -0300 Subject: [PATCH 145/191] Move Ably sandbox code to test/lib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’ll reuse it in the upcoming integration tests. --- test/cdn-bundle/test/cdnBundle.test.ts | 2 +- test/{cdn-bundle => }/lib/ablySandbox.ts | 2 +- test/lib/tsconfig.json | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) rename test/{cdn-bundle => }/lib/ablySandbox.ts (94%) create mode 100644 test/lib/tsconfig.json diff --git a/test/cdn-bundle/test/cdnBundle.test.ts b/test/cdn-bundle/test/cdnBundle.test.ts index 3a81467d..bc7106bc 100644 --- a/test/cdn-bundle/test/cdnBundle.test.ts +++ b/test/cdn-bundle/test/cdnBundle.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { createSandboxAblyAPIKey } from '../lib/ablySandbox.js'; +import { createSandboxAblyAPIKey } from '../../lib/ablySandbox.js'; test.describe('CDN bundle', () => { /** diff --git a/test/cdn-bundle/lib/ablySandbox.ts b/test/lib/ablySandbox.ts similarity index 94% rename from test/cdn-bundle/lib/ablySandbox.ts rename to test/lib/ablySandbox.ts index 1ec7a72b..fe2aa5a3 100644 --- a/test/cdn-bundle/lib/ablySandbox.ts +++ b/test/lib/ablySandbox.ts @@ -1,5 +1,5 @@ import https from 'node:https'; -import testAppSetup from '../../ably-common/test-resources/test-app-setup.json'; +import testAppSetup from '../ably-common/test-resources/test-app-setup.json'; export interface TestApp { keys: TestAppKey[]; diff --git a/test/lib/tsconfig.json b/test/lib/tsconfig.json new file mode 100644 index 00000000..ea50023a --- /dev/null +++ b/test/lib/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "resolveJsonModule": true + } +} From a2b0d08abda61aa241a4a531b181df1414f6a676 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 10 Oct 2023 15:34:11 -0300 Subject: [PATCH 146/191] Add integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds integration tests that test the core functionality of the SDK. I had originally written these as a single `describe` containing four `it`s, each of which included the test setup and then a list of scenarios, these scenarios just being separated by comments. However, Dom was keen for me to make more use of the vitest APIs for structuring my tests. This resulted in the current setup, where there is one `describe` for each area of functionality, each of these `describe`s then containing multiple `it`s. Each `it` within a given `describe` relies on the side effects of the previous `it`s within that `describe`. Dom preferred this new approach and was happy to accept the compromises that this resulted in, such as having `describe`-level local variables which have no type information, and the fact that the failure of one `it` does not prevent the subsequent `it`s (which rely on the preceding `it`s all having succeeded) from running. With my initial approach, I was able to get the tests running in parallel using `describe.concurrent` on the top-level `describe`. I wasn’t able to figure out how to achieve this in the new approach, in which we want to run all `describe`s in parallel, but run the `it`s within a given `describe` sequentially. I tried using `.concurrent` on the top-level `describe` and `.sequential` on each nested `describe`, but it still just ran everything sequentially. Resolves COL-56. --- .github/workflows/dev-ci.yml | 2 + CONTRIBUTING.md | 17 +- test/integration/integration.test.ts | 610 +++++++++++++++++++++++++++ test/integration/utilities/setup.ts | 36 ++ 4 files changed, 661 insertions(+), 4 deletions(-) create mode 100644 test/integration/integration.test.ts create mode 100644 test/integration/utilities/setup.ts diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 66ac38fe..5ce87a33 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -38,6 +38,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + submodules: true - uses: actions/setup-node@v1 with: node-version: 18 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3be2f6b4..cb678700 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,16 @@ ## Test suite +### Setup + +Before running the tests, you first need to initialize the repository’s submodules: + +``` +git submodule update --init +``` + +### Running the tests + To run the Jest tests, simply run the following command: ```bash @@ -39,7 +49,6 @@ npm test To test the bundle that we upload to the CDN: -1. Initialize submodules: `git submodule update --init` -2. Install browser for Playwright to use: `npx run playwright install chromium` -3. Build the bundle: `npm run build` -4. Run the test: `npm run test:cdn-bundle` +1. Install browser for Playwright to use: `npx run playwright install chromium` +2. Build the bundle: `npm run build` +3. Run the test: `npm run test:cdn-bundle` diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts new file mode 100644 index 00000000..b4072878 --- /dev/null +++ b/test/integration/integration.test.ts @@ -0,0 +1,610 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { createClients } from './utilities/setup.js'; +import { LocationsEventMap } from '../../src/Locations.js'; +import { SpaceEventMap } from '../../src/Space.js'; +import { MembersEventMap } from '../../src/Members.js'; +import { CursorsEventMap } from '../../src/Cursors.js'; +import { nanoid } from 'nanoid'; +import { LocksEventMap } from '../../src/Locks.js'; + +/* + * These tests have one `describe` for each area of functionality, each of these `describe`s then containing multiple `it`s. + * + * Each `it` within a given `describe` is considered to be a single step within a test, and relies on the side effects of the previous `it`s within that `describe`. + */ +describe( + 'integration tests', + () => { + describe('space members', () => { + let performerSpaces; + let observerSpaces; + let performerClientId; + let performerSpace; + let observerSpace; + + beforeAll(async () => { + [{ spaces: performerSpaces, clientId: performerClientId }, { spaces: observerSpaces }] = await createClients({ + count: 2, + }); + + const spaceName = nanoid(); + performerSpace = await performerSpaces.get(spaceName); + // Motivation for choosing 5 seconds for `offlineTimeout`: in scenario 1.4 we wait to observe the members `remove` event, which requires us to wait a duration of `offlineTimeout`, and hence means that execution of this test case will take at least `offlineTimeout`. We choose 5 seconds as an arbitrary value which is small enough such that we’re happy for the test to take this long to execute, but not so short as to run the risk of missing this `remove` event whilst waiting for scenario_1_3’s `leave()` to return. + observerSpace = await observerSpaces.get(spaceName, { offlineTimeout: 5000 }); + }); + + it('scenario 1.1: entering a space', async () => { + const promisesForEventsTriggeredByEntering = { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + // Note: I think that there are situations in which this listener might not get called, for the same reasons as described in commit 6e77941 — that is, the call to `performerSpace.enter()` might result in `observerSpace` seeing a presence UPDATE instead of an ENTER and hence not emitting a members `enter` event. I haven’t observed this happening in practice yet, and I’d like to be able to test that the SDK emits a members `enter` event, so I’m going to keep the test as it is for now. If we can think of a way for this test to be sure that a members `enter` event will be emitted, then we should implement it. + membersEnter: new Promise((resolve) => { + observerSpace.members.once('enter', resolve); + }), + }; + + await performerSpace.enter(); + + const eventsTriggeredByEntering = { + spaceUpdate: await promisesForEventsTriggeredByEntering.spaceUpdate, + membersEnter: await promisesForEventsTriggeredByEntering.membersEnter, + }; + + expect(eventsTriggeredByEntering.spaceUpdate.members).toHaveLength(1); + + for (const member of [ + eventsTriggeredByEntering.spaceUpdate.members[0], + eventsTriggeredByEntering.membersEnter, + ]) { + expect(member.clientId).toEqual(performerClientId); + expect(member.profileData).to.be.null; + expect(member.isConnected).to.be.true; + } + }); + + it('scenario 1.2: updating profile data', async () => { + const promisesForEventsTriggeredByUpdatingProfileData = { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + membersUpdateProfile: new Promise((resolve) => { + observerSpace.members.once('updateProfile', resolve); + }), + }; + + await performerSpace.updateProfileData({ name: 'Luna Gomes' }); + + const eventsTriggeredByUpdatingProfileData = { + spaceUpdate: await promisesForEventsTriggeredByUpdatingProfileData.spaceUpdate, + membersUpdateProfile: await promisesForEventsTriggeredByUpdatingProfileData.membersUpdateProfile, + }; + + expect(eventsTriggeredByUpdatingProfileData.spaceUpdate.members).toHaveLength(1); + + for (const member of [ + eventsTriggeredByUpdatingProfileData.spaceUpdate.members[0], + eventsTriggeredByUpdatingProfileData.membersUpdateProfile, + ]) { + expect(member.clientId).toEqual(performerClientId); + // i.e. matches that passed to `performerSpace.updateProfileData()` + expect(member.profileData).toEqual({ name: 'Luna Gomes' }); + expect(member.isConnected).to.be.true; + } + }); + + it('scenario 1.3: leaving space', async () => { + const promisesForEventsTriggeredByLeaving = { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + membersLeave: new Promise((resolve) => { + observerSpace.members.once('leave', resolve); + }), + }; + + // this profile data is different to that which we passed to `updateProfileData` in scenario 1.2 + await performerSpace.leave({ name: 'Huey Brahma', age: 30 }); + + const eventsTriggeredByLeaving = { + spaceUpdate: await promisesForEventsTriggeredByLeaving.spaceUpdate, + membersLeave: await promisesForEventsTriggeredByLeaving.membersLeave, + }; + + expect(eventsTriggeredByLeaving.spaceUpdate.members).toHaveLength(1); + + for (const member of [eventsTriggeredByLeaving.spaceUpdate.members[0], eventsTriggeredByLeaving.membersLeave]) { + expect(member.clientId).toEqual(performerClientId); + // i.e. matches that passed to `performerSpace.leave()` + expect(member.profileData).toEqual({ name: 'Huey Brahma', age: 30 }); + expect(member.isConnected).to.be.false; + } + }); + + it('scenario 1.4: SDK removes member', async () => { + const promisesForEventsTriggeredByRemoval = { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + membersRemove: new Promise((resolve) => { + observerSpace.members.once('remove', resolve); + }), + }; + + const eventsTriggeredByRemoval = { + spaceUpdate: await promisesForEventsTriggeredByRemoval.spaceUpdate, + membersRemove: await promisesForEventsTriggeredByRemoval.membersRemove, + }; + + expect(eventsTriggeredByRemoval.spaceUpdate.members).to.be.empty; + + expect(eventsTriggeredByRemoval.membersRemove.clientId).toEqual(performerClientId); + // i.e. continues to match that passed to `performerSpace.leave()` + expect(eventsTriggeredByRemoval.membersRemove.profileData).toEqual({ name: 'Huey Brahma', age: 30 }); + expect(eventsTriggeredByRemoval.membersRemove.isConnected).to.be.false; + }); + }); + + describe('cursors', () => { + let performerSpaces; + let observerSpaces; + let performerClientId; + let performerSpace; + let observerSpace; + + beforeAll(async () => { + [{ spaces: performerSpaces, clientId: performerClientId }, { spaces: observerSpaces }] = await createClients({ + count: 2, + }); + + const spaceName = nanoid(); + performerSpace = await performerSpaces.get(spaceName); + observerSpace = await observerSpaces.get(spaceName); + + // needed in order to set cursor position + await performerSpace.enter(); + }); + + it('scenario 2.1: a workaround to get `cursors.set()` working', async () => { + // `performerSpace.cursors.set()` will drop all requests until: + // + // - it has received a presence update on the cursors channel, and, + // - at the moment of receiving this presence update, `presence.get()` on the cursors channel indicates that more than one member is present + // + // But, it does not subscribe for presence updates on the cursors channel until one of the following happens: + // + // 1. its `set()` method is called + // 2. one of its `get*()` methods is called + // 3. its `subscribe` or `unsubscribe` method is called + // + // This seems to mean that a client that sends cursor updates but does not listen for them will drop the first update passed to `cursors.set()`. + // + // So, to work around this, here I perform a "sacrificial" call to `performerSpace.cursors.set()`, the idea of which is to put `performerSpace.cursors.set()` into a state in which it will not drop the updates passed in subsequent calls. + await performerSpace.cursors.set({ position: { x: 0, y: 0 } }); + }); + + it('scenario 2.2: set cursor position', async () => { + // Before calling `performerSpace.cursors.set()` below, we want to be sure that `performerSpace.cursors` has found out about the presence enter operations triggered by calling `performerSpace.cursors.set()` in scenario 2.1, and by calling `observerSpace.cursors.subscribe()` below, so that it doesn’t drop the cursor updates that we pass to `set()`. So here we set up a promise which will resolve once `performerSpace.cursors.channel` sees two clients (i.e. `performerSpaces` and `observerSpaces`) as present. + const performerCursorsChannelObserverPresentPromise = new Promise((resolve) => { + const presence = performerSpace.cursors.channel.presence; + const listener = async () => { + const members = await presence.get(); + if (members.length === 2) { + presence.unsubscribe(listener); + resolve(); + } + }; + presence.subscribe(listener); + }); + + // There’s a lot going on in this scenario (two other promises that are there to guarantee predictable behaviour in the test), so it’s worth pointing out that `cursorUpdatesPromise` is the core of the assertions in this test scenario. + const cursorUpdatesPromise = new Promise((resolve) => { + const observedCursorEventsData: CursorsEventMap['update'][] = []; + const cursorsListener = (data: CursorsEventMap['update']) => { + observedCursorEventsData.push(data); + if (observedCursorEventsData.length === 4) { + observerSpace.cursors.unsubscribe(cursorsListener); + resolve(observedCursorEventsData); + } + }; + + observerSpace.cursors.subscribe('update', cursorsListener); + }); + + // To be sure that the `observerSpace.cursors.subscribe()` listener will receive the cursor positions sent by the calls to `performerSpace.cursors.set()` below, we need to know that the cursors channel attach operation triggered by calling `observerSpace.cursors.subscribe()` has completed. The `cursors.subscribe()` API does not currently provide any direct way for the user to know that this attach operation has completed, so here we do so by directly observing the channel. + // + // We should consider exposing the completion of the attach operation via the `cursors.subscribe()` API, the same way as ably-js exposes it through `presence.subscribe()`. I’m not convinced of the necessity though — not sure how useful it’d be for an average user, and we can work around it in tests (as I have here). + const observerCursorsChannelAttachedPromise = new Promise((resolve) => { + observerSpace.cursors.channel.whenState('attached', resolve); + }); + + await Promise.all([performerCursorsChannelObserverPresentPromise, observerCursorsChannelAttachedPromise]); + + const cursorsToSet = [ + { position: { x: 0, y: 15 }, data: { state: 'move' } }, + { position: { x: 30, y: 20 }, data: { state: 'move' } }, + { position: { x: 40, y: 30 }, data: { state: 'move' } }, + { position: { x: 50, y: 0 }, data: { state: 'leave' } }, + ]; + + for (const cursorToSet of cursorsToSet) { + await performerSpace.cursors.set(cursorToSet); + } + + // Note that we check that the order in which we recieve the cursor updates matches that in which they were passed to `set()` + const observedCursorEventsData = await cursorUpdatesPromise; + for (const [index, setCursor] of cursorsToSet.entries()) { + expect(observedCursorEventsData[index]).toMatchObject({ clientId: performerClientId, ...setCursor }); + } + }); + }); + + describe('member location', () => { + let performerSpaces; + let observerSpaces; + let performerClientId; + let performerSpace; + let observerSpace; + + beforeAll(async () => { + [{ spaces: performerSpaces, clientId: performerClientId }, { spaces: observerSpaces }] = await createClients({ + count: 2, + }); + + const spaceName = nanoid(); + performerSpace = await performerSpaces.get(spaceName); + // Motivation for choosing 5 seconds for `offlineTimeout`: same as in scenario 1.4; that is, in scenario_3_4 we want to wait to observe a members `remove` event, but don’t want test to take too long to execute. + observerSpace = await observerSpaces.get(spaceName, { offlineTimeout: 5000 }); + + // We enter `performerSpace` and wait for `observerSpace` to receive the `update` event that this triggers (we do this so that we can be sure we aren’t instead going to see this event in scenario 3.1). + const promisesForEventsTriggeredByEntering = { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + }; + + await performerSpace.enter(); + + const eventsTriggeredByEntering = { + spaceUpdate: await promisesForEventsTriggeredByEntering.spaceUpdate, + }; + expect(eventsTriggeredByEntering.spaceUpdate.members).toHaveLength(1); + expect(eventsTriggeredByEntering.spaceUpdate.members[0].clientId).toEqual(performerClientId); + }); + + let promisesForEventsTriggeredBySettingSlide1; + + it('scenario 3.1: set a location', async () => { + promisesForEventsTriggeredBySettingSlide1 = { + observerSpace: { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + locationsUpdate: new Promise((resolve) => { + observerSpace.locations.once('update', resolve); + }), + }, + // This listener is not part of scenario 3.1; it’s used at the start of scenario_3_2. + performerSpace: { + locationsUpdate: new Promise((resolve) => { + performerSpace.locations.once('update', resolve); + }), + }, + }; + + await performerSpace.locations.set({ slide: 1 }); + + const eventsTriggeredBySettingSlide1 = { + spaceUpdate: await promisesForEventsTriggeredBySettingSlide1.observerSpace.spaceUpdate, + locationsUpdate: await promisesForEventsTriggeredBySettingSlide1.observerSpace.locationsUpdate, + }; + + expect(eventsTriggeredBySettingSlide1.spaceUpdate.members).toHaveLength(1); + expect(eventsTriggeredBySettingSlide1.spaceUpdate.members[0].clientId).toEqual(performerClientId); + // i.e. matches that passed to `performerSpace.locations.set()` + expect(eventsTriggeredBySettingSlide1.spaceUpdate.members[0].location).toEqual({ slide: 1 }); + + expect(eventsTriggeredBySettingSlide1.locationsUpdate.member.clientId).toEqual(performerClientId); + expect(eventsTriggeredBySettingSlide1.locationsUpdate.previousLocation).toBeNull(); + // i.e. matches that passed to `performerSpace.locations.set()` + expect(eventsTriggeredBySettingSlide1.locationsUpdate.currentLocation).toEqual({ slide: 1 }); + }); + + let promisesForEventsTriggeredBySettingSlide2; + + it('scenario 3.2: set another location', async () => { + // (Start of setting up state for this scenario) + await (async () => { + // Wait for `performerSpace` to become aware of the location update that it just triggered via `locations.set()`. This ensures that `previousLocation` will be as we expect in this scenario’s assertions. + const event = await promisesForEventsTriggeredBySettingSlide1.performerSpace.locationsUpdate; + expect(event.currentLocation).toEqual({ slide: 1 }); + })(); + // (End of setting up state for this scenario) + + promisesForEventsTriggeredBySettingSlide2 = { + observerSpace: { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + locationsUpdate: new Promise((resolve) => { + observerSpace.locations.once('update', resolve); + }), + }, + // This listener is not part of scenario 3.2; it’s used at the start of scenario_3_3. + performerSpace: { + locationsUpdate: new Promise((resolve) => { + performerSpace.locations.once('update', resolve); + }), + }, + }; + + // different data to that in scenario 3.1 + await performerSpace.locations.set({ slide: 2 }); + + const eventsTriggeredBySettingSlide2 = { + spaceUpdate: await promisesForEventsTriggeredBySettingSlide2.observerSpace.spaceUpdate, + locationsUpdate: await promisesForEventsTriggeredBySettingSlide2.observerSpace.locationsUpdate, + }; + + expect(eventsTriggeredBySettingSlide2.spaceUpdate.members).toHaveLength(1); + expect(eventsTriggeredBySettingSlide2.spaceUpdate.members[0].clientId).toEqual(performerClientId); + // i.e. matches that passed by this scenario’s call to `locations.set()` + expect(eventsTriggeredBySettingSlide2.spaceUpdate.members[0].location).toEqual({ slide: 2 }); + + expect(eventsTriggeredBySettingSlide2.locationsUpdate.member.clientId).toEqual(performerClientId); + // i.e. matches that passed by scenario 3.2’s call to `locations.set()` + expect(eventsTriggeredBySettingSlide2.locationsUpdate.previousLocation).toEqual({ slide: 1 }); + // i.e. matches that passed by this scenario’s call to `locations.set()` + expect(eventsTriggeredBySettingSlide2.locationsUpdate.currentLocation).toEqual({ slide: 2 }); + }); + + it('scenario 3.3: leaving space', async () => { + // (Start of setting up state for this scenario) + await (async () => { + // Wait for `performerSpace` to become aware of the location update that it just triggered via `locations.set()`. This ensures that `location` will be as we expect in this scenario’s assertions. + const event = await promisesForEventsTriggeredBySettingSlide2.performerSpace.locationsUpdate; + expect(event.currentLocation).toEqual({ slide: 2 }); + })(); + // (End of setting up state for this scenario) + + const promisesForEventsTriggeredByLeaving = { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + }; + + await performerSpace.leave(); + + const eventsTriggeredByLeaving = { + spaceUpdate: await promisesForEventsTriggeredByLeaving.spaceUpdate, + }; + + expect(eventsTriggeredByLeaving.spaceUpdate.members).toHaveLength(1); + expect(eventsTriggeredByLeaving.spaceUpdate.members[0].clientId).toEqual(performerClientId); + // i.e. matches that passed to `performerSpace.locations.set()` in scenario 3.2 + expect(eventsTriggeredByLeaving.spaceUpdate.members[0].location).toEqual({ slide: 2 }); + }); + + it('scenario 3.4: SDK removes member', async () => { + const promisesForEventsTriggeredByRemoval = { + spaceUpdate: new Promise((resolve) => { + observerSpace.once('update', resolve); + }), + locationsUpdate: new Promise((resolve) => { + observerSpace.locations.once('update', resolve); + }), + }; + + const eventsTriggeredByRemoval = { + spaceUpdate: await promisesForEventsTriggeredByRemoval.spaceUpdate, + locationsUpdate: await promisesForEventsTriggeredByRemoval.locationsUpdate, + }; + + expect(eventsTriggeredByRemoval.spaceUpdate.members).to.be.empty; + + expect(eventsTriggeredByRemoval.locationsUpdate.member.clientId).toEqual(performerClientId); + // i.e. that which was the `currentLocation` in scenario 3.3 + expect(eventsTriggeredByRemoval.locationsUpdate.previousLocation).toEqual({ slide: 2 }); + expect(eventsTriggeredByRemoval.locationsUpdate.currentLocation).toBeNull(); + }); + }); + + describe('locking', () => { + let spaces1; + let clientId1; + let spaces2; + let clientId2; + let space1; + let space2; + + beforeAll(async () => { + [{ spaces: spaces1, clientId: clientId1 }, { spaces: spaces2, clientId: clientId2 }] = await createClients({ + count: 2, + }); + + const spaceName = nanoid(); + space1 = await spaces1.get(spaceName); + space2 = await spaces2.get(spaceName); + + // need to enter in order to acquire locks + await space1.enter(); + await space2.enter(); + }); + + let lockId; + + it('scenario 4.1: query an unlocked lock', async () => { + lockId = nanoid(); + + const getLockReturnValues = await Promise.all([space1.locks.get(lockId), space2.locks.get(lockId)]); + + for (const returnValue of getLockReturnValues) { + expect(returnValue).toBeUndefined(); + } + }); + + it('scenario 4.2: acquire a lock', async () => { + const promisesForEventsTriggeredByAcquiringLock = { + space1: { + locksUpdate: new Promise((resolve) => { + space1.locks.once('update', resolve); + }), + }, + space2: { + locksUpdate: new Promise((resolve) => { + space2.locks.once('update', resolve); + }), + }, + }; + + const lock = await space1.locks.acquire(lockId); + + expect(lock.status).toEqual('pending'); + + const eventsTriggeredByAcquiringLock = { + space1: { + locksUpdate: await promisesForEventsTriggeredByAcquiringLock.space1.locksUpdate, + }, + space2: { + locksUpdate: await promisesForEventsTriggeredByAcquiringLock.space2.locksUpdate, + }, + }; + + for (const event of [ + eventsTriggeredByAcquiringLock.space1.locksUpdate, + eventsTriggeredByAcquiringLock.space2.locksUpdate, + ]) { + expect(event.id).toEqual(lockId); + expect(event.member.clientId).toEqual(clientId1); + expect(event.reason).toBeUndefined(); + expect(event.status).toEqual('locked'); + } + }); + + it('scenario 4.3: query a locked lock', async () => { + const getLockReturnValues = await Promise.all([space1.locks.get(lockId), space2.locks.get(lockId)]); + + for (const lock of getLockReturnValues) { + expect(lock.id).toEqual(lockId); + expect(lock.member.clientId).toEqual(clientId1); + expect(lock.reason).toBeUndefined(); + expect(lock.status).toEqual('locked'); + } + }); + + it('scenario 4.4: try to acquire a lock that’s already held', async () => { + const promisesForEventsTriggeredByAcquiringHeldLock = { + space1: { + locksUpdate: new Promise((resolve) => { + space1.locks.once('update', resolve); + }), + }, + space2: { + locksUpdate: new Promise((resolve) => { + space2.locks.once('update', resolve); + }), + }, + }; + + const space2Lock = await space2.locks.acquire(lockId); + + expect(space2Lock.status).toEqual('pending'); + + const eventsTriggeredByAcquiringHeldLock = { + space1: { + locksUpdate: await promisesForEventsTriggeredByAcquiringHeldLock.space1.locksUpdate, + }, + space2: { + locksUpdate: await promisesForEventsTriggeredByAcquiringHeldLock.space2.locksUpdate, + }, + }; + + // Note that the emitted events make it appear as though space1 has lost the lock but space2 also failed to acquire it, which is not what I would have expected to happen. https://ably.atlassian.net/browse/COL-549 aims to fix this. + + for (const event of [ + eventsTriggeredByAcquiringHeldLock.space1.locksUpdate, + eventsTriggeredByAcquiringHeldLock.space2.locksUpdate, + ]) { + expect(event.id).toEqual(lockId); + expect(event.member.clientId).toEqual(clientId2); + expect(event.status).toEqual('unlocked'); + expect(event.reason?.statusCode).toEqual(400); + expect(event.reason?.code).toEqual(101003); + } + }); + + it('scenario 4.5: lock holder leaves space', async () => { + const promisesForEventsTriggeredByLockHolderLeaving = { + space1: { + locksUpdate: new Promise((resolve) => { + space1.locks.once('update', resolve); + }), + }, + space2: { + locksUpdate: new Promise((resolve) => { + space2.locks.once('update', resolve); + }), + }, + }; + + await space1.leave(); + + const eventsTriggeredByLockHolderLeaving = { + space1: { + locksUpdate: await promisesForEventsTriggeredByLockHolderLeaving.space1.locksUpdate, + }, + space2: { + locksUpdate: await promisesForEventsTriggeredByLockHolderLeaving.space2.locksUpdate, + }, + }; + + for (const event of [ + eventsTriggeredByLockHolderLeaving.space1.locksUpdate, + eventsTriggeredByLockHolderLeaving.space2.locksUpdate, + ]) { + expect(event.status).toEqual('unlocked'); + expect(event.reason).toBeUndefined(); + } + }); + + it('scenario 4.6: other client tries to acquire a lock that’s no longer held', async () => { + const promisesForEventsTriggeredByOtherClientAcquiringLock = { + space1: { + locksUpdate: new Promise((resolve) => { + space1.locks.once('update', resolve); + }), + }, + space2: { + locksUpdate: new Promise((resolve) => { + space2.locks.once('update', resolve); + }), + }, + }; + + const lock2 = await space2.locks.acquire(lockId); + + expect(lock2.status).toEqual('pending'); + + const eventsTriggeredByOtherClientAcquiringLock = { + space1: { + locksUpdate: await promisesForEventsTriggeredByOtherClientAcquiringLock.space1.locksUpdate, + }, + space2: { + locksUpdate: await promisesForEventsTriggeredByOtherClientAcquiringLock.space2.locksUpdate, + }, + }; + + for (const event of [ + eventsTriggeredByOtherClientAcquiringLock.space1.locksUpdate, + eventsTriggeredByOtherClientAcquiringLock.space2.locksUpdate, + ]) { + expect(event.id).toEqual(lockId); + expect(event.member.clientId).toEqual(clientId2); + expect(event.reason).toBeUndefined(); + expect(event.status).toEqual('locked'); + } + }); + }); + }, + { timeout: 60_000 }, +); diff --git a/test/integration/utilities/setup.ts b/test/integration/utilities/setup.ts new file mode 100644 index 00000000..6400e953 --- /dev/null +++ b/test/integration/utilities/setup.ts @@ -0,0 +1,36 @@ +import { Realtime } from 'ably'; +import { nanoid } from 'nanoid'; +import { createSandboxAblyAPIKey } from '../../lib/ablySandbox.js'; +import Spaces from '../../../src/Spaces.js'; + +/** + * Fetches a key for the Ably sandbox environment. This key is shared between all callers of this function. + */ +const fetchSharedSandboxKey = (() => { + const sandboxKeyPromise = createSandboxAblyAPIKey(); + + return async () => { + return await sandboxKeyPromise; + }; +})(); + +/** + * Performs the following part of a test setup: + * + * > Given $count Spaces clients, all configured to use the same API key, and each configured to use a different randomly-generated client ID... + */ +export async function createClients({ count }: { count: number }) { + const sandboxKey = await fetchSharedSandboxKey(); + + return Array.from({ length: count }, () => { + const clientId = nanoid(); + const realtime = new Realtime.Promise({ + environment: 'sandbox', + key: sandboxKey, + clientId: clientId, + }); + const spaces = new Spaces(realtime); + + return { spaces: spaces, clientId: clientId }; + }); +} From d8795c8c316d9936bd994a7c45b53235e7f6f3a6 Mon Sep 17 00:00:00 2001 From: Dominik Piatek Date: Wed, 8 Nov 2023 15:19:10 +0000 Subject: [PATCH 147/191] Use ::$space as space channel suffix ::$space is a channel tag and has special meaning within Ably, unlike the suffix `-space`. --- docs/channel-usage.md | 2 +- src/Space.test.ts | 2 +- src/Space.ts | 3 +-- src/Spaces.test.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/channel-usage.md b/docs/channel-usage.md index ee928cc6..27ef599e 100644 --- a/docs/channel-usage.md +++ b/docs/channel-usage.md @@ -8,7 +8,7 @@ The below channels are used by the Spaces library internally. Each `Space` (as defined by the [`Space` class](https://sdk.ably.com/builds/ably/spaces/main/typedoc/classes/Space.html)) creates its own [Ably Channel](https://ably.com/docs/channels). -The channel name is defined by the `name` of the Space and takes the form: `${name}-space`. The full name of a `channel` belonging to a `Space` called 'slides' would therefore be `slides-space`. +The channel name is defined by the `name` of the Space and takes the form: `${name}::$space`. The full name of a `channel` belonging to a `Space` called 'slides' would therefore be `slides::$space`. ### Cursors diff --git a/src/Space.test.ts b/src/Space.test.ts index d8f7a011..7a69ad01 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -48,7 +48,7 @@ describe('Space', () => { expect(channelSpy).toHaveBeenNthCalledWith( 1, - 'test-space', + 'test::$space', expect.objectContaining({ params: expect.objectContaining({ agent: expect.stringContaining('spaces'), diff --git a/src/Space.ts b/src/Space.ts index ada781fd..85b4fe7b 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -15,8 +15,7 @@ import type { Subset, PresenceMember } from './utilities/types.js'; import { VERSION } from './version.js'; -// Replace by ::$space when that channel tag will be available -const SPACE_CHANNEL_TAG = '-space'; +const SPACE_CHANNEL_TAG = '::$space'; const SPACE_OPTIONS_DEFAULTS = { offlineTimeout: 120_000, diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index bebb9c84..c0451e17 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -29,7 +29,7 @@ describe('Spaces', () => { expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenNthCalledWith( 1, - 'test-space', + 'test::$space', expect.objectContaining({ params: expect.objectContaining({ agent: expect.stringContaining('spaces'), From 946b49c3048d2e1325afc2e6f21bedf7a4f07ac1 Mon Sep 17 00:00:00 2001 From: Dominik Piatek Date: Wed, 8 Nov 2023 15:35:34 +0000 Subject: [PATCH 148/191] Bump to 0.3.0 --- CHANGELOG.md | 19 +++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb6beae..2b2443aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # CHANGELOG +## v0.3.0 + +Breaking changes in this release: + +* [COL-577] Use ::$space as space channel suffix [\#244](https://github.com/ably/spaces/pull/244) + +This change updates the name of the channel [used by spaces internally](./docs/channel-usage.md). + +Previously, if you create a space with `spaces.get('example')`, this would create an Ably channel named `example-space`. This will now become `example::$space`. + +When deploying this change, it's important to note that there will be no continuty between the channel used so far and the channel used after the update. If you have customer applications that are running spaces, sessions using the old channel should end before this update is implemented. + +Other notable changes: + +* [COL-533] Upgrade Ably to 1.2.46 by @lawrence-forooghian in https://github.com/ably/spaces/pull/235 +* [COL-56] Add integration tests by @lawrence-forooghian in https://github.com/ably/spaces/pull/229 + +**Full Changelog**: https://github.com/ably/spaces/compare/0.2.0...0.3.0 + ## v0.2.0 In this release, we introduce React hooks for Spaces [\#233](https://github.com/ably/spaces/pull/233). See the [React Hooks documentation](/docs/react.md) for further details. diff --git a/package-lock.json b/package-lock.json index e972621b..94fdecd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ably/spaces", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ably/spaces", - "version": "0.2.0", + "version": "0.3.0", "license": "ISC", "dependencies": { "nanoid": "^5.0.2" diff --git a/package.json b/package.json index f18eddd5..e902c4e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/spaces", - "version": "0.2.0", + "version": "0.3.0", "description": "", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", diff --git a/src/version.ts b/src/version.ts index 95f51588..e1d77662 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ // Manually update when bumping version -const VERSION = '0.2.0'; +const VERSION = '0.3.0'; export { VERSION }; From 2cf104c32fc0d96b192737c688274f17627bb2bc Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 25 Oct 2023 17:40:38 +0100 Subject: [PATCH 149/191] fix: useCursors test with newest ably-js --- src/react/useCursors.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/react/useCursors.test.tsx b/src/react/useCursors.test.tsx index b7020e33..8337b7e3 100644 --- a/src/react/useCursors.test.tsx +++ b/src/react/useCursors.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { Realtime } from 'ably/promises'; import { it, beforeEach, describe, expect, vi } from 'vitest'; -import { waitFor, renderHook } from '@testing-library/react'; +import { waitFor, renderHook, act } from '@testing-library/react'; import { SpacesProvider } from './contexts/SpacesContext.js'; import { SpaceProvider } from './contexts/SpaceContext.js'; import Spaces from '../index.js'; @@ -114,7 +114,9 @@ describe('useCursors', () => { data: [{ cursor: { position: { x: 1, y: 1 } } }], }; - dispensing.processBatch(fakeMessage); + await act(() => { + dispensing.processBatch(fakeMessage); + }); await waitFor(() => { expect(result.current.cursors).toEqual({ From dea9a1f50bfd8732305b7a84ac7b99194ec5dc9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 08:50:48 +0000 Subject: [PATCH 150/191] build(deps-dev): bump prettier from 3.0.3 to 3.1.0 Bumps [prettier](https://github.com/prettier/prettier) from 3.0.3 to 3.1.0. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.0.3...3.1.0) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94fdecd4..a6f52bb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5207,9 +5207,9 @@ } }, "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -10692,9 +10692,9 @@ "dev": true }, "prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true }, "pretty-format": { From 8480e07368c1ccf2674d266ae455c600565c8e23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 08:12:39 +0000 Subject: [PATCH 151/191] build(deps-dev): bump rollup from 3.29.1 to 4.5.0 Bumps [rollup](https://github.com/rollup/rollup) from 3.29.1 to 4.5.0. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v3.29.1...v4.5.0) --- updated-dependencies: - dependency-name: rollup dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 337 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 315 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6f52bb0..6bb2b5b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "rollup": "^3.28.0", + "rollup": "^4.5.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", "typescript": "^4.9.5", @@ -933,9 +933,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", - "integrity": "sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", @@ -946,7 +946,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -954,6 +954,162 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.0.tgz", + "integrity": "sha512-OINaBGY+Wc++U0rdr7BLuFClxcoWaVW3vQYqmQq6B3bqQ/2olkaoz+K8+af/Mmka/C2yN5j+L9scBkv4BtKsDA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.5.0.tgz", + "integrity": "sha512-UdMf1pOQc4ZmUA/NTmKhgJTBimbSKnhPS2zJqucqFyBRFPnPDtwA8MzrGNTjDeQbIAWfpJVAlxejw+/lQyBK/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.5.0.tgz", + "integrity": "sha512-L0/CA5p/idVKI+c9PcAPGorH6CwXn6+J0Ys7Gg1axCbTPgI8MeMlhA6fLM9fK+ssFhqogMHFC8HDvZuetOii7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.5.0.tgz", + "integrity": "sha512-QZCbVqU26mNlLn8zi/XDDquNmvcr4ON5FYAHQQsyhrHx8q+sQi/6xduoznYXwk/KmKIXG5dLfR0CvY+NAWpFYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.5.0.tgz", + "integrity": "sha512-VpSQ+xm93AeV33QbYslgf44wc5eJGYfYitlQzAi3OObu9iwrGXEnmu5S3ilkqE3Pr/FkgOiJKV/2p0ewf4Hrtg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.5.0.tgz", + "integrity": "sha512-OrEyIfpxSsMal44JpEVx9AEcGpdBQG1ZuWISAanaQTSMeStBW+oHWwOkoqR54bw3x8heP8gBOyoJiGg+fLY8qQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.5.0.tgz", + "integrity": "sha512-1H7wBbQuE6igQdxMSTjtFfD+DGAudcYWhp106z/9zBA8OQhsJRnemO4XGavdzHpGhRtRxbgmUGdO3YQgrWf2RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.5.0.tgz", + "integrity": "sha512-FVyFI13tXw5aE65sZdBpNjPVIi4Q5mARnL/39UIkxvSgRAIqCo5sCpCELk0JtXHGee2owZz5aNLbWNfBHzr71Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.5.0.tgz", + "integrity": "sha512-eBPYl2sLpH/o8qbSz6vPwWlDyThnQjJfcDOGFbNjmjb44XKC1F5dQfakOsADRVrXCNzM6ZsSIPDG5dc6HHLNFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.5.0.tgz", + "integrity": "sha512-xaOHIfLOZypoQ5U2I6rEaugS4IYtTgP030xzvrBf5js7p9WI9wik07iHmsKaej8Z83ZDxN5GyypfoyKV5O5TJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.5.0.tgz", + "integrity": "sha512-Al6quztQUrHwcOoU2TuFblUQ5L+/AmPBXFR6dUvyo4nRj2yQRK0WIUaGMF/uwKulvRcXkpHe3k9A8Vf93VDktA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.0.tgz", + "integrity": "sha512-8kdW+brNhI/NzJ4fxDufuJUjepzINqJKLGHuxyAtpPG9bMbn8P5mtaCcbOm0EzLJ+atg+kF9dwg8jpclkVqx5w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1139,9 +1295,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/express": { @@ -5543,18 +5699,30 @@ } }, "node_modules/rollup": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.1.tgz", - "integrity": "sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.5.0.tgz", + "integrity": "sha512-41xsWhzxqjMDASCxH5ibw1mXk+3c4TNI2UjKbLxe6iEzrSQnqOzmmK8/3mufCPbzHNJ2e04Fc1ddI35hHy+8zg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.5.0", + "@rollup/rollup-android-arm64": "4.5.0", + "@rollup/rollup-darwin-arm64": "4.5.0", + "@rollup/rollup-darwin-x64": "4.5.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.5.0", + "@rollup/rollup-linux-arm64-gnu": "4.5.0", + "@rollup/rollup-linux-arm64-musl": "4.5.0", + "@rollup/rollup-linux-x64-gnu": "4.5.0", + "@rollup/rollup-linux-x64-musl": "4.5.0", + "@rollup/rollup-win32-arm64-msvc": "4.5.0", + "@rollup/rollup-win32-ia32-msvc": "4.5.0", + "@rollup/rollup-win32-x64-msvc": "4.5.0", "fsevents": "~2.3.2" } }, @@ -6591,6 +6759,22 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/vitest": { "version": "0.34.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.4.tgz", @@ -7502,9 +7686,9 @@ } }, "@rollup/pluginutils": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", - "integrity": "sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", "dev": true, "requires": { "@types/estree": "^1.0.0", @@ -7512,6 +7696,90 @@ "picomatch": "^2.3.1" } }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.0.tgz", + "integrity": "sha512-OINaBGY+Wc++U0rdr7BLuFClxcoWaVW3vQYqmQq6B3bqQ/2olkaoz+K8+af/Mmka/C2yN5j+L9scBkv4BtKsDA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.5.0.tgz", + "integrity": "sha512-UdMf1pOQc4ZmUA/NTmKhgJTBimbSKnhPS2zJqucqFyBRFPnPDtwA8MzrGNTjDeQbIAWfpJVAlxejw+/lQyBK/w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.5.0.tgz", + "integrity": "sha512-L0/CA5p/idVKI+c9PcAPGorH6CwXn6+J0Ys7Gg1axCbTPgI8MeMlhA6fLM9fK+ssFhqogMHFC8HDvZuetOii7w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.5.0.tgz", + "integrity": "sha512-QZCbVqU26mNlLn8zi/XDDquNmvcr4ON5FYAHQQsyhrHx8q+sQi/6xduoznYXwk/KmKIXG5dLfR0CvY+NAWpFYQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.5.0.tgz", + "integrity": "sha512-VpSQ+xm93AeV33QbYslgf44wc5eJGYfYitlQzAi3OObu9iwrGXEnmu5S3ilkqE3Pr/FkgOiJKV/2p0ewf4Hrtg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.5.0.tgz", + "integrity": "sha512-OrEyIfpxSsMal44JpEVx9AEcGpdBQG1ZuWISAanaQTSMeStBW+oHWwOkoqR54bw3x8heP8gBOyoJiGg+fLY8qQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.5.0.tgz", + "integrity": "sha512-1H7wBbQuE6igQdxMSTjtFfD+DGAudcYWhp106z/9zBA8OQhsJRnemO4XGavdzHpGhRtRxbgmUGdO3YQgrWf2RA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.5.0.tgz", + "integrity": "sha512-FVyFI13tXw5aE65sZdBpNjPVIi4Q5mARnL/39UIkxvSgRAIqCo5sCpCELk0JtXHGee2owZz5aNLbWNfBHzr71Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.5.0.tgz", + "integrity": "sha512-eBPYl2sLpH/o8qbSz6vPwWlDyThnQjJfcDOGFbNjmjb44XKC1F5dQfakOsADRVrXCNzM6ZsSIPDG5dc6HHLNFg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.5.0.tgz", + "integrity": "sha512-xaOHIfLOZypoQ5U2I6rEaugS4IYtTgP030xzvrBf5js7p9WI9wik07iHmsKaej8Z83ZDxN5GyypfoyKV5O5TJA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.5.0.tgz", + "integrity": "sha512-Al6quztQUrHwcOoU2TuFblUQ5L+/AmPBXFR6dUvyo4nRj2yQRK0WIUaGMF/uwKulvRcXkpHe3k9A8Vf93VDktA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.0.tgz", + "integrity": "sha512-8kdW+brNhI/NzJ4fxDufuJUjepzINqJKLGHuxyAtpPG9bMbn8P5mtaCcbOm0EzLJ+atg+kF9dwg8jpclkVqx5w==", + "dev": true, + "optional": true + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -7668,9 +7936,9 @@ } }, "@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "@types/express": { @@ -10927,11 +11195,23 @@ } }, "rollup": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.1.tgz", - "integrity": "sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==", - "dev": true, - "requires": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.5.0.tgz", + "integrity": "sha512-41xsWhzxqjMDASCxH5ibw1mXk+3c4TNI2UjKbLxe6iEzrSQnqOzmmK8/3mufCPbzHNJ2e04Fc1ddI35hHy+8zg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.5.0", + "@rollup/rollup-android-arm64": "4.5.0", + "@rollup/rollup-darwin-arm64": "4.5.0", + "@rollup/rollup-darwin-x64": "4.5.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.5.0", + "@rollup/rollup-linux-arm64-gnu": "4.5.0", + "@rollup/rollup-linux-arm64-musl": "4.5.0", + "@rollup/rollup-linux-x64-gnu": "4.5.0", + "@rollup/rollup-linux-x64-musl": "4.5.0", + "@rollup/rollup-win32-arm64-msvc": "4.5.0", + "@rollup/rollup-win32-ia32-msvc": "4.5.0", + "@rollup/rollup-win32-x64-msvc": "4.5.0", "fsevents": "~2.3.2" } }, @@ -11665,6 +11945,17 @@ "fsevents": "~2.3.2", "postcss": "^8.4.27", "rollup": "^3.27.1" + }, + "dependencies": { + "rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + } } }, "vite-node": { diff --git a/package.json b/package.json index e902c4e9..03709ca0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "rollup": "^3.28.0", + "rollup": "^4.5.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", "typescript": "^4.9.5", From 3b21cec8e2b03d525aa38a746a7688670660f757 Mon Sep 17 00:00:00 2001 From: Dominik Piatek Date: Mon, 20 Nov 2023 16:38:33 +0000 Subject: [PATCH 152/191] Upgrade rollup plugins --- package-lock.json | 32 ++++++++++++++++---------------- package.json | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6bb2b5b7..4709b087 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ }, "devDependencies": { "@playwright/test": "^1.39.0", - "@rollup/plugin-node-resolve": "^15.2.1", - "@rollup/plugin-terser": "^0.4.3", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", "@testing-library/react": "^14.0.0", "@types/express": "^4.17.18", "@types/react": "^18.2.23", @@ -886,9 +886,9 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", - "integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -902,7 +902,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.78.0||^3.0.0" + "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -911,9 +911,9 @@ } }, "node_modules/@rollup/plugin-terser": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", - "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, "dependencies": { "serialize-javascript": "^6.0.1", @@ -924,7 +924,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.x || ^3.x" + "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -7661,9 +7661,9 @@ } }, "@rollup/plugin-node-resolve": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", - "integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", "dev": true, "requires": { "@rollup/pluginutils": "^5.0.1", @@ -7675,9 +7675,9 @@ } }, "@rollup/plugin-terser": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", - "integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, "requires": { "serialize-javascript": "^6.0.1", diff --git a/package.json b/package.json index 03709ca0..d688582d 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "keywords": [], "devDependencies": { "@playwright/test": "^1.39.0", - "@rollup/plugin-node-resolve": "^15.2.1", - "@rollup/plugin-terser": "^0.4.3", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", "@testing-library/react": "^14.0.0", "@types/express": "^4.17.18", "@types/react": "^18.2.23", From 4988a3891af3d5c56ece8f3737e1b533a5e0f807 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:46:19 +0000 Subject: [PATCH 153/191] build(deps-dev): bump @babel/traverse from 7.22.8 to 7.23.2 in /demo Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.8 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- demo/package-lock.json | 208 ++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 98 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index cf8846eb..243cbb69 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -41,35 +41,40 @@ }, "..": { "name": "@ably/spaces", - "version": "0.1.3", + "version": "0.2.0", "license": "ISC", "dependencies": { - "nanoid": "^4.0.2" + "nanoid": "^5.0.2" }, "devDependencies": { + "@playwright/test": "^1.39.0", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-terser": "^0.4.3", "@testing-library/react": "^14.0.0", + "@types/express": "^4.17.18", "@types/react": "^18.2.23", "@types/react-dom": "^18.2.8", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", + "@vitest/coverage-v8": "^0.34.6", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", + "express": "^4.18.2", "jsdom": "^22.1.0", - "prettier": "^2.8.3", + "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "rollup": "^3.28.0", "ts-node": "^10.9.1", + "typedoc": "^0.25.2", "typescript": "^4.9.5", "vitest": "^0.34.3" }, "peerDependencies": { - "ably": "^1.2.45", + "ably": "^1.2.46", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, @@ -129,12 +134,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -189,12 +195,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -232,22 +238,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -339,9 +345,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -371,13 +377,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -385,9 +391,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -427,33 +433,33 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -462,13 +468,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -20438,25 +20444,30 @@ "@ably/spaces": { "version": "file:..", "requires": { + "@playwright/test": "^1.39.0", "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-terser": "^0.4.3", "@testing-library/react": "^14.0.0", + "@types/express": "^4.17.18", "@types/react": "^18.2.23", "@types/react-dom": "^18.2.8", "@typescript-eslint/eslint-plugin": "^5.51.0", "@typescript-eslint/parser": "^5.51.0", "@vitest/coverage-c8": "^0.33.0", + "@vitest/coverage-v8": "^0.34.6", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^46.7.0", "eslint-plugin-security": "^1.7.1", + "express": "^4.18.2", "jsdom": "^22.1.0", - "nanoid": "^4.0.2", - "prettier": "^2.8.3", + "nanoid": "^5.0.2", + "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "rollup": "^3.28.0", "ts-node": "^10.9.1", + "typedoc": "^0.25.2", "typescript": "^4.9.5", "vitest": "^0.34.3" } @@ -20478,12 +20489,13 @@ } }, "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -20524,12 +20536,12 @@ } }, "@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -20557,19 +20569,19 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { @@ -20634,9 +20646,9 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -20657,20 +20669,20 @@ } }, "@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/plugin-transform-react-jsx-self": { @@ -20692,42 +20704,42 @@ } }, "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, From af77b3cfaa783a4e437364537f745ce659282cf9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 12:40:25 +0000 Subject: [PATCH 154/191] build(deps-dev): bump typescript from 4.9.5 to 5.2.2 Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.2.2. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/commits) --- updated-dependencies: - dependency-name: typescript dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4709b087..8fc78659 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "rollup": "^4.5.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", - "typescript": "^4.9.5", + "typescript": "^5.2.2", "vitest": "^0.34.3" }, "peerDependencies": { @@ -6573,16 +6573,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ufo": { @@ -11852,9 +11852,9 @@ } }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true }, "ufo": { diff --git a/package.json b/package.json index d688582d..32850980 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "rollup": "^4.5.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", - "typescript": "^4.9.5", + "typescript": "^5.2.2", "vitest": "^0.34.3" }, "dependencies": { From 5668c999849bd8c401f92a395cae804e58549e97 Mon Sep 17 00:00:00 2001 From: Dominik Piatek Date: Tue, 21 Nov 2023 13:20:14 +0000 Subject: [PATCH 155/191] Do not run upload actions if env var is not set ABLY_AWS_ACCOUNT_ID_SDK is an org level secret that will not be available to anybody outside of the Ably org, like dependabot. To prevent failures from PRs opened by dependabot et al, do run the upload steps. --- .github/workflows/dev-ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 5ce87a33..029bc7dd 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -72,12 +72,20 @@ jobs: run: npm run docs - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 + env: + ably_aws_account_id_sdk: ${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }} + # do not run if these variables are not available; they will not be available for anybody outside the Ably org + if: ${{ env.ably_aws_account_id_sdk != '' }} with: aws-region: eu-west-2 role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/ably-sdk-builds-spaces role-session-name: "${{ github.run_id }}-${{ github.run_number }}" - name: Upload Documentation uses: ably/sdk-upload-action@v1 + env: + ably_aws_account_id_sdk: ${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }} + # do not run if these variables are not available; they will not be available for anybody outside the Ably org + if: ${{ env.ably_aws_account_id_sdk != '' }} with: sourcePath: docs/typedoc/generated githubToken: ${{ secrets.GITHUB_TOKEN }} From c8f19e78671e46a1f341f157dfe3b7c8df4787d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:04:24 +0000 Subject: [PATCH 156/191] build(deps-dev): bump eslint-plugin-jsdoc from 46.7.0 to 46.9.0 Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 46.7.0 to 46.9.0. - [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases) - [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc) - [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v46.7.0...v46.9.0) --- updated-dependencies: - dependency-name: eslint-plugin-jsdoc dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fc78659..3e9afea6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -302,12 +302,12 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", - "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", "dev": true, "dependencies": { - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "esquery": "^1.5.0", "jsdoc-type-pratt-parser": "~4.0.0" }, @@ -2459,9 +2459,9 @@ "dev": true }, "node_modules/comment-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", - "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, "engines": { "node": ">= 12.0.0" @@ -3158,14 +3158,14 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.7.0.tgz", - "integrity": "sha512-VuNF+5WaiqocDDA6zvm+/D6DYo+DPFuSBOb8oSWbu0CVh+aaL3TAtpB0L0XdYYib1HHudMCHd2QeA25Tn1Pkfw==", + "version": "46.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.0.tgz", + "integrity": "sha512-UQuEtbqLNkPf5Nr/6PPRCtr9xypXY+g8y/Q7gPa0YK7eDhh0y2lWprXRnaYbW7ACgIUvpDKy9X2bZqxtGzBG9Q==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.40.1", + "@es-joy/jsdoccomment": "~0.41.0", "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", @@ -7336,12 +7336,12 @@ } }, "@es-joy/jsdoccomment": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", - "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", "dev": true, "requires": { - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "esquery": "^1.5.0", "jsdoc-type-pratt-parser": "~4.0.0" } @@ -8797,9 +8797,9 @@ "dev": true }, "comment-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", - "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true }, "concat-map": { @@ -9369,14 +9369,14 @@ } }, "eslint-plugin-jsdoc": { - "version": "46.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.7.0.tgz", - "integrity": "sha512-VuNF+5WaiqocDDA6zvm+/D6DYo+DPFuSBOb8oSWbu0CVh+aaL3TAtpB0L0XdYYib1HHudMCHd2QeA25Tn1Pkfw==", + "version": "46.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.0.tgz", + "integrity": "sha512-UQuEtbqLNkPf5Nr/6PPRCtr9xypXY+g8y/Q7gPa0YK7eDhh0y2lWprXRnaYbW7ACgIUvpDKy9X2bZqxtGzBG9Q==", "dev": true, "requires": { - "@es-joy/jsdoccomment": "~0.40.1", + "@es-joy/jsdoccomment": "~0.41.0", "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", From 67aafaf01c03f6a9446e5e0b3bbcd2a3cbfbfe27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:03:48 +0000 Subject: [PATCH 157/191] build(deps-dev): bump eslint from 8.49.0 to 8.54.0 Bumps [eslint](https://github.com/eslint/eslint) from 8.49.0 to 8.54.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.49.0...v8.54.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 102 ++++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e9afea6..8d5fd189 100644 --- a/package-lock.json +++ b/package-lock.json @@ -692,9 +692,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -715,21 +715,21 @@ } }, "node_modules/@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -751,9 +751,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@istanbuljs/schema": { @@ -1722,6 +1722,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vitest/coverage-c8": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.33.0.tgz", @@ -2997,18 +3003,19 @@ } }, "node_modules/eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -3766,9 +3773,9 @@ } }, "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -7516,9 +7523,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -7533,18 +7540,18 @@ } }, "@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" } @@ -7556,9 +7563,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "@istanbuljs/schema": { @@ -8243,6 +8250,12 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "@vitest/coverage-c8": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.33.0.tgz", @@ -9213,18 +9226,19 @@ "dev": true }, "eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -9825,9 +9839,9 @@ } }, "globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" From f8b949d7402d853ee26578fa150bf0316c01d18a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:04:07 +0000 Subject: [PATCH 158/191] build(deps-dev): bump @playwright/test from 1.39.0 to 1.40.0 Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.39.0 to 1.40.0. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.39.0...v1.40.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d5fd189..96286e03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -871,12 +871,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", - "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz", + "integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==", "dev": true, "dependencies": { - "playwright": "1.39.0" + "playwright": "1.40.0" }, "bin": { "playwright": "cli.js" @@ -5285,12 +5285,12 @@ } }, "node_modules/playwright": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", - "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz", + "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==", "dev": true, "dependencies": { - "playwright-core": "1.39.0" + "playwright-core": "1.40.0" }, "bin": { "playwright": "cli.js" @@ -5303,9 +5303,9 @@ } }, "node_modules/playwright-core": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", - "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", + "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -7659,12 +7659,12 @@ } }, "@playwright/test": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", - "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz", + "integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==", "dev": true, "requires": { - "playwright": "1.39.0" + "playwright": "1.40.0" } }, "@rollup/plugin-node-resolve": { @@ -10933,19 +10933,19 @@ } }, "playwright": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", - "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz", + "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.39.0" + "playwright-core": "1.40.0" } }, "playwright-core": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", - "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", + "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", "dev": true }, "postcss": { From 7b3d121e743c180e0e247157f4be5c7813cee412 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:35:15 +0000 Subject: [PATCH 159/191] Consolidate docstring comments for Space and Spaces --- src/Space.ts | 204 ++++++++++++-------------------------------------- src/Spaces.ts | 100 +++---------------------- 2 files changed, 59 insertions(+), 245 deletions(-) diff --git a/src/Space.ts b/src/Space.ts index 85b4fe7b..1cd15f59 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -41,7 +41,7 @@ export namespace SpaceEvents { } /** - * The property names of `SpaceEventMap` are the names of the events emitted by { @link Space }. + * The property names of `SpaceEventMap` are the names of the events emitted by {@link Space}. */ export interface SpaceEventMap { /** @@ -69,8 +69,7 @@ export interface SpaceState { export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileData; /** - * - * A space is a virtual area of your application in which realtime collaboration between users can take place. You can have any number of virtual spaces within an application, with a single space being anything from a web page, a sheet within a spreadsheet, an individual slide in a slideshow, or the entire slideshow itself. + * A [space](https://ably.com/docs/spaces/space) is a virtual area of your application in which realtime collaboration between users can take place. You can have any number of virtual spaces within an application, with a single space being anything from a web page, a sheet within a spreadsheet, an individual slide in a slideshow, or the entire slideshow itself. * * The following features can be implemented within a space: * @@ -81,15 +80,6 @@ export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileDat * * A `Space` instance consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member’s location within the application. The position of members’ cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. * - * Space state can be {@link subscribe | subscribed} to by using a `Space` object. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. - * - * To subscribe to any events in a space, you first need to create or retrieve a space. - * - * - * - * - * An instance of a Space created using {@link default.get | spaces.get}. Inherits from {@link EventEmitter}. - * */ class Space extends EventEmitter { /** @@ -102,33 +92,27 @@ class Space extends EventEmitter { */ readonly connectionId: string | undefined; /** - * The options passed to {@link default.get | `Spaces.get()`}, with default values filled in. + * The options passed to {@link default.get | `Spaces.get()`}. */ readonly options: SpaceOptions; /** - * * An instance of {@link Locations}. - * */ readonly locations: Locations; /** - * * An instance of {@link Cursors}. - * */ readonly cursors: Cursors; /** - * * An instance of {@link Members}. - * */ readonly members: Members; /** - * The [ably-js](https://github.com/ably/ably-js) realtime channel instance that this `Cursors` instance uses for transmitting and receiving data. + * The [realtime channel instance](https://ably.com/docs/channels) that this `Space` instance uses for transmitting and receiving data. */ readonly channel: Types.RealtimeChannelPromise; /** - * Provides access to the component locking feature for this space. + * An instance of {@link Locks}. */ readonly locks: Locks; /** @@ -200,30 +184,17 @@ class Space extends EventEmitter { } /** - * - * Entering a space will register a client as a member and emit an {@link MembersEventMap.enter | `enter` } event to all subscribers. Use the `enter()` method to enter a space. - * - * Being entered into a space is required for members to: + * Enter a space to register a client as a member and emit an {@link MembersEventMap.enter | `enter`} event to all subscribers. * - * - { @link updateProfileData | Update their profile data. } - * - { @link Locations.set | Set their location. } - * - { @link Cursors.set | Set their cursor position. } - * - * The following is an example of entering a space: - * - * ```javascript - * await space.enter(); - * ``` - * + * `profileData` can optionally be passed when entering a space. This data can be any arbitrary JSON-serializable object which will be attached to the {@link SpaceMember | member object}. * - * - * > **Moved documentation** - * > - * > This documentation has been moved to { @link ProfileData }. + * Being entered into a space is required for members to: * - * Profile data is returned in the payload of all space events. + * - {@link updateProfileData | Update their profile data.} + * - {@link Locations.set | Set their location.} + * - {@link Cursors.set | Set their cursor position.} * - * The following is an example of setting profile data when entering a space: + * The following is an example of entering a space and setting profile data: * * ```javascript * await space.enter({ @@ -231,13 +202,8 @@ class Space extends EventEmitter { * avatar: 'https://slides-internal.com/users/coranges.png', * }); * ``` - * - * - * - * Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the {@link SpaceMember | member object }. Returns all current space members. - * * - * @param profileData Data to associate with the member who is entering the space. + * @param profileData The data to associate with a member, such as a preferred username or profile picture. */ async enter(profileData: ProfileData = null): Promise { return new Promise((resolve) => { @@ -273,47 +239,31 @@ class Space extends EventEmitter { /** * {@label MAIN_OVERLOAD} * - * - * Profile data can be updated at any point after entering a space by calling `updateProfileData()`. This will emit an `update` event. If a client hasn’t yet entered the space, `updateProfileData()` will instead {@link enter | enter the space }, with the profile data, and emit an { @link MembersEventMap.enter | `enter` } event. + * Update a member's profile data and emit an {@link MembersEventMap.updateProfile | `updateProfile`} event to all subscribers. This data can be any arbitrary JSON-serializable object which will be attached to the {@link SpaceMember | member object}. * - * The following is an example of updating profile data: + * If the client hasn't yet entered the space, `updateProfileData()` will instead enter them into the space, with the profile data, and emit an {@link MembersEventMap.enter | `enter`} event. + * + * The following is an example of a member updating their profile data: * * ```javascript * space.updateProfileData({ - * username: 'Claire Lemons', - * avatar: 'https://slides-internal.com/users/clemons.png', + * username: 'Claire Lime' * }); * ``` - * A function can be passed to `updateProfileData()` in order to update a field based on the existing profile data: + * + * @param profileData The updated profile data to associate with a member, such as a preferred username or profile picture. + */ + async updateProfileData(profileData: ProfileData): Promise; + /** + * Update a member's profile data by passing a function, for example to update a field based on the member's existing profile data: * * ```javascript * space.updateProfileData(currentProfile => { - * return { ...currentProfile, username: 'Clara Lemons' } + * return { ...currentProfile, username: 'Claire Lime' } * }); * ``` - * - * - * - * Update `profileData`. This data can be an arbitrary JSON-serializable object which is attached to the {@link SpaceMember | member object }. If the connection - * has not entered the space, calling `updateProfileData` will call `enter` instead. - * - * A function can also be passed in. This function will receive the existing `profileData` and lets you update based on the existing value of `profileData`: - * - * ```ts - * await space.updateProfileData((oldProfileData) => { - * const newProfileData = getNewProfileData(); - * return { ...oldProfileData, ...newProfileData }; - * }) - * ``` - * * - * @param profileData The new profile data. - */ - async updateProfileData(profileData: ProfileData): Promise; - /** - * See the documentation for { @link updateProfileData:MAIN_OVERLOAD | the main overload }. - * - * @param updateFn A function which receives the existing profile data and returns the new profile data. + * @param updateFn The function which receives the existing profile data and returns the new profile data. */ async updateProfileData(updateFn: UpdateProfileDataFunction): Promise; async updateProfileData(profileDataOrUpdateFn: ProfileData | UpdateProfileDataFunction): Promise { @@ -342,23 +292,13 @@ class Space extends EventEmitter { } /** - * - * Leaving a space will emit a { @link MembersEventMap.leave | `leave` } event to all subscribers. + * Leave a space and emit a {@link MembersEventMap.leave | `leave`} event to all subscribers. `profileData` can optionally be updated when leaving the space. * - * The following is an example of explicitly leaving a space: + * The member may not immediately be removed from the space, depending on the {@link SpaceOptions.offlineTimeout | offlineTimeout} configured. * - * ```javascript - * await space.leave(); - * ``` * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no `leave` event will be emitted. * - * - * - * - * Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See {@link SpaceOptions.offlineTimeout | offlineTimeout }. - * - * - * @param profileData If specified, this updated profile data will accompany the { @link MembersEventMap.leave | `leave` } event. + * @param profileData The updated profile data to associate with a member. */ async leave(profileData: ProfileData = null) { const self = await this.members.getSelf(); @@ -381,15 +321,13 @@ class Space extends EventEmitter { } /** - * - * The current state of the space can be retrieved in a one-off call. This will return an array of all `member` objects currently in the space. This is a local call and retrieves the membership of the space retained in memory by the SDK. + * Retrieve the current state of a space in a one-off call. Returns an array of all `member` objects currently in the space. This is a local call and retrieves the membership of the space retained in memory by the SDK. * - * The following is an example of retrieving the current space state. Ths includes members that have recently left the space, but have not yet been removed: + * The following is an example of retrieving space state: * * ```javascript * const spaceState = await space.getState(); * ``` - * */ async getState(): Promise { const members = await this.members.getAll(); @@ -399,14 +337,13 @@ class Space extends EventEmitter { /** * {@label WITH_EVENTS} * - * - * Subscribe to space state updates by registering a listener. Use the `subscribe()` method on the `space` object to receive updates. + * Subscribe to space state updates by registering a listener for an event name, or an array of event names. * * The following events will trigger a space event: * * - A member enters the space * - A member leaves the space - * - A member is removed from the space state { @link SpaceOptions.offlineTimeout | after the offlineTimeout period } has elapsed + * - A member is removed from the space state after the {@link SpaceOptions.offlineTimeout | offlineTimeout} period has elapsed * - A member updates their profile data * - A member sets a new location * @@ -414,7 +351,7 @@ class Space extends EventEmitter { * * > **Note** * > - * > Avatar stacks and member location events can be subscribed to using the space’s {@link members | `members` } and {@link locations | `locations` } properties.0These events are filtered versions of space state events. Only a single [message](https://ably.com/docs/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. + * > Avatar stacks and member location events can be subscribed to using the space’s {@link members | `members`} and {@link locations | `locations`} properties. These events are filtered versions of space state events. Only a single [message](https://ably.com/docs/channels/messages) is published per event by Ably, irrespective of whether you register listeners for space state or individual namespaces. If you register listeners for both, it is still only a single message. * > * > The key difference between the subscribing to space state or to individual feature events, is that space state events return the current state of the space as an array of all members in each event payload. Individual member and location event payloads only include the relevant data for the member that triggered the event. * @@ -425,64 +362,17 @@ class Space extends EventEmitter { * console.log(spaceState.members); * }); * ``` - * The following is an example payload of a space event. - * - * ```json - * [ - * { - * "clientId": "clemons#142", - * "connectionId": "hd9743gjDc", - * "isConnected": false, - * "lastEvent": { - * "name": "leave", - * "timestamp": 1677595689759 - * }, - * "location": null, - * "profileData": { - * "username": "Claire Lemons", - * "avatar": "https://slides-internal.com/users/clemons.png" - * } - * }, - * { - * "clientId": "amint#5", - * "connectionId": "hg35a4fgjAs", - * "isConnected": true, - * "lastEvent": { - * "name": "enter", - * "timestamp": 173459567340 - * }, - * "location": null, - * "profileData": { - * "username": "Arit Mint", - * "avatar": "https://slides-internal.com/users/amint.png" - * } - * }, - * ... - * ] - * ``` - * The following are the properties of an individual `member` within a space event payload: - * - * | Property | Description | Type | - * |---------------------|------------------------------------------------------------------------|---------| - * | clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | - * | connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | - * | isConnected | Whether the member is connected to Ably or not. | Boolean | - * | profileData | The optional {@link updateProfileData | profile data } associated with the member. | Object | - * | location | The current { @link Locations | location } of the member. | Object | - * | lastEvent.name | The most recent event emitted by the member. | String | - * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * - * * - * @param eventOrEvents The event name or an array of event names. * @param listener The listener to add. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link SpaceEventMap} type. */ subscribe(eventOrEvents: K | K[], listener?: EventListener): void; /** - * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * Subscribe to space state updates by registering a listener for all event types. * - * @param listener The listener to add. + * @param listener An event listener. */ subscribe(listener?: EventListener): void; subscribe( @@ -505,26 +395,30 @@ class Space extends EventEmitter { /** * {@label WITH_EVENTS} * - * - * Unsubscribe from space events to remove previously registered listeners. + * Unsubscribe from specific events, or an array of event names, to remove previously registered listeners. * * The following is an example of removing a listener: * * ```javascript * space.unsubscribe('update', listener); * ``` - * * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to remove. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link SpaceEventMap} type. */ unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; /** - * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but unsubscribes from _all_ events. + * Unsubscribe from all events to remove previously registered listeners. + * + * The following is an example of removing a listener for all events: + * + * ```javascript + * space.unsubscribe(listener); + * ``` * - * @param listener The listener to remove. + * @param listener An event listener. */ unsubscribe(listener?: EventListener): void; unsubscribe( diff --git a/src/Spaces.ts b/src/Spaces.ts index 421c08ab..be2b98f0 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -17,42 +17,30 @@ export interface ClientWithOptions extends Types.RealtimePromise { /** * The `Spaces` class is the entry point to the Spaces SDK. Use its {@link get | `get()`} method to access an individual {@link Space | `Space`}. * - * To create an instance of `Spaces`, you first need to create an instance of the [ably-js](https://github.com/ably/ably-js) Realtime client. You then pass this instance to the {@link constructor | `Spaces()` constructor}. + * To create an instance of `Spaces`, you first need to create an instance of an [Ably realtime client](https://ably.com/docs/getting-started/setup). You then pass this instance to the {@link constructor | Spaces constructor}. */ class Spaces { private spaces: Record = {}; /** - * - * Instance of the [ably-js](https://github.com/ably/ably-js) client that was passed to the {@link constructor}. - * + * Instance of the [Ably realtime client](https://ably.com/docs/getting-started/setup) client that was passed to the {@link constructor}. */ client: Types.RealtimePromise; /** - * - * Instance of the [ably-js](https://github.com/ably/ably-js) connection, belonging to the client that was passed to the {@link constructor}. - * + * Instance of the [Ably realtime client](https://ably.com/docs/getting-started/setup) connection, belonging to the client that was passed to the {@link constructor}. */ connection: Types.ConnectionPromise; /** - * * Version of the Spaces library. - * */ readonly version = VERSION; /** - * - * Create a new instance of the Space SDK by passing an instance of the realtime, promise-based [Ably client](https://github.com/ably/ably-js): + * Create a new instance of the Spaces SDK by passing an instance of the [Ably promise-based realtime client](https://ably.com/docs/getting-started/setup). A [`clientId`](https://ably.com/docs/auth/identified-clients) is required. * - * Please note that a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascript) is required. + * An Ably API key is needed to authenticate. [Basic authentication](https://ably.com/docs/auth/basic) may be used for convenience, however Ably strongly recommends you use [token authentication](https://ably.com/docs/auth/token) in a production environment. * - * An API key will required for [basic authentication](https://ably.com/docs/auth/basic?lang=javascript). We strongly recommended that you use [token authentication](https://ably.com/docs/realtime/authentication#token-authentication) in any production environments. - * - * Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/setup?lang=javascript) for information on setting up a realtime promise client. - * - * - * @param client An instance of the realtime, promise-based Ably client from [ably-js](https://github.com/ably/ably-js). + * @param client An instance of the Ably prmise-based realtime client. */ constructor(client: Types.RealtimePromise) { this.client = client; @@ -67,42 +55,18 @@ class Spaces { } /** - * - * A `space` object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the `spaces` collection using the {@link get | `get()`} method. * - * The following restrictions apply to space names: - * - * - Avoid starting names with `[` or `:` - * - Ensure names aren’t empty - * - Exclude whitespace and wildcards, such as `*` - * - Use the correct case, whether it be uppercase or lowercase + * Create or retrieve an existing [space](https://ably.com/docs/spaces/space) from the `Spaces` collection. A space is uniquely identified by its unicode string name. * * The following is an example of creating a space: * * ```javascript * const space = await spaces.get('board-presentation'); * ``` - * - * - * - * ## Advanced properties - * - * The following sections are only relevant if you want to further customize a space, or understand more about the Spaces SDK. They aren’t required to get up and running with the basics. - * - * ### Space options - * - * An additional set of optional properties may be passed when {@link get | creating or retrieving } a space to customize the behavior of different features. * - * The following properties can be customized: + * A set of {@link SpaceOptions | options} may be passed when creating a space to customize a space. * - * | Property | Description | Type | - * |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| - * | offlineTimeout | Number of milliseconds after a member loses connection or closes their browser window to wait before they are removed from the member list. The default is 120,000ms (2 minutes). | Number | - * | cursors | A {@link CursorsOptions | cursor options} object for customizing live cursor behavior. | Object | - * | cursors.outboundBatchInterval | The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 100ms. | Number | - * | cursors.paginationLimit | The number of pages searched from history for the last published cursor position. The default is 5. | Number | - * - * The following is an example of customizing the space options when calling `spaces.get()`: + * The following is an example of setting custom `SpaceOptions`: * * ```javascript * const space = await spaces.get('board-presentation', { @@ -110,53 +74,9 @@ class Spaces { * cursors: { paginationLimit: 10 } * }); * ``` - * ### Space foundations - * - * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. - * - * A space is created as an Ably [channel](/channels). Members [attach](https://ably.com/docs/channels#attach) to the channel and join its [presence set](https://ably.com/docs/presence-occupancy/presence) when they { @link Space.enter | enter } the space. Avatar stacks, member locations and component locking are all handled on this channel. - * - * To manage the state of the space, you can monitor the [state of the underlying channel](https://ably.com/docs/channels#states). The channel object can be accessed through {@link Space.channel | `space.channel`}. - * - * The following is an example of registering a listener to wait for a channel to become attached: - * - * ```javascript - * space.channel.on('attached', (stateChange) => { - * console.log(stateChange) - * }); - * ``` - * - * > **Note** - * > - * > Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](https://ably.com/docs/channels). - * - * - * - * - * ## Cursor options - * - * Cursor options are set when creating or retrieving a {@link Space | `Space` } instance. They are used to control the behavior of live cursors. - * - * The following cursor options can be set: - * - * ### outboundBatchInterval - * - * The `outboundBatchInterval` is the interval at which a batch of cursor positions are published, in milliseconds, for each client. This is multiplied by the number of members in a space. - * - * The default value is 25ms which is optimal for the majority of use cases. If you wish to optimize the interval further, then decreasing the value will improve performance by further ‘smoothing’ the movement of cursors at the cost of increasing the number of events sent. Be aware that at a certain point the rate at which a browser is able to render the changes will impact optimizations. - * - * ### paginationLimit - * - * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members’ cursor is obtained from [history](https://ably.com/docs/storage-history/history). The `paginationLimit` is the number of pages that should be searched to find the last position of each cursor. The default is 5. - * - * - * - * - * Get or create a Space instance. Returns a {@link Space} instance. Configure the space by passing {@link SpaceOptions} as the second argument. - * * * @param name The name of the space to create or retrieve. - * @param options Additional options to customize the behaviour of the space. + * @param options Additional options to customize the behavior of the space. */ async get(name: string, options?: Subset): Promise { if (typeof name !== 'string' || name.length === 0) { From 092fca7e885538c507e8b04c6ca2632cf458127d Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:35:34 +0000 Subject: [PATCH 160/191] Consolidate docstring comments for Cursors --- src/Cursors.ts | 266 ++++++------------------------------------------- 1 file changed, 33 insertions(+), 233 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 5198470a..5760663b 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -12,7 +12,7 @@ import type { RealtimeMessage } from './utilities/types.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; /** - * The property names of `CursorsEventMap` are the names of the events emitted by { @link Cursors }. + * The property names of `CursorsEventMap` are the names of the events emitted by {@link Cursors}. */ export interface CursorsEventMap { /** @@ -24,48 +24,25 @@ export interface CursorsEventMap { const CURSORS_CHANNEL_TAG = '::$cursors'; /** - * - * The live cursors feature enables you to track the cursors of members within a space in realtime. + * [Live cursors](https://ably.com/docs/spaces/cursors) enable you to track the cursors of members within a {@link Space | space} in realtime. * * Cursor events are emitted whenever a member moves their mouse within a space. In order to optimize the efficiency and frequency of updates, cursor position events are automatically batched. The batching interval may be customized in order to further optimize for increased performance versus the number of events published. * * Live cursor updates are not available as part of the {@link Space.subscribe | space state} and must be subscribed to using {@link Cursors.subscribe | `space.cursors.subscribe()`}. * - * > **Important** - * > - * > Live cursors are a great way of providing contextual awareness as to what members are looking at within an application. However, too many cursors moving across a page can often be a distraction rather than an enhancement. As such, Ably recommends a maximum of 20 members simultaneously streaming their cursors in a space at any one time for an optimal end-user experience. - * - * - * - * - * ## Live cursor foundations - * - * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. - * - * Live cursors build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. - * - * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](https://ably.com/docs/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the {@link Space.subscribe | space state } and may only be subscribed to via the {@link Space.cursors | `cursors` } property. - * - * The channel is only created when a member calls `space.cursors.set()`. The live cursors channel object can be accessed through `space.cursors.channel`. To monitor the [underlying state of the cursors channel](https://ably.com/docs/channels#states), the channel object can be accessed through `space.cursors.channel`. - * - * - * - * - * Handles tracking of member cursors within a space. Inherits from {@link EventEmitter}. - * */ export default class Cursors extends EventEmitter { private readonly cursorBatching: CursorBatching; private readonly cursorDispensing: CursorDispensing; private readonly cursorHistory: CursorHistory; /** - * The {@link SpaceOptions.cursors | cursors options} passed to {@link default.get | `Spaces.get()`}, with default values filled in. + * The {@link SpaceOptions.cursors | cursors options} passed to {@link default.get | `Spaces.get()`}. */ readonly options: CursorsOptions; private readonly channelName: string; /** - * The [ably-js](https://github.com/ably/ably-js) realtime channel instance that this `Cursors` instance uses for transmitting and receiving data. + * The [realtime channel](https://ably.com/docs/channels) instance that this `Cursors` instance uses for transmitting and receiving data. */ public channel?: Types.RealtimeChannelPromise; @@ -84,32 +61,13 @@ export default class Cursors extends EventEmitter { } /** - * Schedules a cursor update event to be sent that will cause the following events to fire + * Set the position of a member's cursor and emit an {@link CursorUpdate | `update`} event. * - * @return {void} - * - * - * Set the position of a member’s cursor using the `set()` method. A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Calling `set()` will emit a cursor event so that other members are informed of the cursor movement in realtime. - * - * A member must have been { @link Space.enter | entered } into the space to set their cursor position. - * - * The `set()` method takes the following parameters: + * A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Data may optionally be passed containing additional information such as the color the cursor should be displayed as, or the ID of the element the member is dragging. * - * | Parameter | Description | Type | - * |------------|---------------------------------------------------------------------------------------------------------------------|--------| - * | position.x | The position of the member’s cursor on the X-axis. | Number | - * | position.y | The position of the member’s cursor on the Y-axis. | Number | - * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor, such as a color. | Object | + * A member must have been {@link Space.enter | entered} into the space to set their cursor position. * - * > **Note** - * > - * > The `data` parameter can be used to stream additional information related to a cursor’s movement, such as: - * > - * > - The color that other member’s should display a cursor as. - * > - The ID of an element that a user may be dragging for drag and drop functionality. - * > - Details of any cursor annotations. - * > - * > Be aware that as live cursor updates are batched it is not advisable to publish data unrelated to cursor position in the `data` parameter. Use a [pub/sub channel](https://ably.com/docs/channels) instead. + * @return {void} * * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the `set()` method: * @@ -118,44 +76,6 @@ export default class Cursors extends EventEmitter { * space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); * }); * ``` - * The following is an example payload of a cursor event. Cursor events are uniquely identifiable by the `connectionId` of a cursor. - * - * ```json - * { - * "hd9743gjDc": { - * "connectionId": "hd9743gjDc", - * "clientId": "clemons#142", - * "position": { - * "x": 864, - * "y": 32 - * }, - * "data": { - * "color": "red" - * } - * } - * } - * ``` - * The following are the properties of a cursor event payload: - * - * > **Moved documentation** - * > - * > This documentation has been moved to {@link CursorUpdate} and {@link CursorPosition}. - * - * - * - * - * Set the position of a cursor. If a member has not yet entered the space, this method will error. - * - * A event payload returned contains an object with 2 properties. `position` is an object with 2 required properties, `x` and `y`. These represent the position of the cursor on a 2D plane. A second optional property, `data` can also be passed. This is an object of any shape and is meant for data associated with the cursor movement (like drag or hover calculation results): - * - * Example usage: - * - * ```ts - * window.addEventListener('mousemove', ({ clientX, clientY }) => { - * space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: "red" } }); - * }); - * ``` - * * * @param cursor An object describing the cursor update that should be emitted. */ @@ -216,12 +136,11 @@ export default class Cursors extends EventEmitter { /** * {@label WITH_EVENTS} * - * - * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling `set()`. Use the `subscribe()` method on the `cursors` object of a space to receive updates. + * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member {@link Cursors.set | sets} their cursor position. * * > **Note** * > - * > The rate at which cursor events are published is controlled by the `outboundBatchInterval` property set in the {@link CursorsOptions | cursor options } of a space. + * > The rate at which cursor events are published is controlled by the {@link CursorsOptions.outboundBatchInterval | `outboundBatchInterval`} property set in the `CursorsOptions` of a space. * * The following is an example of subscribing to cursor events: * @@ -230,24 +149,9 @@ export default class Cursors extends EventEmitter { * console.log(cursorUpdate); * }); * ``` - * - * - * - * Listen to `CursorUpdate` events. See {@link EventEmitter} for overloaded usage. - * - * Available events: - * - * - ##### **update** * - * Emits an event when a new cursor position is set. The argument supplied to the event listener is a {@link CursorUpdate}. - * - * ```ts - * space.cursors.subscribe('update', (cursorUpdate: CursorUpdate) => {}); - * ``` - * - * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to add. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link CursorsEventMap} type. */ @@ -256,9 +160,9 @@ export default class Cursors extends EventEmitter { listener?: EventListener, ): void; /** - * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * Subscribe to cursor updates by registering a listener for all events. * - * @param listener The listener to add. + * @param listener An event listener. */ subscribe(listener?: EventListener): void; subscribe( @@ -289,31 +193,16 @@ export default class Cursors extends EventEmitter { /** * {@label WITH_EVENTS} * - * * Unsubscribe from cursor events to remove previously registered listeners. * - * The following is an example of removing a listener for cursor update events: + * The following is an example of removing a listener for cursor {@link CursorUpdate | `update`} events: * * ```javascript * space.cursors.unsubscribe(`update`, listener); * ``` - * Or remove all listeners: - * - * ```javascript - * space.cursors.unsubscribe(); - * ``` - * - * - * - * Remove all event listeners, all event listeners for an event, or specific listeners. See {@link EventEmitter} for detailed usage. - * - * ```ts - * space.cursors.unsubscribe('update'); - * ``` - * * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to remove. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link CursorsEventMap} type. */ @@ -322,9 +211,15 @@ export default class Cursors extends EventEmitter { listener?: EventListener, ): void; /** - * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * Unsubscribe from all events to remove previously registered listeners. * - * @param listener The listener to remove. + * The following is an example of removing a listener for all events: + * + * ```javascript + * space.cursors.unsubscribe(listener); + * ``` + * + * @param listener An event listener. */ unsubscribe(listener?: EventListener): void; unsubscribe( @@ -352,18 +247,14 @@ export default class Cursors extends EventEmitter { } /** - * - * See the documentation for {@link getAll}. * - * - * Get the last `CursorUpdate` object for self. + * Retrieve the cursor position of the current member in a one-off call. * - * Example: + * The following is an example of retrieving a member's own cursor position: * - * ```ts + * ```javascript * const selfPosition = await space.cursors.getSelf(); * ``` - * */ async getSelf(): Promise { const self = await this.space.members.getSelf(); @@ -374,18 +265,13 @@ export default class Cursors extends EventEmitter { } /** - * - * See the documentation for {@link getAll}. - * - * - * Get the last `CursorUpdate` object for everyone else but yourself. + * Retrieve the cursor position of all members other than the member themselves in a one-off call. * - * Example: + * The following is an example of retrieving the cursor positions of all members, except the member themselves: * - * ```ts + * ```javascript * const otherPositions = await space.cursors.getOthers(); * ``` - * */ async getOthers(): Promise> { const self = await this.space.members.getSelf(); @@ -398,99 +284,13 @@ export default class Cursors extends EventEmitter { } /** - * - * Cursor positions can be retrieved in one-off calls. These are local calls that retrieve the latest position of cursors retained in memory by the SDK. + * Retrieve the cursor position of all members in a one-off call. This is useful for retrieving the initial position of members's cursors. * - * The following is an example of retrieving a member’s own cursor position: - * - * ```javascript - * const myCursor = await space.cursors.getSelf(); - * ``` - * The following is an example payload returned by `space.cursors.getSelf()`: - * - * ```json - * { - * “clientId”: “DzOBJqgGXzyUBb816Oa6i”, - * “connectionId”: “__UJBKZchX”, - * "position": { - * "x": 864, - * "y": 32 - * } - * } - * ``` - * The following is an example of retrieving the cursor positions for all members other than the member themselves: - * - * ```javascript - * const othersCursors = await space.cursors.getOthers(); - * ``` - * The following is an example payload returned by `space.cursors.getOthers()`: - * - * ```json - * { - * "3ej3q7yZZz": { - * "clientId": "yyXidHatpP3hJpMpXZi8W", - * "connectionId": "3ej3q7yZZz", - * "position": { - * "x": 12, - * "y": 3 - * } - * }, - * "Z7CA3-1vlR": { - * "clientId": "b18mj5B5hm-govdFEYRyb", - * "connectionId": "Z7CA3-1vlR", - * "position": { - * "x": 502, - * "y": 43 - * } - * } - * } - * ``` - * The following is an example of retrieving the cursor positions for all members, including the member themselves. `getAll()` is useful for retrieving the initial position of members’ cursors. + * The following is an example of retrieving the cursor positions for all members: * * ```javascript * const allCursors = await space.cursors.getAll(); * ``` - * The following is an example payload returned by `space.cursors.getAll()`: - * - * ```json - * { - * "3ej3q7yZZz": { - * "clientId": "yyXidHatpP3hJpMpXZi8W", - * "connectionId": "3ej3q7yZZz", - * "position": { - * "x": 12, - * "y": 3 - * } - * }, - * "Z7CA3-1vlR": { - * "clientId": "b18mj5B5hm-govdFEYRyb", - * "connectionId": "Z7CA3-1vlR", - * "position": { - * "x": 502, - * "y": 43 - * } - * }, - * "__UJBKZchX": { - * “clientId”: “DzOBJqgGXzyUBb816Oa6i”, - * “connectionId”: “__UJBKZchX”, - * "position": { - * "x": 864, - * "y": 32 - * } - * } - * } - * ``` - * - * - * - * Get the last `CursorUpdate` object for all the members. - * - * Example: - * - * ```ts - * const allLatestPositions = await space.cursors.getAll(); - * ``` - * */ async getAll(): Promise> { const channel = this.getChannel(); From 61535dd2e1f9ceba9b47901c9fef16cdaf476190 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:35:53 +0000 Subject: [PATCH 161/191] Consolidate docstring comments for Locations --- src/Locations.ts | 258 ++++++++++++----------------------------------- 1 file changed, 64 insertions(+), 194 deletions(-) diff --git a/src/Locations.ts b/src/Locations.ts index b1483d12..8d1917e1 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -12,6 +12,38 @@ import SpaceUpdate from './SpaceUpdate.js'; export namespace LocationsEvents { /** * The data attached to an {@link LocationsEventMap.update | `update`} event. + * + * The following is an example payload of a location event: + * + * ```json + * { + * "member": { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": true, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * }, + * "location": { + * "slide": "3", + * "component": "slide-title" + * }, + * "lastEvent": { + * "name": "update", + * "timestamp": 1972395669758 + * } + * }, + * "previousLocation": { + * "slide": "2", + * "component": null + * }, + * "currentLocation": { + * "slide": "3", + * "component": "slide-title" + * } + * } + * ``` */ export interface UpdateEvent { /** @@ -19,12 +51,10 @@ export namespace LocationsEvents { */ member: SpaceMember; /** - * * The new location of the member. */ currentLocation: unknown; /** - * * The previous location of the member. */ previousLocation: unknown; @@ -32,7 +62,7 @@ export namespace LocationsEvents { } /** - * The property names of `LocationsEventMap` are the names of the events emitted by { @link Locations }. + * The property names of `LocationsEventMap` are the names of the events emitted by {@link Locations}. */ export interface LocationsEventMap { /** @@ -42,25 +72,10 @@ export interface LocationsEventMap { } /** - * - * The member location feature enables you to track where members are within a space, to see which part of your application they’re interacting with. A location could be the form field they have selected, the cell they’re currently editing in a spreadsheet, or the slide they’re viewing within a slide deck. Multiple members can be present in the same location. - * - * Member locations are used to visually display which component other members currently have selected, or are currently active on. Events are emitted whenever a member sets their location, such as when they click on a new cell, or slide. Events are received by members subscribed to location events and the UI component can be highlighted with the active member’s profile data to visually display their location. + * [Member locations](https://ably.com/docs/spaces/locations) enable you to track where members are within a {@link Space | space}, to see which part of your application they’re interacting with. A location could be the form field they have selected, the cell they’re currently editing in a spreadsheet, or the slide they’re viewing within a slide deck. Multiple members can be present in the same location. * - * + * Location events are emitted whenever a member changes their location, such as by clicking on a new cell or slide. This enables UI components to be highlighted with the active member's profile data to visually display their location. * - * - * ## Member location foundations - * - * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. - * - * Member locations build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. Members are entered into the presence set when they {@link Space.enter | enter the space}. - * - * - * - * - * Handles the tracking of member locations within a space. Inherits from {@link EventEmitter}. - * */ export default class Locations extends EventEmitter { private lastLocationUpdate: Record = {}; @@ -103,26 +118,19 @@ export default class Locations extends EventEmitter { } /** - * - * Use the `set()` method to emit a location event in realtime when a member changes their location. This will be received by all location subscribers to inform them of the location change. A `location` can be any JSON-serializable object, such as a slide number or element ID. + * Set the location of a member an emit an {@link LocationsEvents.UpdateEvent | `update`} event. * - * A member must have been { @link Space.enter | entered } into the space to set their location. + * A `location` can be any JSON-serializable object, such as a slide number or element ID. * - * The `set()` method is commonly combined with [`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) or a React [synthetic event](https://react.dev/learn/responding-to-events#adding-event-handlers), such as `onClick` or `onHover`. + * A member must have been {@link Space.enter | entered} into the space to set their location. * * The following is an example of a member setting their location to a specific slide number, and element on that slide: * * ```javascript * await space.locations.set({ slide: '3', component: 'slide-title' }); * ``` - * * - * - * Set your current location. The `location` argument can be any JSON-serializable object. Emits a `locationUpdate` event to all connected clients in this space. - * - * - * - * @param location The new location. Can be any JSON-serializable object, such as a slide number or element ID. + * @param location The member's new location. Can be any JSON-serializable object. */ async set(location: unknown) { const self = await this.space.members.getSelf(); @@ -138,14 +146,13 @@ export default class Locations extends EventEmitter { /** * {@label WITH_EVENTS} * - * - * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling {@link set}. + * Subscribe to location events by registering a listener. Location events are emitted whenever a member {@link Locations.set | changes} their location. * - * All location changes are {@link LocationsEventMap.update | `update`} events. When a location update is received, clear the highlight from the UI component of the member’s {@link LocationsEvents.UpdateEvent.previousLocation | `previousLocation`} and add it to {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation`}. + * All location changes are `update` events. When a location update is received, clear the highlight from the UI component of the member’s {@link LocationsEvents.UpdateEvent.previousLocation | `previousLocation`} and add it to {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation`}. * * > **Note** * > - * > A location update is also emitted when a member {@link Space.leave | leaves} a space. The member’s {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation` } will be `null` for these events so that any UI component highlighting can be cleared. + * > A location update is also emitted when a member is {@link MembersEventMap.remove | removed} from a space. The member’s {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation` } will be `null` for these events so that any UI component highlighting can be cleared. Member location subscription listeners only trigger on events related to members’ locations. Each event only contains the payload of the member that triggered it. Alternatively, {@link Space.subscribe | space state } can be subscribed to which returns an array of all members with their latest state every time any event is triggered. * * The following is an example of subscribing to location events: * @@ -154,65 +161,9 @@ export default class Locations extends EventEmitter { * console.log(locationUpdate); * }); * ``` - * The following is an example payload of a location event. Information about location is returned in {@link LocationsEvents.UpdateEvent.currentLocation | `currentLocation`} and {@link LocationsEvents.UpdateEvent.previousLocation | `previousLocation`}: * - * ```json - * { - * "member": { - * "clientId": "clemons#142", - * "connectionId": "hd9743gjDc", - * "isConnected": true, - * "profileData": { - * "username": "Claire Lemons", - * "avatar": "https://slides-internal.com/users/clemons.png" - * }, - * "location": { - * "slide": "3", - * "component": "slide-title" - * }, - * "lastEvent": { - * "name": "update", - * "timestamp": 1972395669758 - * } - * }, - * "previousLocation": { - * "slide": "2", - * "component": null - * }, - * "currentLocation": { - * "slide": "3", - * "component": "slide-title" - * } - * } - * ``` - * The following are the properties of a location event payload: - * - * > **Moved documentation** - * > - * > This documentation has been moved to { @link LocationsEvents.UpdateEvent }. - * - * > **Further reading** - * > - * > Member location subscription listeners only trigger on events related to members’ locations. Each event only contains the payload of the member that triggered it. Alternatively, {@link Space.subscribe | space state } can be subscribed to which returns an array of all members with their latest state every time any event is triggered. - * - * - * - * - * Listen to events for locations. See {@link EventEmitter} for overloaded usage. - * - * Available events: - * - * - ##### **update** - * - * Fires when a member updates their location. The argument supplied to the event listener is an UpdateEvent. - * - * ```ts - * space.locations.subscribe('update', (locationUpdate: LocationsEvents.UpdateEvent) => {}); - * ``` - * - * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to add. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link LocationsEventMap} type. */ @@ -221,9 +172,9 @@ export default class Locations extends EventEmitter { listener?: EventListener, ): void; /** - * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * Subscribe to location updates by registering a listener for all events. * - * @param listener The listener to add. + * @param listener An event listener. */ subscribe(listener?: EventListener): void; subscribe( @@ -246,7 +197,6 @@ export default class Locations extends EventEmitter { /** * {@label WITH_EVENTS} * - * * Unsubscribe from location events to remove previously registered listeners. * * The following is an example of removing a listener for location update events: @@ -254,23 +204,9 @@ export default class Locations extends EventEmitter { * ```javascript * space.locations.unsubscribe('update', listener); * ``` - * Or remove all listeners: * - * ```javascript - * space.locations.unsubscribe(); - * ``` - * - * - * - * Remove all event listeners, all event listeners for an event, or specific listeners. See {@link EventEmitter} for detailed usage. - * - * ```ts - * space.locations.unsubscribe('update'); - * ``` - * - * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to remove. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link LocationsEventMap} type. */ @@ -279,9 +215,15 @@ export default class Locations extends EventEmitter { listener?: EventListener, ): void; /** - * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but unsubscribes from _all_ events. + * Unsubscribe from all events to remove previously registered listeners. + * + * The following is an example of removing a listener for all events: + * + * ```javascript + * space.locations.unsubscribe(listener); + * ``` * - * @param listener The listener to remove. + * @param listener An event listener. */ unsubscribe(listener?: EventListener): void; unsubscribe( @@ -302,18 +244,13 @@ export default class Locations extends EventEmitter { } /** - * - * See the documentation for {@link getAll}. + * Retrieve the location of the current member in a one-off call. * - * - * Get location for self. + * The following is an example of retrieving a member's own location: * - * Example: - * - * ```ts + * ```javascript * const myLocation = await space.locations.getSelf(); * ``` - * */ async getSelf(): Promise { const self = await this.space.members.getSelf(); @@ -321,18 +258,13 @@ export default class Locations extends EventEmitter { } /** - * - * See the documentation for {@link getAll}. + * Retrieve the locations of all members other than the member themselves in a one-off call. * - * - * Get location for other members + * The following is an example of retrieving the locations of all members, except the member themselves: * - * Example: - * - * ```ts + * ```javascript * const otherLocations = await space.locations.getOthers() * ``` - * */ async getOthers(): Promise> { const members = await this.space.members.getOthers(); @@ -344,75 +276,13 @@ export default class Locations extends EventEmitter { } /** - * - * Member locations can also be retrieved in one-off calls. These are local calls and retrieve the location of members retained in memory by the SDK. - * - * The following is an example of retrieving a member’s own location: - * - * ```javascript - * const myLocation = await space.locations.getSelf(); - * ``` - * The following is an example payload returned by `space.locations.getSelf()`. It will return the properties of the member’s `location`: - * - * ```json - * { - * "slide": "3", - * "component": "slide-title" - * } - * ``` - * The following is an example of retrieving the location objects of all members other than the member themselves. - * - * ```javascript - * const othersLocations = await space.locations.getOthers(); - * ``` - * The following is an example payload returned by `space.locations.getOthers()`: It will return the properties of all member’s `location` by their `connectionId`: + * Retrieve the location of all members in a one-off call. * - * ```json - * { - * "xG6H3lnrCn": { - * "slide": "1", - * "component": "textBox-1" - * }, - * "el29SVLktW": { - * "slide": "1", - * "component": "image-2" - * } - * } - * ``` - * The following is an example of retrieving the location objects of all members, including the member themselves: + * The following is an example of retrieving the locations of all members: * * ```javascript * const allLocations = await space.locations.getAll(); * ``` - * The following is an example payload returned by `space.locations.getAll()`. It will return the properties of all member’s `location` by their `connectionId`: - * - * ```json - * { - * "xG6H3lnrCn": { - * "slide": "1", - * "component": "textBox-1" - * }, - * "el29SVLktW": { - * "slide": "1", - * "component": "image-2" - * }, - * "dieF3291kT": { - * "slide": "3", - * "component": "slide-title" - * } - * } - * ``` - * - * - * - * Get location for all members. - * - * Example: - * - * ```ts - * const allLocations = await space.locations.getAll(); - * ``` - * */ async getAll(): Promise> { const members = await this.space.members.getAll(); From 33127296622df5a4126f752e3dcb431eeb4540a4 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:36:09 +0000 Subject: [PATCH 162/191] Consolidate docstring comments for Locks --- src/Locks.ts | 283 ++++++++------------------------------------------- 1 file changed, 40 insertions(+), 243 deletions(-) diff --git a/src/Locks.ts b/src/Locks.ts index 2b4e1c32..2a20b4f9 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -9,14 +9,12 @@ import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from import SpaceUpdate from './SpaceUpdate.js'; /** - * * Additional attributes that can be set when acquiring a lock. - * */ export type LockAttributes = Record; /** - * Options for customizing the behaviour of {@link Locks.get | `Locks.get()`}. + * Options for customizing the behavior of {@link Locks.get | `Locks.get()`}. */ export interface LockOptions { /** @@ -26,7 +24,7 @@ export interface LockOptions { } /** - * The property names of `LocksEventMap` are the names of the events emitted by { @link Locks }. + * The property names of `LocksEventMap` are the names of the events emitted by {@link Locks}. */ export interface LocksEventMap { /** @@ -36,26 +34,11 @@ export interface LocksEventMap { } /** - * - * The component locking feature enables members to optimistically lock stateful UI components before editing them. This reduces the chances of conflicting changes being made to the same component by different members. A component could be a cell in a spreadsheet that a member is updating, or an input field on a form they’re filling in. + * [Component locking](https://ably.com/docs/spaces/locking) enables members to optimistically lock stateful UI components before editing them. This reduces the chances of conflicting changes being made to the same component by different members. A component could be a cell in a spreadsheet that a member is updating, or an input field on a form they’re filling in. * * Once a lock has been acquired by a member, the component that it relates to can be updated in the UI to visually indicate to other members that it is locked and and which member has the lock. The component can then be updated once the editing member has released the lock to indicate that it is now unlocked. * - * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three { @link LockStatus | states } and may only transition between states in specific circumstances. - * - * > **Important** - * > - * > Optimistic locking means that there is a chance that two members may begin editing the same UI component before it is confirmed which member holds the lock. On average, the time taken to reconcile which member holds a lock is in the hundreds of milliseconds. Your application needs to handle the member that successfully obtained the lock, as well as the member that had their request invalidated. - * - * ## Lock states - * - * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call {@link acquire | `acquire()` } on the lock identifier related to it. Alternatively, you could wait until they receive a `locked` event and display a spinning symbol in the UI until this is received. In either case a subsequent `unlocked` event may invalidate that member’s lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. - * - * A lock will be in one of the following states: - * - * > **Moved documentation** - * > - * > This documentation has been moved to { @link LockStatuses }. + * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three {@link LockStatus | states} and may only transition between states in specific circumstances. * * The following lock state transitions may occur: * @@ -65,13 +48,12 @@ export interface LocksEventMap { * - `locked` → `unlocked`: the lock was either explicitly {@link release | released} by the member, or their request was invalidated by a concurrent request which took precedence. * - `unlocked` → `locked`: the requesting member reacquired a lock they previously held. * - * Only transitions that result in a `locked` or `unlocked` status will emit a lock event that members can {@link subscribe | `subscribe()` } to. + * Only lock transitions that result in a `locked` or `unlocked` status will emit a lock {@link LocksEventMap.update | update} event. * - * + * > **Note** + * > + * > Optimistic locking means that there is a chance that two members may begin editing the same UI component before it is confirmed which member holds the lock. On average, the time taken to reconcile which member holds a lock is in the hundreds of milliseconds. Your application needs to handle the member that successfully obtained the lock, as well as the member that had their request invalidated. * - * - * Provides a mechanism to "lock" a component, reducing the chances of conflict in an application whilst being edited by multiple members. Inherits from {@link EventEmitter}. - * */ export default class Locks extends EventEmitter { // locks tracks the local state of locks, which is used to determine whether @@ -92,8 +74,7 @@ export default class Locks extends EventEmitter { } /** - * - * Use the `get()` method to query whether a lock is currently locked, and by which member if it is. The lock is identifiable by its unique string ID. + * Query whether a lock is currently locked, and if locked, return which member holds the lock. A lock is identifiable by its unique string ID. * * The following is an example of checking whether a lock identifier is currently locked: * @@ -111,20 +92,8 @@ export default class Locks extends EventEmitter { * const { request } = space.locks.get(id); * const viewLock = request.attributes.get(key); * ``` - * If the lock is not currently held by a member, `get()` will return `undefined`. Otherwise it will return the most recent lock event for the lock. - * - * * - * - * Get a lock by its id. - * - * Example: - * - * ```ts - * const id = "/slide/1/element/3"; - * const lock = space.locks.get(id); - * ``` - * + * If the lock is not currently held by a member, `get()` will return `undefined`. * * @param id A unique identifier which specifies the lock to query. */ @@ -142,69 +111,13 @@ export default class Locks extends EventEmitter { // This will be async in the future, when pending requests are no longer processed // in the library. /** - * - * Locks can also be retrieved in one-off calls. These are local calls and retrieve the locks retained in memory by the SDK. + * Retrieve all locks that are currently held in a one-off call. This is a local call that retrieves the latest locks retained in memory by the SDK. * - * The following is an example of retrieving an array of all currently held locks in a space: + * The following is an example of retrieving an array of all currently held locks: * * ```javascript * const allLocks = await space.locks.getAll(); * ``` - * The following is an example payload returned by `space.locks.getAll()`: - * - * ```json - * [ - * { - * "id": "s1-c2", - * "status": "locked", - * "timestamp": 1247525627533, - * "member": { - * "clientId": "amint#5", - * "connectionId": "hg35a4fgjAs", - * "isConnected": true, - * "lastEvent": { - * "name": "update", - * "timestamp": 173459567340 - * }, - * "location": null, - * "profileData": { - * "username": "Arit Mint", - * "avatar": "https://slides-internal.com/users/amint.png" - * } - * } - * }, - * { - * "id": "s3-c4", - * "status": "locked", - * "timestamp": 1247115627423, - * "member": { - * "clientId": "torange#1", - * "connectionId": "tt7233ghUa", - * "isConnected": true, - * "lastEvent": { - * "name": "update", - * "timestamp": 167759566354 - * }, - * "location": null, - * "profileData": { - * "username": "Tara Orange", - * "avatar": "https://slides-internal.com/users/torange.png" - * } - * } - * } - * ] - * ``` - * - * - * - * Get all locks that have the `locked` status. - * - * Example: - * - * ```ts - * const locks = await space.locks.getAll(); - * ``` - * */ async getAll(): Promise { const allLocks: Lock[] = []; @@ -222,18 +135,13 @@ export default class Locks extends EventEmitter { } /** - * - * See the documentation for {@link getAll}. - * - * - * Get all locks belonging to self that have the `locked` status. + * Retrieve all locks that are currently held by the current member in a one-off call. This is a local call that retrieves the latest locks retained in memory by the SDK. * - * Example: + * The following is an example of retrieving all locks held by the member: * - * ```ts + * ```javascript * const locks = await space.locks.getSelf(); * ``` - * */ async getSelf(): Promise { const self = await this.space.members.getSelf(); @@ -244,18 +152,13 @@ export default class Locks extends EventEmitter { } /** - * - * See the documentation for {@link getAll}. + * Retrieve all locks that are currently held by members except the member itself, in a one-off call. This is a local call that retrieves the latest locks retained in memory by the SDK. * - * - * Get all locks belonging to all members except self that have the `locked` status. + * The following is an example of retrieving all locks held by other members: * - * Example: - * - * ```ts + * ```javascript * const locks = await space.locks.getOthers(); * ``` - * */ async getOthers(): Promise { const self = await this.space.members.getSelf(); @@ -267,8 +170,7 @@ export default class Locks extends EventEmitter { } /** - * - * Use the `acquire()` method to attempt to acquire a lock with a given unique ID. Additional `attributes` may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using `attributes` is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. + * Attempt to acquire a lock with a given unique ID. Additional `attributes` may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using `attributes` is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. * * A member must have been {@link Space.enter | entered } into the space to acquire a lock. * @@ -277,41 +179,17 @@ export default class Locks extends EventEmitter { * ```javascript * const acquireLock = await space.locks.acquire(id); * ``` + * * The following is an example of passing a set of `attributes` when trying to acquire a lock: * * ```javascript - * const lockAttributes = new Map(); - * lockAttributes.set('component', 'cell-d3'); + * const lockAttributes = { 'component': 'cell-d3' }; * const acquireLock = await space.locks.acquire(id, { lockAttributes }); * ``` - * The following is an example payload returned by `space.locks.acquire()`. The promise will resolve to a lock request with the `pending` status: - * - * ```json - * { - * "id": "s2-d14", - * "status": "pending", - * "timestamp": 1247525689781, - * "attributes": { - * "componentId": "cell-d14" - * } - * } - * ``` - * Once a member requests a lock by calling `acquire()`, the lock is temporarily in the {@link LockStatuses.Pending | pending state }. An event will be emitted based on whether the lock request was successful (a status of `locked`) or invalidated (a status of `unlocked`). This can be {@link subscribe | subscribed } to in order for the client to know whether their lock request was successful or not. - * - * - * - * - * Send a request to acquire a lock. Returns a Promise which resolves once the request has been sent. A resolved Promise holds a `pending` {@link Lock}. An error will be thrown if a lock request with a status of `pending` or `locked` already exists, returning a rejected promise. - * - * When a lock acquisition by a member is confirmed with the `locked` status, an `update` event will be emitted. Hence to handle lock acquisition, `acquire()` needs to always be used together with `subscribe()`. * - * Example: + * Once a member requests a lock by calling `acquire()`, the lock is temporarily in the {@link LockStatuses.Pending | pending state }. An event will be emitted based on whether the lock request was successful (a status of {@link LockStatuses.Locked | `locked`}), or invalidated (a status of {@link LockStatuses.Unlocked | `unlocked`}). * - * ```ts - * const id = "/slide/1/element/3"; - * const lockRequest = await space.locks.acquire(id); - * ``` - * + * An error will be thrown if a lock request with a status of {@link LockStatuses.Pending | `pending`} or {@link LockStatuses.Locked | `locked`} already exists, returning a rejected promise. * * @param id A unique identifier which specifies the lock to acquire. * @param opts An object whose {@link LockOptions.attributes | `attributes`} property specifies additional metadata to associate with the lock. @@ -350,32 +228,18 @@ export default class Locks extends EventEmitter { } /** - * - * Use the `release()` method to explicitly release a lock once a member has finished editing the related component. For example, the `release()` method can be called once a user clicks outside of the component, such as clicking on another cell within a spreadsheet. Any UI indications that the previous cell was locked can then be cleared. + * Release a lock once a member has finished editing the related component. For example, call `release()` once a user clicks outside of the component, such as clicking on another cell within a spreadsheet. Any UI indications that the previous cell was locked can then be cleared. * * The following is an example of releasing a lock: * * ```javascript * await space.locks.release(id); * ``` - * Releasing a lock will emit a lock event with a {@link LockStatuses | lock status } of `unlocked`. + * Releasing a lock will emit a lock event with an {@link LockStatuses.Unlocked | `unlocked `} status. * * > **Note** * > - * > When a member {@link Space.leave | leaves } a space, their locks are automatically released. - * - * - * - * - * Releases a previously requested lock. - * - * Example: - * - * ```ts - * const id = "/slide/1/element/3"; - * await space.locks.release(id); - * ``` - * + * > When a member is {@link MembersEventMap.remove | removed } from a space, their locks are automatically released. * * @param id A unique identifier which specifies the lock to release. */ @@ -398,8 +262,6 @@ export default class Locks extends EventEmitter { /** * {@label WITH_EVENTS} - * - * * Subscribe to lock events by registering a listener. Lock events are emitted whenever the {@link LockStatuses | lock state } transitions into `locked` or `unlocked`. * * All lock events are `update` events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. @@ -411,73 +273,17 @@ export default class Locks extends EventEmitter { * console.log(lock); * }); * ``` - * The following is an example payload of a lock event: - * - * ```json - * { - * "id": "s2-d14", - * "status": "unlocked", - * "timestamp": 1247525689781, - * "attributes": { - * "componentId": "cell-d14" - * }, - * "reason": { - * "message": "lock is currently locked", - * "code": 101003, - * "statusCode": 400 - * }, - * "member": { - * "clientId": "smango", - * "connectionId": "hs343gjsdc", - * "isConnected": true, - * "profileData": { - * "username": "Saiorse Mango" - * }, - * "location": { - * "slide": "sheet-2", - * "component": "d-14" - * }, - * "lastEvent": { - * "name": "update", - * "timestamp": 1247525689781 - * } - * } - * } - * ``` - * The following are the properties of a lock event payload: - * - * > **Moved documentation** - * > - * > This documentation has been moved to { @link Lock }. - * - * - * - * - * Listen to lock events. See {@link EventEmitter} for overloaded usage. - * - * Available events: * - * - ##### **update** - * - * Listen to changes to locks. - * - * ```ts - * space.locks.subscribe('update', (lock: Lock) => {}) - * ``` - * - * The argument supplied to the callback is a {@link Lock}, representing the lock request and it's status. - * - * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to add. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link LocksEventMap} type. */ subscribe(eventOrEvents: K | K[], listener?: EventListener): void; /** - * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * Subscribe to lock updates by registering a listener for all events. * - * @param listener The listener to add. + * @param listener An event listener. */ subscribe(listener?: EventListener): void; subscribe( @@ -500,7 +306,6 @@ export default class Locks extends EventEmitter { /** * {@label WITH_EVENTS} * - * * Unsubscribe from lock events to remove previously registered listeners. * * The following is an example of removing a listener for lock update events: @@ -508,31 +313,23 @@ export default class Locks extends EventEmitter { * ```javascript * space.locks.unsubscribe('update', listener); * ``` - * Or remove all listeners: - * - * ```javascript - * space.locks.unsubscribe(); - * ``` - * * - * - * Remove all event listeners, all event listeners for an event, or specific listeners. See {@link EventEmitter} for detailed usage. - * - * ```ts - * space.locks.unsubscribe('update'); - * ``` - * - * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to remove. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link LocksEventMap} type. */ unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; /** - * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but unsubscribes from _all_ events. + * Unsubscribe from all events to remove previously registered listeners. + * + * The following is an example of removing a listener for all events: + * + * ```javascript + * space.locks.unsubscribe(listener); + * ``` * - * @param listener The listener to remove. + * @param listener An event listener. */ unsubscribe(listener?: EventListener): void; unsubscribe( From d4cdc3c8eb14c23e4c31d2b85d391581866f8a4c Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:36:29 +0000 Subject: [PATCH 163/191] Consolidate docstring comments for Members --- src/Members.ts | 361 +++++-------------------------------------------- 1 file changed, 31 insertions(+), 330 deletions(-) diff --git a/src/Members.ts b/src/Members.ts index 40986fa0..4eebfe44 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -6,21 +6,18 @@ import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; /** - * The property names of `MembersEventMap` are the names of the events emitted by { @link Members }. + * The property names of `MembersEventMap` are the names of the events emitted by {@link Members}. */ export interface MembersEventMap { /** - * - * A member has left the space. The member has either left explicitly by calling { @link Space.leave | `space.leave()` }, or has abruptly disconnected and not re-established a connection within 15 seconds. + * A member has left the space. The member has either left explicitly by calling {@link Space.leave | `space.leave()`}, or has abruptly disconnected and not re-established a connection within 15 seconds. */ leave: SpaceMember; /** - * - * A new member has entered the space. The member has either entered explicitly by calling {@link Space.enter | `space.enter()` }, or has attempted to update their profile data before entering a space, which will instead emit an `enter` event. + * A new member has entered the space. The member has either entered explicitly by calling {@link Space.enter | `space.enter()`}, or has attempted to update their profile data before entering a space, which will instead emit an `enter` event. */ enter: SpaceMember; /** - * * This event is emitted whenever any one of the following events is emitted: * * - {@link enter} @@ -30,49 +27,18 @@ export interface MembersEventMap { */ update: SpaceMember; /** - * - * A member has updated their profile data by calling { @link Space.updateProfileData | `space.updateProfileData()` }. + * A member has updated their profile data by calling {@link Space.updateProfileData | `space.updateProfileData()`}. */ updateProfile: SpaceMember; /** - * - * A member has been removed from the members list after the { @link SpaceOptions.offlineTimeout | `offlineTimeout` } period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their `leave` and `remove` events. + * A member has been removed from the members list after the {@link SpaceOptions.offlineTimeout | `offlineTimeout`} period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their `leave` and `remove` events. */ remove: SpaceMember; } /** - * - * Avatar stacks are the most common way of showing the online status of members in an application by displaying an avatar for each member. Events are emitted whenever a member enters or leaves a space, or updates their profile data. Additional information can also be provided, such as a profile picture and email address. + * [Avatar stacks](https://ably.com/docs/spaces/avatar) are the most common way of showing the online status of members in an application by displaying an avatar for each member. {@link MembersEventMap | Events} are emitted whenever a member enters or leaves a space, or updates their profile data. Additional information can also be provided, such as a profile picture and email address. * - * Subscribe to the space’s { @link Space.members | `members` } property in order to keep your avatar stack updated in realtime. - * - * ## Event types - * - * The following four event types are emitted by members: - * - * > **Moved documentation** - * > - * > This documentation has been moved to { @link MembersEventMap }. - * - * > **Note** - * > - * > Members {@link Space.enter | enter }, {@link Space.leave | leave }, and {@link Space.updateProfileData | update } a {@link Space | space } directly. The space’s { @link Space.members | `members` } property is used to subscribe to these updates. - * - * - * - * - * ## Avatar stack foundations - * - * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. - * - * Avatar stacks build upon the functionality of the Pub/Sub Channels [presence](https://ably.com/docs/presence-occupancy/presence) feature. Members are entered into the presence set when they { @link Space.enter | enter the space }. - * - * - * - * - * Handles members within a space. - * */ class Members extends EventEmitter { private lastMemberUpdate: Record = {}; @@ -113,153 +79,26 @@ class Members extends EventEmitter { } /** - * - * See the documentation for {@link getAll}. - * - * - * Returns a Promise which resolves to the {@link SpaceMember} object relating to the local connection. Will resolve to `null` if the client hasn't entered the space yet. + * Retrieve the current member's own member object in a one-off call. * - * Example: + * The following is an example of retrieving a member's own member object: * - * ```ts - * const myMember = await space.members.getSelf(); + * ```javascript + * const myMemberInfo = await space.members.getSelf(); * ``` - * */ async getSelf(): Promise { return this.space.connectionId ? await this.getByConnectionId(this.space.connectionId) : null; } /** - * - * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member’s own profile data to them, or retrieving a list of all other members to use to {@link Space.updateProfileData | update their profile data }. + * Retrieve the member objects of all members within the space in a one-off call. * - * The following is an example of retrieving a member’s own member object: - * - * ```javascript - * const myMemberInfo = await space.members.getSelf(); - * ``` - * The following is an example payload returned by `space.members.getSelf()`: - * - * ```json - * { - * "clientId": "clemons#142", - * "connectionId": "hd9743gjDc", - * "isConnected": true, - * "lastEvent": { - * "name": "enter", - * "timestamp": 1677595689759 - * }, - * "location": null, - * "profileData": { - * "username": "Claire Lemons", - * "avatar": "https://slides-internal.com/users/clemons.png" - * } - * } - * ``` - * The following is an example of retrieving an array of member objects for all members other than the member themselves. Ths includes members that have recently left the space, but have not yet been removed. - * - * ```javascript - * const othersMemberInfo = await space.members.getOthers(); - * ``` - * The following is an example payload returned by `space.members.getOthers()`: - * - * ```json - * [ - * { - * "clientId": "torange#1", - * "connectionId": "tt7233ghUa", - * "isConnected": true, - * "lastEvent": { - * "name": "enter", - * "timestamp": 167759566354 - * }, - * "location": null, - * "profileData": { - * "username": "Tara Orange", - * "avatar": "https://slides-internal.com/users/torange.png" - * } - * }, - * { - * "clientId": "amint#5", - * "connectionId": "hg35a4fgjAs", - * "isConnected": true, - * "lastEvent": { - * "name": "update", - * "timestamp": 173459567340 - * }, - * "location": null, - * "profileData": { - * "username": "Arit Mint", - * "avatar": "https://slides-internal.com/users/amint.png" - * } - * } - * ] - * ``` - * The following is an example of retrieving an array of all member objects, including the member themselves. Ths includes members that have recently left the space, but have not yet been removed. + * The following is an example of retrieving all members: * * ```javascript * const allMembers = await space.members.getAll(); * ``` - * The following is an example payload returned by `space.members.getAll()`: - * - * ```json - * [ - * { - * "clientId": "clemons#142", - * "connectionId": "hd9743gjDc", - * "isConnected": false, - * "lastEvent": { - * "name": "enter", - * "timestamp": 1677595689759 - * }, - * "location": null, - * "profileData": { - * "username": "Claire Lemons", - * "avatar": "https://slides-internal.com/users/clemons.png" - * } - * }, - * { - * "clientId": "amint#5", - * "connectionId": "hg35a4fgjAs", - * "isConnected": true, - * "lastEvent": { - * "name": "update", - * "timestamp": 173459567340 - * }, - * "location": null, - * "profileData": { - * "username": "Arit Mint", - * "avatar": "https://slides-internal.com/users/amint.png" - * } - * }, - * { - * "clientId": "torange#1", - * "connectionId": "tt7233ghUa", - * "isConnected": true, - * "lastEvent": { - * "name": "enter", - * "timestamp": 167759566354 - * }, - * "location": null, - * "profileData": { - * "username": "Tara Orange", - * "avatar": "https://slides-internal.com/users/torange.png" - * } - * } - * ] - * ``` - * - * - * - * Returns a Promise which resolves to an array of all {@link SpaceMember} objects (members) currently in the space, including any who have left and not yet timed out. (_see: {@link SpaceOptions.offlineTimeout | offlineTimeout}_) - * - * Example: - * - * ```ts - * const allMembers = await space.members.getAll(); - * ``` - * */ async getAll(): Promise { const presenceMembers = await this.space.channel.presence.get(); @@ -268,18 +107,13 @@ class Members extends EventEmitter { } /** - * - * See the documentation for {@link getAll}. - * - * - * Returns a Promise which resolves to an array of all {@link SpaceMember} objects (members) currently in the space, excluding your own member object. + * Retrieve the member objects of all members within the space other than the member themselves, in a one-off call. * - * Example: + * The following is an example of retrieving all members other than the member themselves: * - * ```ts - * const otherMembers = await space.members.getOthers(); + * ```javascript + * const othersMemberInfo = await space.members.getOthers(); * ``` - * */ async getOthers(): Promise { const members = await this.getAll(); @@ -289,146 +123,34 @@ class Members extends EventEmitter { /** * {@label WITH_EVENTS} * - * - * Subscribe to members’ online status and profile updates by registering a listener. Member events are emitted whenever a member {@link Space.enter | enters} or {@link Space.leave | leaves} the space, or updates their profile data. Use the `subscribe()` method on the `members` object of a space to receive updates. + * Subscribe to members’ online status and profile updates by registering a listener. Member events are emitted whenever a member {@link Space.enter | enters} or {@link Space.leave | leaves} the space, or {@link Space.updateProfileData | updates their profile data}. * - * The following is an example of subscribing to the different member event types: + * The following is an example of subscribing to enter events: * * ```javascript - * // Subscribe to member enters in a space * space.members.subscribe('enter', (memberUpdate) => { * console.log(memberUpdate); * }); - * - * // Subscribe to member profile data updates in a space - * space.members.subscribe('update', (memberUpdate) => { - * console.log(memberUpdate); - * }); - * - * // Subscribe to member leaves in a space - * space.members.subscribe('leave', (memberUpdate) => { - * console.log(memberUpdate); - * }); - * - * // Subscribe to member removals in a space - * space.members.subscribe('remove', (memberUpdate) => { - * console.log(memberUpdate); - * }); * ``` + * * It’s also possible to subscribe to multiple event types with the same listener by using an array: * * ```javascript - * space.members.subscribe(['enter', 'update'], (memberUpdate) => { + * space.members.subscribe(['enter', 'leave'], (memberUpdate) => { * console.log(memberUpdate); * }); * ``` + * * Or subscribe to all event types: * * ```javascript - * space.members.subscribe((memberUpdate) => { + * space.members.subscribe('update', (memberUpdate) => { * console.log(memberUpdate); * }); * ``` - * The following is an example payload of a member event. The `lastEvent.name` describes which {@link SpaceEventMap | event type } a payload relates to. - * - * ```json - * { - * "clientId": "clemons#142", - * "connectionId": "hd9743gjDc", - * "isConnected": true, - * "lastEvent": { - * "name": "enter", - * "timestamp": 1677595689759 - * }, - * "location": null, - * "profileData": { - * "username": "Claire Lemons", - * "avatar": "https://slides-internal.com/users/clemons.png" - * } - * } - * ``` - * The following are the properties of a member event payload: - * - * | Property | Description | Type | - * |---------------------|-------------------------------------------------------------------------------------------------------------------|---------| - * | clientId | The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. | String | - * | connectionId | The unique identifier of the member’s [connection](https://ably.com/docs/connect). | String | - * | isConnected | Whether the member is connected to Ably or not. | Boolean | - * | profileData | The optional {@link Space.updateProfileData | profile data } associated with the member. | Object | - * | location | The current {@link Locations | location} of the member. Will be `null` for `enter`, `leave` and `remove` events. | Object | - * | lastEvent.name | This documentation has been moved to {@link SpaceMember}. | String | - * | lastEvent.timestamp | This documentation has been moved to {@link SpaceMember}. | Number | - * - * > **Further reading** - * > - * > Avatar stack subscription listeners only trigger on events related to members’ online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, {@link Space.subscribe | space state } can be subscribed to which returns an array of all members with their latest state every time any event is triggered. - * - * - * - * - * Listen to member events for the space. See {@link EventEmitter} for overloaded usage. - * - * The argument supplied to the callback is the {@link SpaceMember} object representing the member that triggered the event. - * - * Example: - * - * ```ts - * space.members.subscribe((member: SpaceMember) => {}); - * ``` - * - * Available events: - * - * - ##### **enter** - * - * Listen to enter events of members. - * - * ```ts - * space.members.subscribe('enter', (member: SpaceMember) => {}) - * ``` - * The argument supplied to the callback is a {@link SpaceMember} object representing the member entering the space. - * - * - ##### **leave** - * - * Listen to leave events of members. The leave event will be issued when a member calls `space.leave()` or is disconnected. - * - * ```ts - * space.members.subscribe('leave', (member: SpaceMember) => {}) - * ``` * - * The argument supplied to the callback is a {@link SpaceMember} object representing the member leaving the space. - * - * - ##### **remove** - * - * Listen to remove events of members. The remove event will be triggered when the {@link SpaceOptions.offlineTimeout | offlineTimeout } has passed. - * - * ```ts - * space.members.subscribe('remove', (member: SpaceMember) => {}) - * ``` - * - * The argument supplied to the callback is a {@link SpaceMember} object representing the member removed from the space. - * - * - ##### **updateProfile** - * - * Listen to profile update events of members. - * - * ```ts - * space.members.subscribe('updateProfile', (member: SpaceMember) => {}) - * ``` - * The argument supplied to the callback is a {@link SpaceMember} object representing the member entering the space. - * - * - ##### **update** - * - * Listen to `enter`, `leave`, `updateProfile` and `remove` events. - * - * ```ts - * space.members.subscribe('update', (member: SpaceMember) => {}) - * ``` - * - * The argument supplied to the callback is a {@link SpaceMember} object representing the member affected by the change. - * - * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to add. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link MembersEventMap} type. */ @@ -437,9 +159,9 @@ class Members extends EventEmitter { listener?: EventListener, ): void; /** - * Behaves the same as { @link subscribe:WITH_EVENTS | the overload which accepts one or more event names }, but subscribes to _all_ events. + * Subscribe to members’ online status and profile updates by registering a listener for all event types. * - * @param listener The listener to add. + * @param listener An event listener. */ subscribe(listener?: EventListener): void; subscribe( @@ -462,7 +184,6 @@ class Members extends EventEmitter { /** * {@label WITH_EVENTS} * - * * Unsubscribe from member events to remove previously registered listeners. * * The following is an example of removing a listener for one member event type: @@ -470,35 +191,15 @@ class Members extends EventEmitter { * ```javascript * space.members.unsubscribe('enter', listener); * ``` + * * It’s also possible to remove listeners for multiple member event types: * * ```javascript * space.members.unsubscribe(['enter', 'leave'], listener); * ``` - * Or remove all listeners: - * - * ```javascript - * space.members.unsubscribe(); - * ``` - * - * - * - * Remove all the event listeners or specific listeners. See {@link EventEmitter} for detailed usage. - * - * ```ts - * // Unsubscribe from all events - * space.members.unsubscribe(); - * - * // Unsubscribe from enter events - * space.members.unsubscribe('enter'); - * - * // Unsubscribe from leave events - * space.members.unsubscribe('leave'); - * ``` - * * - * @param eventOrEvents The event name or an array of event names. - * @param listener The listener to remove. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of the {@link MembersEventMap} type. */ @@ -507,9 +208,9 @@ class Members extends EventEmitter { listener?: EventListener, ): void; /** - * Behaves the same as { @link unsubscribe:WITH_EVENTS | the overload which accepts one or more event names }, but unsubscribes from _all_ events. + * Unsubscribe from all events to remove previously registered listeners. * - * @param listener The listener to remove. + * @param listener An event listener. */ unsubscribe(listener?: EventListener): void; unsubscribe( From b57ea839f883de2aa893c37da008e934f57f0beb Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:36:54 +0000 Subject: [PATCH 164/191] Consolidate docstring comments for types --- src/types.ts | 130 +++++++++++++++++++++++---------------------------- 1 file changed, 58 insertions(+), 72 deletions(-) diff --git a/src/types.ts b/src/types.ts index 73511128..afdfff5a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,151 +2,149 @@ import { Types } from 'ably'; import type { LockAttributes } from './Locks.js'; /** - * Options to configure the behaviour of a {@link Cursors | `Cursors`} instance. + * Options to configure the behavior of a {@link Cursors | `Cursors`} instance. */ export interface CursorsOptions { /** - * - * The interval in milliseconds at which a batch of cursor positions are published. This is multiplied by the number of members in the space minus 1. The default value is 25ms. - * + * The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 25ms. Decreasing the value will improve performance by further ‘smoothing’ the movement of cursors at the cost of increasing the number of events sent. */ outboundBatchInterval: number; /** - * * The number of pages searched from [history](https://ably.com/docs/storage-history/history) for the last published cursor position. The default is 5. - * */ paginationLimit: number; } /** - * * Represents a cursors position. - * */ export interface CursorPosition { /** - * * The position of the member’s cursor on the X-axis. */ x: number; /** - * * The position of the member’s cursor on the Y-axis. */ y: number; } /** - * - * Represent data that can be associated with a cursor update. - * + * Represents data that can be associated with a cursor update. A JSON-serializable object containing additional information about the cursor, such as a color or the ID of an element the cursor is dragging. */ export type CursorData = Record; /** - * - * Represents an update to a cursor. - * + * Represents a cursor update event. + * + * The following is an example payload of a cursor event. Cursor events are uniquely identifiable by the {@link CursorUpdate.connectionId | `connectionId`} of a cursor. + * + * ```json + * { + * "hd9743gjDc": { + * "connectionId": "hd9743gjDc", + * "clientId": "clemons#142", + * "position": { + * "x": 864, + * "y": 32 + * }, + * "data": { + * "color": "red" + * } + * } + * } + * ``` */ export interface CursorUpdate { /** - * * The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. */ clientId: string; /** - * * The unique identifier of the member’s [connection](https://ably.com/docs/connect). */ connectionId: string; /** - * * An object containing the position of a member’s cursor. */ position: CursorPosition; /** - * * An optional arbitrary JSON-serializable object containing additional information about the cursor. */ data?: CursorData; } /** - * - * Used to configure a Space instance on creation. - * + * Options to configure a {@link Space | Space} instance on its creation. */ export interface SpaceOptions { /** - * * Number of milliseconds after a user loses connection or closes their browser window to wait before their {@link SpaceMember} object is removed from the members list. The default is 120000ms (2 minutes). - * */ offlineTimeout: number; /** - * - * Options relating to configuring the cursors API (see below). - * + * Options to configure live cursors behavior. */ cursors: CursorsOptions; } /** - * - * Profile data can be set when {@link Space.enter | entering } a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. + * Optional data that is associated with a member. Examples include a preferred username, or a profile picture that can be subsequently displayed in their avatar. Can be any arbitrary JSON-serializable object. */ export type ProfileData = Record | null; /** - * - * A SpaceMember represents a member within a Space instance. Each new connection that enters will create a new member, even if they have the same [`clientId`](https://ably.com/docs/auth/identified-clients?lang=javascript). - * + * Represents a member within a Space instance. Each new connection that enters will create a new member, even if they have the same [`clientId`](https://ably.com/docs/auth/identified-clients). + * + * The following is an example payload of a `SpaceMember`: + * + * ```json + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": false, + * "lastEvent": { + * "name": "leave", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * } + * ``` */ export interface SpaceMember { /** - * - * The client identifier for the user, provided to the ably client instance. - * + * The [client identifier](https://ably.com/docs/auth/identified-clients) for the member. */ clientId: string; /** - * - * Identifier for the connection used by the user. This is a unique identifier. - * + * The unique identifier of the member’s [connection](https://ably.com/docs/connect). */ connectionId: string; /** - * - * Whether the user is connected to Ably. - * + * Whether the user is connected to Ably or not. */ isConnected: boolean; /** - * - * Optional user data that can be attached to a user, such as a username or image to display in an avatar stack. - * + * Optional data that is associated with a member, such as a preferred username or profile picture to display in an avatar stack. */ profileData: ProfileData; /** - * * The current location of the user within the space. - * */ location: unknown; /** - * - * The most recent event emitted by [presence](https://ably.com/docs/presence-occupancy/presence?lang=javascript) and its timestamp. Events will be either `enter`, `leave`, `update` or `present`. - * + * The most recent event emitted by [presence](https://ably.com/docs/presence-occupancy/presence) and its timestamp. Events will be either `enter`, `leave`, `update` or `present`. */ lastEvent: { /** - * * The most recent event emitted by the member. */ name: Types.PresenceAction; /** - * * The timestamp of the most recently emitted event. */ timestamp: number; @@ -154,66 +152,54 @@ export interface SpaceMember { } /** - * The `LockStatuses` namespace describes the possible values of the {@link LockStatus} type. + * Describes the possible values of the {@link LockStatus} type. */ export namespace LockStatuses { /** - * - * A member has requested a lock by calling { @link Locks.acquire | `acquire()` }. + * A member has requested a lock by calling {@link Locks.acquire | `acquire()`}. */ export type Pending = 'pending'; /** - * - * The lock is confirmed to be held by the requesting member. + * A lock is confirmed to be held by the requesting member. */ export type Locked = 'locked'; /** - * - * The lock is confirmed to not be locked by the requesting member, or has been { @link Locks.release | released } by a member previously holding the lock. + * A lock is confirmed to not be locked by the requesting member, or has been {@link Locks.release | released} by a member previously holding the lock. */ export type Unlocked = 'unlocked'; } /** - * - * Represents a status of a lock. - * + * Represents the status of a lock. */ export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatuses.Unlocked; /** - * * Represents a Lock. - * */ export type Lock = { /** - * * The unique ID of the lock request. */ id: string; /** - * - * The lock status of the event. + * The status of the lock event. */ status: LockStatus; /** - * Information about the space member who requested the lock. + * Information about the member who requested the lock. */ member: SpaceMember; /** - * * The timestamp of the lock event. */ timestamp: number; /** - * * The optional attributes of the lock, such as the ID of the component it relates to. */ attributes?: LockAttributes; /** - * - * The reason why the `request.status` is `unlocked`. + * The reason why the lock status is {@link LockStatuses.Unlocked | `unlocked`}. */ reason?: Types.ErrorInfo; }; From c61d90728c19e6450901c9be5a1fcee08bfd43b3 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:37:19 +0000 Subject: [PATCH 165/191] Make EventEmitter docstring comments consistent --- src/utilities/EventEmitter.ts | 40 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index df391d2a..eb2c44a8 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -115,16 +115,16 @@ export default class EventEmitter { /** * {@label WITH_EVENTS} - * Add an event listener - * @param eventOrEvents the name of the event to listen to or the listener to be called. - * @param listener (optional) the listener to be called. + * Add an event listener. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. * * @typeParam K A type which allows one or more names of the properties of {@link T}. */ on(eventOrEvents?: K | K[], listener?: EventListener): void; /** - * Behaves the same as { @link on:WITH_EVENTS | the overload which accepts one or more event names }, but listens to _all_ events. - * @param listener (optional) the listener to be called. + * Add an event listener for all event types. + * @param listener An event listener. */ on(listener?: EventListener): void; /** @@ -159,16 +159,16 @@ export default class EventEmitter { /** * {@label WITH_EVENTS} - * Remove one or more event listeners - * @param eventOrEvents the name of the event whose listener is to be removed. - * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. + * Remove one or more event listeners. + * @param eventOrEvents An event name, or an array of event names. + * @param listener An event listener. If not supplied, all listeners are removed. * * @typeParam K A type which allows one or more names of the properties of {@link T}. TypeScript will infer this type based on the {@link eventOrEvents} argument. */ off(eventOrEvents?: K | K[], listener?: EventListener): void; /** - * Behaves the same as { @link off:WITH_EVENTS | the overload which accepts one or more event names }, but removes the listener from _all_ events. - * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. + * Remove one or more event listeners for all event types. + * @param listener An event listener. If not supplied, all listeners are removed. */ off(listener?: EventListener): void; /** @@ -227,9 +227,9 @@ export default class EventEmitter { } /** - * Get the array of listeners for a given event; excludes once events - * @param event (optional) the name of the event, or none for 'any' - * @return array of events, or null if none + * Get the array of listeners for a given event. Excludes `once` events. + * @param event An event name. + * @return An array of events, or `null` if there are none. * * @typeParam K A type which allows a name of the properties of {@link T}. TypeScript will infer this type based on the {@link event} argument. */ @@ -251,8 +251,8 @@ export default class EventEmitter { * @internal * * Emit an event - * @param event the event name - * @param arg the arguments to pass to the listener + * @param event An event name. + * @param arg The arguments to pass to the listener. */ emit(event: K, arg: T[K]) { const eventThis = { event }; @@ -285,16 +285,16 @@ export default class EventEmitter { /** * {@label WITH_EVENTS} - * Listen for a single occurrence of an event - * @param event the name of the event to listen to - * @param listener (optional) the listener to be called + * Listen for a single occurrence of an event. + * @param event An event name. + * @param listener An event listener. * * @typeParam K A type which allows a name of one of the properties of {@link T}. TypeScript will infer this type based on the {@link event} argument. */ once(event: K, listener?: EventListener): void | Promise; /** - * Behaves the same as { @link once:WITH_EVENTS | the overload which accepts one or more event names }, but listens for _all_ events. - * @param listener (optional) the listener to be called + * Listen for a single occurrence of an event of any event type. + * @param listener An event listener. */ once(listener?: EventListener): void | Promise; /** From da108da95ef3cd976a99d8f77b7cb8102e97193a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 09:03:12 +0000 Subject: [PATCH 166/191] build(deps-dev): bump typedoc from 0.25.2 to 0.25.3 Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.25.2 to 0.25.3. - [Release notes](https://github.com/TypeStrong/TypeDoc/releases) - [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md) - [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.25.2...v0.25.3) --- updated-dependencies: - dependency-name: typedoc dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96286e03..f2ce93f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6535,9 +6535,9 @@ } }, "node_modules/typedoc": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz", - "integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.3.tgz", + "integrity": "sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==", "dev": true, "dependencies": { "lunr": "^2.3.9", @@ -11834,9 +11834,9 @@ } }, "typedoc": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz", - "integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==", + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.3.tgz", + "integrity": "sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==", "dev": true, "requires": { "lunr": "^2.3.9", From a37f50a8986cc4ea26cb7db7a0899e9eab7aef32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 09:31:05 +0000 Subject: [PATCH 167/191] build(deps-dev): bump @testing-library/react from 14.0.0 to 14.1.2 Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 14.0.0 to 14.1.2. - [Release notes](https://github.com/testing-library/react-testing-library/releases) - [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/react-testing-library/compare/v14.0.0...v14.1.2) --- updated-dependencies: - dependency-name: "@testing-library/react" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2ce93f5..9d667397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1192,9 +1192,9 @@ "dev": true }, "node_modules/@testing-library/react": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", - "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", + "integrity": "sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -7850,9 +7850,9 @@ } }, "@testing-library/react": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", - "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", + "integrity": "sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg==", "dev": true, "requires": { "@babel/runtime": "^7.12.5", From 0a73e675f793d1b0b5ea3da6b5d1861f3f4466d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 14:20:07 +0000 Subject: [PATCH 168/191] build(deps-dev): bump vitest from 0.34.4 to 0.34.6 Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 0.34.4 to 0.34.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v0.34.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 682 +++++++++++++++++++++++----------------------- 1 file changed, 342 insertions(+), 340 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d667397..b51b0b85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -316,9 +316,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.7.tgz", + "integrity": "sha512-YGSPnndkcLo4PmVl2tKatEn+0mlVMr3yEpOOT0BeMria87PhvoJb5dg5f5Ft9fbCVgtAz4pWMzZVgSEGpDAlww==", "cpu": [ "arm" ], @@ -332,9 +332,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.7.tgz", + "integrity": "sha512-YEDcw5IT7hW3sFKZBkCAQaOCJQLONVcD4bOyTXMZz5fr66pTHnAet46XAtbXAkJRfIn2YVhdC6R9g4xa27jQ1w==", "cpu": [ "arm64" ], @@ -348,9 +348,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.7.tgz", + "integrity": "sha512-jhINx8DEjz68cChFvM72YzrqfwJuFbfvSxZAk4bebpngGfNNRm+zRl4rtT9oAX6N9b6gBcFaJHFew5Blf6CvUw==", "cpu": [ "x64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.7.tgz", + "integrity": "sha512-dr81gbmWN//3ZnBIm6YNCl4p3pjnabg1/ZVOgz2fJoUO1a3mq9WQ/1iuEluMs7mCL+Zwv7AY5e3g1hjXqQZ9Iw==", "cpu": [ "arm64" ], @@ -380,9 +380,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.7.tgz", + "integrity": "sha512-Lc0q5HouGlzQEwLkgEKnWcSazqr9l9OdV2HhVasWJzLKeOt0PLhHaUHuzb8s/UIya38DJDoUm74GToZ6Wc7NGQ==", "cpu": [ "x64" ], @@ -396,9 +396,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.7.tgz", + "integrity": "sha512-+y2YsUr0CxDFF7GWiegWjGtTUF6gac2zFasfFkRJPkMAuMy9O7+2EH550VlqVdpEEchWMynkdhC9ZjtnMiHImQ==", "cpu": [ "arm64" ], @@ -412,9 +412,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.7.tgz", + "integrity": "sha512-CdXOxIbIzPJmJhrpmJTLx+o35NoiKBIgOvmvT+jeSadYiWJn0vFKsl+0bSG/5lwjNHoIDEyMYc/GAPR9jxusTA==", "cpu": [ "x64" ], @@ -428,9 +428,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.7.tgz", + "integrity": "sha512-Y+SCmWxsJOdQtjcBxoacn/pGW9HDZpwsoof0ttL+2vGcHokFlfqV666JpfLCSP2xLxFpF1lj7T3Ox3sr95YXww==", "cpu": [ "arm" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.7.tgz", + "integrity": "sha512-inHqdOVCkUhHNvuQPT1oCB7cWz9qQ/Cz46xmVe0b7UXcuIJU3166aqSunsqkgSGMtUCWOZw3+KMwI6otINuC9g==", "cpu": [ "arm64" ], @@ -460,9 +460,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.7.tgz", + "integrity": "sha512-2BbiL7nLS5ZO96bxTQkdO0euGZIUQEUXMTrqLxKUmk/Y5pmrWU84f+CMJpM8+EHaBPfFSPnomEaQiG/+Gmh61g==", "cpu": [ "ia32" ], @@ -476,9 +476,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.7.tgz", + "integrity": "sha512-BVFQla72KXv3yyTFCQXF7MORvpTo4uTA8FVFgmwVrqbB/4DsBFWilUm1i2Oq6zN36DOZKSVUTb16jbjedhfSHw==", "cpu": [ "loong64" ], @@ -492,9 +492,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.7.tgz", + "integrity": "sha512-DzAYckIaK+pS31Q/rGpvUKu7M+5/t+jI+cdleDgUwbU7KdG2eC3SUbZHlo6Q4P1CfVKZ1lUERRFP8+q0ob9i2w==", "cpu": [ "mips64el" ], @@ -508,9 +508,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.7.tgz", + "integrity": "sha512-JQ1p0SmUteNdUaaiRtyS59GkkfTW0Edo+e0O2sihnY4FoZLz5glpWUQEKMSzMhA430ctkylkS7+vn8ziuhUugQ==", "cpu": [ "ppc64" ], @@ -524,9 +524,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.7.tgz", + "integrity": "sha512-xGwVJ7eGhkprY/nB7L7MXysHduqjpzUl40+XoYDGC4UPLbnG+gsyS1wQPJ9lFPcxYAaDXbdRXd1ACs9AE9lxuw==", "cpu": [ "riscv64" ], @@ -540,9 +540,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.7.tgz", + "integrity": "sha512-U8Rhki5PVU0L0nvk+E8FjkV8r4Lh4hVEb9duR6Zl21eIEYEwXz8RScj4LZWA2i3V70V4UHVgiqMpszXvG0Yqhg==", "cpu": [ "s390x" ], @@ -556,9 +556,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.7.tgz", + "integrity": "sha512-ZYZopyLhm4mcoZXjFt25itRlocKlcazDVkB4AhioiL9hOWhDldU9n38g62fhOI4Pth6vp+Mrd5rFKxD0/S+7aQ==", "cpu": [ "x64" ], @@ -572,9 +572,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.7.tgz", + "integrity": "sha512-/yfjlsYmT1O3cum3J6cmGG16Fd5tqKMcg5D+sBYLaOQExheAJhqr8xOAEIuLo8JYkevmjM5zFD9rVs3VBcsjtQ==", "cpu": [ "x64" ], @@ -588,9 +588,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.7.tgz", + "integrity": "sha512-MYDFyV0EW1cTP46IgUJ38OnEY5TaXxjoDmwiTXPjezahQgZd+j3T55Ht8/Q9YXBM0+T9HJygrSRGV5QNF/YVDQ==", "cpu": [ "x64" ], @@ -604,9 +604,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.7.tgz", + "integrity": "sha512-JcPvgzf2NN/y6X3UUSqP6jSS06V0DZAV/8q0PjsZyGSXsIGcG110XsdmuWiHM+pno7/mJF6fjH5/vhUz/vA9fw==", "cpu": [ "x64" ], @@ -620,9 +620,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.7.tgz", + "integrity": "sha512-ZA0KSYti5w5toax5FpmfcAgu3ZNJxYSRm0AW/Dao5up0YV1hDVof1NvwLomjEN+3/GMtaWDI+CIyJOMTRSTdMw==", "cpu": [ "arm64" ], @@ -636,9 +636,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.7.tgz", + "integrity": "sha512-CTOnijBKc5Jpk6/W9hQMMvJnsSYRYgveN6O75DTACCY18RA2nqka8dTZR+x/JqXCRiKk84+5+bRKXUSbbwsS0A==", "cpu": [ "ia32" ], @@ -652,9 +652,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.7.tgz", + "integrity": "sha512-gRaP2sk6hc98N734luX4VpF318l3w+ofrtTu9j5L8EQXF+FzQKV6alCOHMVoJJHvVK/mGbwBXfOL1HETQu9IGQ==", "cpu": [ "x64" ], @@ -1774,26 +1774,26 @@ } }, "node_modules/@vitest/expect": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.4.tgz", - "integrity": "sha512-XlMKX8HyYUqB8dsY8Xxrc64J2Qs9pKMt2Z8vFTL4mBWXJsg4yoALHzJfDWi8h5nkO4Zua4zjqtapQ/IluVkSnA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", "dev": true, "dependencies": { - "@vitest/spy": "0.34.4", - "@vitest/utils": "0.34.4", - "chai": "^4.3.7" + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.4.tgz", - "integrity": "sha512-hwwdB1StERqUls8oV8YcpmTIpVeJMe4WgYuDongVzixl5hlYLT2G8afhcdADeDeqCaAmZcSgLTLtqkjPQF7x+w==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", "dev": true, "dependencies": { - "@vitest/utils": "0.34.4", + "@vitest/utils": "0.34.6", "p-limit": "^4.0.0", "pathe": "^1.1.1" }, @@ -1802,9 +1802,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.4.tgz", - "integrity": "sha512-GCsh4coc3YUSL/o+BPUo7lHQbzpdttTxL6f4q0jRx2qVGoYz/cyTRDJHbnwks6TILi6560bVWoBpYC10PuTLHw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", "dev": true, "dependencies": { "magic-string": "^0.30.1", @@ -1816,9 +1816,9 @@ } }, "node_modules/@vitest/spy": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.4.tgz", - "integrity": "sha512-PNU+fd7DUPgA3Ya924b1qKuQkonAW6hL7YUjkON3wmBwSTIlhOSpy04SJ0NrRsEbrXgMMj6Morh04BMf8k+w0g==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", "dev": true, "dependencies": { "tinyspy": "^2.1.1" @@ -1828,9 +1828,9 @@ } }, "node_modules/@vitest/utils": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.4.tgz", - "integrity": "sha512-yR2+5CHhp/K4ySY0Qtd+CAL9f5Yh1aXrKfAT42bq6CtlGPh92jIDDDSg7ydlRow1CP+dys4TrOrbELOyNInHSg==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", "dev": true, "dependencies": { "diff-sequences": "^29.4.3", @@ -2363,18 +2363,18 @@ } }, "node_modules/chai": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", - "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -2397,10 +2397,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -2939,9 +2942,9 @@ } }, "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.7.tgz", + "integrity": "sha512-6brbTZVqxhqgbpqBR5MzErImcpA0SQdoKOkcWK/U30HtQxnokIpG3TX2r0IJqbFUzqLjhU/zC1S5ndgakObVCQ==", "dev": true, "hasInstallScript": true, "bin": { @@ -2951,28 +2954,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "@esbuild/android-arm": "0.19.7", + "@esbuild/android-arm64": "0.19.7", + "@esbuild/android-x64": "0.19.7", + "@esbuild/darwin-arm64": "0.19.7", + "@esbuild/darwin-x64": "0.19.7", + "@esbuild/freebsd-arm64": "0.19.7", + "@esbuild/freebsd-x64": "0.19.7", + "@esbuild/linux-arm": "0.19.7", + "@esbuild/linux-arm64": "0.19.7", + "@esbuild/linux-ia32": "0.19.7", + "@esbuild/linux-loong64": "0.19.7", + "@esbuild/linux-mips64el": "0.19.7", + "@esbuild/linux-ppc64": "0.19.7", + "@esbuild/linux-riscv64": "0.19.7", + "@esbuild/linux-s390x": "0.19.7", + "@esbuild/linux-x64": "0.19.7", + "@esbuild/netbsd-x64": "0.19.7", + "@esbuild/openbsd-x64": "0.19.7", + "@esbuild/sunos-x64": "0.19.7", + "@esbuild/win32-arm64": "0.19.7", + "@esbuild/win32-ia32": "0.19.7", + "@esbuild/win32-x64": "0.19.7" } }, "node_modules/escalade": { @@ -3686,9 +3689,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -4680,12 +4683,12 @@ } }, "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "dependencies": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "node_modules/lowercase-keys": { @@ -5343,9 +5346,9 @@ } }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -6276,9 +6279,9 @@ "dev": true }, "node_modules/tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true, "engines": { "node": ">=14.0.0" @@ -6593,9 +6596,9 @@ } }, "node_modules/ufo": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.0.tgz", - "integrity": "sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", + "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, "node_modules/unbox-primitive": { @@ -6689,29 +6692,29 @@ } }, "node_modules/vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.2.tgz", + "integrity": "sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==", "dev": true, "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "esbuild": "^0.19.3", + "postcss": "^8.4.31", + "rollup": "^4.2.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", @@ -6744,9 +6747,9 @@ } }, "node_modules/vite-node": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.4.tgz", - "integrity": "sha512-ho8HtiLc+nsmbwZMw8SlghESEE3KxJNp04F/jPUCLVvaURwt0d+r9LxEqCX5hvrrOQ0GSyxbYr5ZfRYhQ0yVKQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -6754,7 +6757,7 @@ "mlly": "^1.4.0", "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -6766,40 +6769,38 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite/node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/vitest": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.4.tgz", - "integrity": "sha512-SE/laOsB6995QlbSE6BtkpXDeVNLJc1u2LHRG/OpnN4RsRzM3GQm4nm3PQCK5OBtrsUqnhzLdnT7se3aeNGdlw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.34.4", - "@vitest/runner": "0.34.4", - "@vitest/snapshot": "0.34.4", - "@vitest/spy": "0.34.4", - "@vitest/utils": "0.34.4", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", - "chai": "^4.3.7", + "chai": "^4.3.10", "debug": "^4.3.4", "local-pkg": "^0.4.3", "magic-string": "^0.30.1", @@ -6810,7 +6811,7 @@ "tinybench": "^2.5.0", "tinypool": "^0.7.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.4", + "vite-node": "0.34.6", "why-is-node-running": "^2.2.2" }, "bin": { @@ -7354,156 +7355,156 @@ } }, "@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.7.tgz", + "integrity": "sha512-YGSPnndkcLo4PmVl2tKatEn+0mlVMr3yEpOOT0BeMria87PhvoJb5dg5f5Ft9fbCVgtAz4pWMzZVgSEGpDAlww==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.7.tgz", + "integrity": "sha512-YEDcw5IT7hW3sFKZBkCAQaOCJQLONVcD4bOyTXMZz5fr66pTHnAet46XAtbXAkJRfIn2YVhdC6R9g4xa27jQ1w==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.7.tgz", + "integrity": "sha512-jhINx8DEjz68cChFvM72YzrqfwJuFbfvSxZAk4bebpngGfNNRm+zRl4rtT9oAX6N9b6gBcFaJHFew5Blf6CvUw==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.7.tgz", + "integrity": "sha512-dr81gbmWN//3ZnBIm6YNCl4p3pjnabg1/ZVOgz2fJoUO1a3mq9WQ/1iuEluMs7mCL+Zwv7AY5e3g1hjXqQZ9Iw==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.7.tgz", + "integrity": "sha512-Lc0q5HouGlzQEwLkgEKnWcSazqr9l9OdV2HhVasWJzLKeOt0PLhHaUHuzb8s/UIya38DJDoUm74GToZ6Wc7NGQ==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.7.tgz", + "integrity": "sha512-+y2YsUr0CxDFF7GWiegWjGtTUF6gac2zFasfFkRJPkMAuMy9O7+2EH550VlqVdpEEchWMynkdhC9ZjtnMiHImQ==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.7.tgz", + "integrity": "sha512-CdXOxIbIzPJmJhrpmJTLx+o35NoiKBIgOvmvT+jeSadYiWJn0vFKsl+0bSG/5lwjNHoIDEyMYc/GAPR9jxusTA==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.7.tgz", + "integrity": "sha512-Y+SCmWxsJOdQtjcBxoacn/pGW9HDZpwsoof0ttL+2vGcHokFlfqV666JpfLCSP2xLxFpF1lj7T3Ox3sr95YXww==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.7.tgz", + "integrity": "sha512-inHqdOVCkUhHNvuQPT1oCB7cWz9qQ/Cz46xmVe0b7UXcuIJU3166aqSunsqkgSGMtUCWOZw3+KMwI6otINuC9g==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.7.tgz", + "integrity": "sha512-2BbiL7nLS5ZO96bxTQkdO0euGZIUQEUXMTrqLxKUmk/Y5pmrWU84f+CMJpM8+EHaBPfFSPnomEaQiG/+Gmh61g==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.7.tgz", + "integrity": "sha512-BVFQla72KXv3yyTFCQXF7MORvpTo4uTA8FVFgmwVrqbB/4DsBFWilUm1i2Oq6zN36DOZKSVUTb16jbjedhfSHw==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.7.tgz", + "integrity": "sha512-DzAYckIaK+pS31Q/rGpvUKu7M+5/t+jI+cdleDgUwbU7KdG2eC3SUbZHlo6Q4P1CfVKZ1lUERRFP8+q0ob9i2w==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.7.tgz", + "integrity": "sha512-JQ1p0SmUteNdUaaiRtyS59GkkfTW0Edo+e0O2sihnY4FoZLz5glpWUQEKMSzMhA430ctkylkS7+vn8ziuhUugQ==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.7.tgz", + "integrity": "sha512-xGwVJ7eGhkprY/nB7L7MXysHduqjpzUl40+XoYDGC4UPLbnG+gsyS1wQPJ9lFPcxYAaDXbdRXd1ACs9AE9lxuw==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.7.tgz", + "integrity": "sha512-U8Rhki5PVU0L0nvk+E8FjkV8r4Lh4hVEb9duR6Zl21eIEYEwXz8RScj4LZWA2i3V70V4UHVgiqMpszXvG0Yqhg==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.7.tgz", + "integrity": "sha512-ZYZopyLhm4mcoZXjFt25itRlocKlcazDVkB4AhioiL9hOWhDldU9n38g62fhOI4Pth6vp+Mrd5rFKxD0/S+7aQ==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.7.tgz", + "integrity": "sha512-/yfjlsYmT1O3cum3J6cmGG16Fd5tqKMcg5D+sBYLaOQExheAJhqr8xOAEIuLo8JYkevmjM5zFD9rVs3VBcsjtQ==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.7.tgz", + "integrity": "sha512-MYDFyV0EW1cTP46IgUJ38OnEY5TaXxjoDmwiTXPjezahQgZd+j3T55Ht8/Q9YXBM0+T9HJygrSRGV5QNF/YVDQ==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.7.tgz", + "integrity": "sha512-JcPvgzf2NN/y6X3UUSqP6jSS06V0DZAV/8q0PjsZyGSXsIGcG110XsdmuWiHM+pno7/mJF6fjH5/vhUz/vA9fw==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.7.tgz", + "integrity": "sha512-ZA0KSYti5w5toax5FpmfcAgu3ZNJxYSRm0AW/Dao5up0YV1hDVof1NvwLomjEN+3/GMtaWDI+CIyJOMTRSTdMw==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.7.tgz", + "integrity": "sha512-CTOnijBKc5Jpk6/W9hQMMvJnsSYRYgveN6O75DTACCY18RA2nqka8dTZR+x/JqXCRiKk84+5+bRKXUSbbwsS0A==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.7.tgz", + "integrity": "sha512-gRaP2sk6hc98N734luX4VpF318l3w+ofrtTu9j5L8EQXF+FzQKV6alCOHMVoJJHvVK/mGbwBXfOL1HETQu9IGQ==", "dev": true, "optional": true }, @@ -8289,31 +8290,31 @@ } }, "@vitest/expect": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.4.tgz", - "integrity": "sha512-XlMKX8HyYUqB8dsY8Xxrc64J2Qs9pKMt2Z8vFTL4mBWXJsg4yoALHzJfDWi8h5nkO4Zua4zjqtapQ/IluVkSnA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", "dev": true, "requires": { - "@vitest/spy": "0.34.4", - "@vitest/utils": "0.34.4", - "chai": "^4.3.7" + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" } }, "@vitest/runner": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.4.tgz", - "integrity": "sha512-hwwdB1StERqUls8oV8YcpmTIpVeJMe4WgYuDongVzixl5hlYLT2G8afhcdADeDeqCaAmZcSgLTLtqkjPQF7x+w==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", "dev": true, "requires": { - "@vitest/utils": "0.34.4", + "@vitest/utils": "0.34.6", "p-limit": "^4.0.0", "pathe": "^1.1.1" } }, "@vitest/snapshot": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.4.tgz", - "integrity": "sha512-GCsh4coc3YUSL/o+BPUo7lHQbzpdttTxL6f4q0jRx2qVGoYz/cyTRDJHbnwks6TILi6560bVWoBpYC10PuTLHw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", "dev": true, "requires": { "magic-string": "^0.30.1", @@ -8322,18 +8323,18 @@ } }, "@vitest/spy": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.4.tgz", - "integrity": "sha512-PNU+fd7DUPgA3Ya924b1qKuQkonAW6hL7YUjkON3wmBwSTIlhOSpy04SJ0NrRsEbrXgMMj6Morh04BMf8k+w0g==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", "dev": true, "requires": { "tinyspy": "^2.1.1" } }, "@vitest/utils": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.4.tgz", - "integrity": "sha512-yR2+5CHhp/K4ySY0Qtd+CAL9f5Yh1aXrKfAT42bq6CtlGPh92jIDDDSg7ydlRow1CP+dys4TrOrbELOyNInHSg==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", "dev": true, "requires": { "diff-sequences": "^29.4.3", @@ -8729,18 +8730,18 @@ "dev": true }, "chai": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", - "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "requires": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" } }, "chalk": { @@ -8754,10 +8755,13 @@ } }, "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } }, "cliui": { "version": "7.0.4", @@ -9178,33 +9182,33 @@ } }, "esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.7.tgz", + "integrity": "sha512-6brbTZVqxhqgbpqBR5MzErImcpA0SQdoKOkcWK/U30HtQxnokIpG3TX2r0IJqbFUzqLjhU/zC1S5ndgakObVCQ==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.19.7", + "@esbuild/android-arm64": "0.19.7", + "@esbuild/android-x64": "0.19.7", + "@esbuild/darwin-arm64": "0.19.7", + "@esbuild/darwin-x64": "0.19.7", + "@esbuild/freebsd-arm64": "0.19.7", + "@esbuild/freebsd-x64": "0.19.7", + "@esbuild/linux-arm": "0.19.7", + "@esbuild/linux-arm64": "0.19.7", + "@esbuild/linux-ia32": "0.19.7", + "@esbuild/linux-loong64": "0.19.7", + "@esbuild/linux-mips64el": "0.19.7", + "@esbuild/linux-ppc64": "0.19.7", + "@esbuild/linux-riscv64": "0.19.7", + "@esbuild/linux-s390x": "0.19.7", + "@esbuild/linux-x64": "0.19.7", + "@esbuild/netbsd-x64": "0.19.7", + "@esbuild/openbsd-x64": "0.19.7", + "@esbuild/sunos-x64": "0.19.7", + "@esbuild/win32-arm64": "0.19.7", + "@esbuild/win32-ia32": "0.19.7", + "@esbuild/win32-x64": "0.19.7" } }, "escalade": { @@ -9779,9 +9783,9 @@ "dev": true }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { @@ -10495,12 +10499,12 @@ } }, "loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "requires": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "lowercase-keys": { @@ -10960,9 +10964,9 @@ }, "dependencies": { "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true } } @@ -11652,9 +11656,9 @@ "dev": true }, "tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true }, "to-regex-range": { @@ -11872,9 +11876,9 @@ "dev": true }, "ufo": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.0.tgz", - "integrity": "sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", + "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, "unbox-primitive": { @@ -11950,32 +11954,30 @@ "dev": true }, "vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.2.tgz", + "integrity": "sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==", "dev": true, "requires": { - "esbuild": "^0.18.10", - "fsevents": "~2.3.2", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "esbuild": "^0.19.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.31", + "rollup": "^4.2.0" }, "dependencies": { - "rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "requires": { - "fsevents": "~2.3.2" - } + "optional": true } } }, "vite-node": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.4.tgz", - "integrity": "sha512-ho8HtiLc+nsmbwZMw8SlghESEE3KxJNp04F/jPUCLVvaURwt0d+r9LxEqCX5hvrrOQ0GSyxbYr5ZfRYhQ0yVKQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", "dev": true, "requires": { "cac": "^6.7.14", @@ -11983,27 +11985,27 @@ "mlly": "^1.4.0", "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" } }, "vitest": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.4.tgz", - "integrity": "sha512-SE/laOsB6995QlbSE6BtkpXDeVNLJc1u2LHRG/OpnN4RsRzM3GQm4nm3PQCK5OBtrsUqnhzLdnT7se3aeNGdlw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, "requires": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.34.4", - "@vitest/runner": "0.34.4", - "@vitest/snapshot": "0.34.4", - "@vitest/spy": "0.34.4", - "@vitest/utils": "0.34.4", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", - "chai": "^4.3.7", + "chai": "^4.3.10", "debug": "^4.3.4", "local-pkg": "^0.4.3", "magic-string": "^0.30.1", @@ -12014,7 +12016,7 @@ "tinybench": "^2.5.0", "tinypool": "^0.7.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.4", + "vite-node": "0.34.6", "why-is-node-running": "^2.2.2" }, "dependencies": { From 62bb100c06da4cae3057e2a94b7cb1221c69d172 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:17:36 +0000 Subject: [PATCH 169/191] build(deps): bump nanoid from 5.0.2 to 5.0.3 Bumps [nanoid](https://github.com/ai/nanoid) from 5.0.2 to 5.0.3. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/5.0.2...5.0.3) --- updated-dependencies: - dependency-name: nanoid dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index b51b0b85..1678227a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4906,9 +4906,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.2.tgz", - "integrity": "sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.3.tgz", + "integrity": "sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==", "funding": [ { "type": "github", @@ -10667,9 +10667,9 @@ "dev": true }, "nanoid": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.2.tgz", - "integrity": "sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==" + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.3.tgz", + "integrity": "sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==" }, "natural-compare": { "version": "1.4.0", From 9cc7eb56414a083caa2d0404ca698e91a9e49d68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 09:02:10 +0000 Subject: [PATCH 170/191] build(deps-dev): bump @types/react-dom from 18.2.8 to 18.2.17 Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.2.8 to 18.2.17. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) --- updated-dependencies: - dependency-name: "@types/react-dom" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1678227a..257efb92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1404,9 +1404,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", - "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "dev": true, "dependencies": { "@types/react": "*" @@ -8053,9 +8053,9 @@ } }, "@types/react-dom": { - "version": "18.2.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", - "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "dev": true, "requires": { "@types/react": "*" From ef98853ffefb58a9e21be97dcb50e79a422d8fe4 Mon Sep 17 00:00:00 2001 From: Dominik Piatek Date: Wed, 29 Nov 2023 11:35:50 +0000 Subject: [PATCH 171/191] Use Node 18 for CDN publishing --- .github/workflows/cdn.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cdn.yml b/.github/workflows/cdn.yml index 17e26e5a..d8abe791 100644 --- a/.github/workflows/cdn.yml +++ b/.github/workflows/cdn.yml @@ -35,10 +35,10 @@ jobs: with: role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/${{ github.event.inputs.role-to-assume }} aws-region: us-east-1 - - name: Use Node.js 14.x + - name: Use Node.js 18 uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18 - name: Install dependencies and build env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} From 88a4046b7db9cf4734fb02669b6afcc3c31199c2 Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Tue, 12 Dec 2023 13:40:15 +0000 Subject: [PATCH 172/191] Update Ably-Agent to be 'spaces' rather than 'ably-spaces' As per the agents.json: https://github.com/ably/ably-common/blob/fbcbc120bad41984443a089a66058d80f915dc7c/protocol/agents.json#L323-L327 Signed-off-by: Lewis Marshall --- src/Spaces.test.ts | 4 ++-- src/Spaces.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index c0451e17..79941d1a 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -41,7 +41,7 @@ describe('Spaces', () => { it('applies the agent header to an existing SDK instance', ({ client }) => { const spaces = new Spaces(client); expect(client.options.agents).toEqual({ - 'ably-spaces': spaces.version, + spaces: spaces.version, 'space-custom-client': true, }); }); @@ -53,7 +53,7 @@ describe('Spaces', () => { expect(ablyClient.options.agents).toEqual({ 'some-client': '1.2.3', - 'ably-spaces': spaces.version, + spaces: spaces.version, 'space-custom-client': true, }); }); diff --git a/src/Spaces.ts b/src/Spaces.ts index be2b98f0..9a5fa006 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -50,7 +50,7 @@ class Spaces { } private addAgent(options: { agents?: Record }) { - const agent = { 'ably-spaces': this.version, 'space-custom-client': true }; + const agent = { spaces: this.version, 'space-custom-client': true }; options.agents = { ...(options.agents ?? options.agents), ...agent }; } From 6fb182bf236c54930a13f0e28560a649b564f6ce Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Tue, 12 Dec 2023 13:43:08 +0000 Subject: [PATCH 173/191] Remove space-custom-client Ably-Agent It's no longer needed since a client is always passed to the constructor now. Signed-off-by: Lewis Marshall --- src/Spaces.test.ts | 2 -- src/Spaces.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index 79941d1a..41d80dc3 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -42,7 +42,6 @@ describe('Spaces', () => { const spaces = new Spaces(client); expect(client.options.agents).toEqual({ spaces: spaces.version, - 'space-custom-client': true, }); }); @@ -54,7 +53,6 @@ describe('Spaces', () => { expect(ablyClient.options.agents).toEqual({ 'some-client': '1.2.3', spaces: spaces.version, - 'space-custom-client': true, }); }); }); diff --git a/src/Spaces.ts b/src/Spaces.ts index 9a5fa006..6117aff1 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -50,7 +50,7 @@ class Spaces { } private addAgent(options: { agents?: Record }) { - const agent = { spaces: this.version, 'space-custom-client': true }; + const agent = { spaces: this.version }; options.agents = { ...(options.agents ?? options.agents), ...agent }; } From cff95e32b01487801d47fcfc21e05d913bb26c3e Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 1 May 2024 13:09:26 +0100 Subject: [PATCH 174/191] Downgrade to using nanoid@3 to support its use in CJS module bundle (#307) * Downgrade to using nanoid@3 to support its use in CJS module bundle Starting from nanoid v4, it only works with ESM projects. We need to support both CJS and ESM bundles for the `spaces` SDK, so we have the following options to use the nanoid package: 1. Replace `require('nanoid')` imports with async `await import('nanoid')`. This solution is not suitable as the methods we currently use nanoid in are not async, and changing them to async will create cascading effects for a bunch of other methods and change the public API for users. 2. Replace the nanoid package with an in-project utility function to generate ids. This may be undesired since nanoid uses different `crypto` packages for its browser and Node.js bundles with some other platform-specific optimizations. Including them locally would not be a trivial change. 3. Downgrade to using nanoid v3, which supports both CJS and ESM and is still being supported by the developers [1]. This is the option implemented in this commit. Resolves #306 [1] https://github.com/ai/nanoid?tab=readme-ov-file#install * Prevent `nanoid` major version updates by dependabot --- .github/dependabot.yml | 3 ++ demo/package-lock.json | 64 +++++++++++++----------------------------- demo/package.json | 2 +- package-lock.json | 44 ++++++----------------------- package.json | 2 +- 5 files changed, 34 insertions(+), 81 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8bdda603..a5eb3219 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,6 @@ updates: schedule: interval: "daily" # weekdays (Monday to Friday) labels: [ ] # prevent the default `dependencies` label from being added to pull requests + ignore: + - dependency-name: "nanoid" + update-types: ["version-update:semver-major"] # prevent from upgrading from nanoid@3, see for more info: https://github.com/ably/spaces/pull/307 diff --git a/demo/package-lock.json b/demo/package-lock.json index 243cbb69..4a8208df 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -12,7 +12,7 @@ "ably": "^1.2.45", "classnames": "^2.3.2", "dayjs": "^1.11.9", - "nanoid": "^4.0.2", + "nanoid": "^3.3.7", "random-words": "^2.0.0", "react": "^18.2.0", "react-contenteditable": "^3.3.7", @@ -41,15 +41,15 @@ }, "..": { "name": "@ably/spaces", - "version": "0.2.0", + "version": "0.3.0", "license": "ISC", "dependencies": { - "nanoid": "^5.0.2" + "nanoid": "^3.3.7" }, "devDependencies": { "@playwright/test": "^1.39.0", - "@rollup/plugin-node-resolve": "^15.2.1", - "@rollup/plugin-terser": "^0.4.3", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", "@testing-library/react": "^14.0.0", "@types/express": "^4.17.18", "@types/react": "^18.2.23", @@ -67,10 +67,10 @@ "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "rollup": "^3.28.0", + "rollup": "^4.5.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", - "typescript": "^4.9.5", + "typescript": "^5.2.2", "vitest": "^0.34.3" }, "peerDependencies": { @@ -2958,9 +2958,9 @@ } }, "node_modules/nanoid": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", - "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -2968,10 +2968,10 @@ } ], "bin": { - "nanoid": "bin/nanoid.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/natural-compare": { @@ -19269,23 +19269,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -20445,8 +20428,8 @@ "version": "file:..", "requires": { "@playwright/test": "^1.39.0", - "@rollup/plugin-node-resolve": "^15.2.1", - "@rollup/plugin-terser": "^0.4.3", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", "@testing-library/react": "^14.0.0", "@types/express": "^4.17.18", "@types/react": "^18.2.23", @@ -20461,14 +20444,14 @@ "eslint-plugin-security": "^1.7.1", "express": "^4.18.2", "jsdom": "^22.1.0", - "nanoid": "^5.0.2", + "nanoid": "^3.3.7", "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "rollup": "^3.28.0", + "rollup": "^4.5.0", "ts-node": "^10.9.1", "typedoc": "^0.25.2", - "typescript": "^4.9.5", + "typescript": "^5.2.2", "vitest": "^0.34.3" } }, @@ -22420,9 +22403,9 @@ } }, "nanoid": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", - "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==" + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, "natural-compare": { "version": "1.4.0", @@ -34461,13 +34444,6 @@ "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" - }, - "dependencies": { - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" - } } }, "postcss-import": { diff --git a/demo/package.json b/demo/package.json index 848019ec..0933a7cc 100644 --- a/demo/package.json +++ b/demo/package.json @@ -16,7 +16,7 @@ "ably": "^1.2.45", "classnames": "^2.3.2", "dayjs": "^1.11.9", - "nanoid": "^4.0.2", + "nanoid": "^3.3.7", "random-words": "^2.0.0", "react": "^18.2.0", "react-contenteditable": "^3.3.7", diff --git a/package-lock.json b/package-lock.json index 257efb92..5f494464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.3.0", "license": "ISC", "dependencies": { - "nanoid": "^5.0.2" + "nanoid": "^3.3.7" }, "devDependencies": { "@playwright/test": "^1.39.0", @@ -4906,9 +4906,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.3.tgz", - "integrity": "sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -4916,10 +4916,10 @@ } ], "bin": { - "nanoid": "bin/nanoid.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^18 || >=20" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/natural-compare": { @@ -5345,24 +5345,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10667,9 +10649,9 @@ "dev": true }, "nanoid": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.3.tgz", - "integrity": "sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==" + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, "natural-compare": { "version": "1.4.0", @@ -10961,14 +10943,6 @@ "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" - }, - "dependencies": { - "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true - } } }, "prelude-ls": { diff --git a/package.json b/package.json index 32850980..40bb949b 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "vitest": "^0.34.3" }, "dependencies": { - "nanoid": "^5.0.2" + "nanoid": "^3.3.7" }, "peerDependencies": { "ably": "^1.2.46", From 22901d74fce569cf66117221d5acb16a16dd76b8 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Fri, 3 May 2024 12:53:18 +0100 Subject: [PATCH 175/191] Release v0.3.1 (#310) * chore: bump version for 0.3.1 release * chore: update changelog for 0.3.1 release --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b2443aa..2e9e5cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## v0.3.1 + +No breaking changes were introduced in this release. + +* Fix not being able to import CJS Spaces bundle due to `ERR_REQUIRE_ESM` error [\#307](https://github.com/ably/spaces/pull/307) + ## v0.3.0 Breaking changes in this release: diff --git a/package-lock.json b/package-lock.json index 5f494464..d8b908d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ably/spaces", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ably/spaces", - "version": "0.3.0", + "version": "0.3.1", "license": "ISC", "dependencies": { "nanoid": "^3.3.7" diff --git a/package.json b/package.json index 40bb949b..29706da7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/spaces", - "version": "0.3.0", + "version": "0.3.1", "description": "", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", diff --git a/src/version.ts b/src/version.ts index e1d77662..02e02a02 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ // Manually update when bumping version -const VERSION = '0.3.0'; +const VERSION = '0.3.1'; export { VERSION }; From 2d10101c240c8a5a58424e62710f09760ac05872 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 11:46:57 +0100 Subject: [PATCH 176/191] Update package.json to ably 2.3.0 --- demo/api/ably-token-request/package-lock.json | 106 ++++++++++++------ demo/api/ably-token-request/package.json | 2 +- demo/package-lock.json | 93 +++++++++------ demo/package.json | 2 +- package-lock.json | 63 ++++++++--- package.json | 2 +- 6 files changed, 182 insertions(+), 86 deletions(-) diff --git a/demo/api/ably-token-request/package-lock.json b/demo/api/ably-token-request/package-lock.json index f9bcb100..51bd49ce 100644 --- a/demo/api/ably-token-request/package-lock.json +++ b/demo/api/ably-token-request/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@netlify/functions": "^1.4.0", "@types/node": "^18.3.0", - "ably": "^1.2.36", + "ably": "^2.3.0", "dotenv": "^16.0.3", "typescript": "^4.9.5" } @@ -102,24 +102,32 @@ } }, "node_modules/ably": { - "version": "1.2.36", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.36.tgz", - "integrity": "sha512-6Pf5l+wapM3sBC49JttyuNRoa5m/GPYQigM+ehlUECv/UjEg+j2vpNFdTidSuBKphwPhGMF0Jjf2/sLhXVWoug==", - "license": "Apache-2.0", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.3.0.tgz", + "integrity": "sha512-4QyN2SEUG1Svpg5LIr/K14U+X9e5Wv/dsldPcPZGwaE1iFVZxl7Ccb6/lcBMn+8Uq0+2q9rzTKFnGERQIRIHVg==", "dependencies": { "@ably/msgpack-js": "^0.4.0", - "got": "^11.8.2", - "ws": "^5.1" + "fastestsmallesttextencoderdecoder": "^1.0.22", + "got": "^11.8.5", + "ulid": "^2.3.0", + "ws": "^8.17.1" }, "engines": { - "node": ">=5.10.x" + "node": ">=16" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, "node_modules/base64-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", @@ -235,6 +243,11 @@ "once": "^1.4.0" } }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -406,17 +419,37 @@ "node": ">=4.2.0" } }, + "node_modules/ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", + "bin": { + "ulid": "bin/cli.js" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", - "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", - "dependencies": { - "async-limiter": "~1.0.0" + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } } }, @@ -493,20 +526,17 @@ } }, "ably": { - "version": "1.2.36", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.36.tgz", - "integrity": "sha512-6Pf5l+wapM3sBC49JttyuNRoa5m/GPYQigM+ehlUECv/UjEg+j2vpNFdTidSuBKphwPhGMF0Jjf2/sLhXVWoug==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.3.0.tgz", + "integrity": "sha512-4QyN2SEUG1Svpg5LIr/K14U+X9e5Wv/dsldPcPZGwaE1iFVZxl7Ccb6/lcBMn+8Uq0+2q9rzTKFnGERQIRIHVg==", "requires": { "@ably/msgpack-js": "^0.4.0", - "got": "^11.8.2", - "ws": "^5.1" + "fastestsmallesttextencoderdecoder": "^1.0.22", + "got": "^11.8.5", + "ulid": "^2.3.0", + "ws": "^8.17.1" } }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, "base64-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", @@ -590,6 +620,11 @@ "once": "^1.4.0" } }, + "fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" + }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -714,18 +749,21 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" }, + "ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", - "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", - "requires": { - "async-limiter": "~1.0.0" - } + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "requires": {} } } } diff --git a/demo/api/ably-token-request/package.json b/demo/api/ably-token-request/package.json index f6bd0725..64508514 100644 --- a/demo/api/ably-token-request/package.json +++ b/demo/api/ably-token-request/package.json @@ -16,7 +16,7 @@ "dependencies": { "@netlify/functions": "^1.4.0", "@types/node": "^18.3.0", - "ably": "^1.2.36", + "ably": "^2.3.0", "dotenv": "^16.0.3", "typescript": "^4.9.5" } diff --git a/demo/package-lock.json b/demo/package-lock.json index 4a8208df..e126d4cd 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@ably/spaces": "file:..", - "ably": "^1.2.45", + "ably": "^2.3.0", "classnames": "^2.3.2", "dayjs": "^1.11.9", "nanoid": "^3.3.7", @@ -41,7 +41,7 @@ }, "..": { "name": "@ably/spaces", - "version": "0.3.0", + "version": "0.3.1", "license": "ISC", "dependencies": { "nanoid": "^3.3.7" @@ -74,7 +74,7 @@ "vitest": "^0.34.3" }, "peerDependencies": { - "ably": "^1.2.46", + "ably": "^2.3.0", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, @@ -1356,16 +1356,18 @@ } }, "node_modules/ably": { - "version": "1.2.45", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", - "integrity": "sha512-8Jk8XT0dSPcLcZ8zSAF+mRtwMzaQR5dWHqGdx9Y859ZLhXiqf4gQb7P+4Elqy1l1IIRoDWgyt0fkyfo7ngUdMA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.3.0.tgz", + "integrity": "sha512-4QyN2SEUG1Svpg5LIr/K14U+X9e5Wv/dsldPcPZGwaE1iFVZxl7Ccb6/lcBMn+8Uq0+2q9rzTKFnGERQIRIHVg==", "dependencies": { "@ably/msgpack-js": "^0.4.0", + "fastestsmallesttextencoderdecoder": "^1.0.22", "got": "^11.8.5", - "ws": "^5.1" + "ulid": "^2.3.0", + "ws": "^8.17.1" }, "engines": { - "node": ">=5.10.x" + "node": ">=16" }, "peerDependencies": { "react": ">=16.8.0", @@ -1478,11 +1480,6 @@ "node": ">=8" } }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, "node_modules/autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -2342,6 +2339,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -19864,6 +19866,14 @@ "node": ">=14.17" } }, + "node_modules/ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", + "bin": { + "ulid": "bin/cli.js" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -20374,11 +20384,23 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", - "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", - "dependencies": { - "async-limiter": "~1.0.0" + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/yallist": { @@ -21251,13 +21273,15 @@ } }, "ably": { - "version": "1.2.45", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.45.tgz", - "integrity": "sha512-8Jk8XT0dSPcLcZ8zSAF+mRtwMzaQR5dWHqGdx9Y859ZLhXiqf4gQb7P+4Elqy1l1IIRoDWgyt0fkyfo7ngUdMA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.3.0.tgz", + "integrity": "sha512-4QyN2SEUG1Svpg5LIr/K14U+X9e5Wv/dsldPcPZGwaE1iFVZxl7Ccb6/lcBMn+8Uq0+2q9rzTKFnGERQIRIHVg==", "requires": { "@ably/msgpack-js": "^0.4.0", + "fastestsmallesttextencoderdecoder": "^1.0.22", "got": "^11.8.5", - "ws": "^5.1" + "ulid": "^2.3.0", + "ws": "^8.17.1" } }, "acorn": { @@ -21334,11 +21358,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" - }, "autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -21946,6 +21965,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" + }, "fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -34923,6 +34947,11 @@ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true }, + "ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==" + }, "update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -35161,12 +35190,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", - "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", - "requires": { - "async-limiter": "~1.0.0" - } + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "requires": {} }, "yallist": { "version": "3.1.1", diff --git a/demo/package.json b/demo/package.json index 0933a7cc..17ef9cad 100644 --- a/demo/package.json +++ b/demo/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@ably/spaces": "file:..", - "ably": "^1.2.45", + "ably": "^2.3.0", "classnames": "^2.3.2", "dayjs": "^1.11.9", "nanoid": "^3.3.7", diff --git a/package-lock.json b/package-lock.json index d8b908d9..918d8e4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "vitest": "^0.34.3" }, "peerDependencies": { - "ably": "^1.2.46", + "ably": "^2.3.0", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, @@ -1848,17 +1848,19 @@ "dev": true }, "node_modules/ably": { - "version": "1.2.46", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.46.tgz", - "integrity": "sha512-N+t2C3bWc8P7dL9sJ/WbfspdshSfdJFhkvlqQKBJjPZCjT0WeyW6PW4nQnJK7TfzsiESWZovZ+Aku+/kgZW2OQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.3.0.tgz", + "integrity": "sha512-4QyN2SEUG1Svpg5LIr/K14U+X9e5Wv/dsldPcPZGwaE1iFVZxl7Ccb6/lcBMn+8Uq0+2q9rzTKFnGERQIRIHVg==", "peer": true, "dependencies": { "@ably/msgpack-js": "^0.4.0", + "fastestsmallesttextencoderdecoder": "^1.0.22", "got": "^11.8.5", - "ws": "^8.14.2" + "ulid": "^2.3.0", + "ws": "^8.17.1" }, "engines": { - "node": ">=5.10.x" + "node": ">=16" }, "peerDependencies": { "react": ">=16.8.0", @@ -3471,6 +3473,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "peer": true + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -6583,6 +6591,15 @@ "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, + "node_modules/ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", + "peer": true, + "bin": { + "ulid": "bin/cli.js" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -7022,9 +7039,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -8331,14 +8348,16 @@ "dev": true }, "ably": { - "version": "1.2.46", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.46.tgz", - "integrity": "sha512-N+t2C3bWc8P7dL9sJ/WbfspdshSfdJFhkvlqQKBJjPZCjT0WeyW6PW4nQnJK7TfzsiESWZovZ+Aku+/kgZW2OQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.3.0.tgz", + "integrity": "sha512-4QyN2SEUG1Svpg5LIr/K14U+X9e5Wv/dsldPcPZGwaE1iFVZxl7Ccb6/lcBMn+8Uq0+2q9rzTKFnGERQIRIHVg==", "peer": true, "requires": { "@ably/msgpack-js": "^0.4.0", + "fastestsmallesttextencoderdecoder": "^1.0.22", "got": "^11.8.5", - "ws": "^8.14.2" + "ulid": "^2.3.0", + "ws": "^8.17.1" } }, "accepts": { @@ -9594,6 +9613,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "peer": true + }, "fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -11855,6 +11880,12 @@ "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, + "ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", + "peer": true + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -12128,9 +12159,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "requires": {} }, "xml-name-validator": { diff --git a/package.json b/package.json index 29706da7..18c90331 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "nanoid": "^3.3.7" }, "peerDependencies": { - "ably": "^1.2.46", + "ably": "^2.3.0", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, From a99e4db2405038e9172090f2ed7acd90fa3348a7 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:00:11 +0100 Subject: [PATCH 177/191] Update to use ably-js v2 API Do not try to fix possible errors yet, just simply update every place to use ably-js v2 API. --- __mocks__/ably/{promises => }/index.ts | 6 +++--- demo/api/ably-token-request/index.ts | 2 +- demo/src/main.tsx | 2 +- examples/avatar-stack.ts | 2 +- examples/live-cursors.ts | 2 +- examples/location.ts | 2 +- examples/locking.ts | 2 +- examples/locks.ts | 4 ++-- src/CursorBatching.ts | 8 ++++---- src/CursorDispensing.ts | 4 ++-- src/CursorHistory.ts | 8 ++++---- src/Cursors.test.ts | 14 +++++++------- src/Cursors.ts | 14 +++++++------- src/Locations.test.ts | 10 +++++----- src/Locks.test.ts | 10 +++++----- src/Locks.ts | 4 ++-- src/Members.test.ts | 10 +++++----- src/Space.test.ts | 14 +++++++------- src/Space.ts | 16 ++++++++-------- src/SpaceUpdate.ts | 6 +++--- src/Spaces.test.ts | 8 ++++---- src/Spaces.ts | 10 +++++----- src/react/useChannelState.ts | 18 +++++++++--------- src/react/useConnectionState.ts | 18 +++++++++--------- src/react/useCursors.test.tsx | 8 ++++---- src/react/useEventListener.ts | 8 ++++---- src/react/useLocations.test.tsx | 8 ++++---- src/react/useLocks.test.tsx | 8 ++++---- src/react/useMembers.test.tsx | 8 ++++---- src/react/useSpace.test.tsx | 4 ++-- src/types.ts | 6 +++--- src/utilities/test/fakes.ts | 10 +++++----- src/utilities/types.ts | 6 +++--- test/cdn-bundle/resources/test.html | 2 +- test/integration/utilities/setup.ts | 2 +- 35 files changed, 132 insertions(+), 132 deletions(-) rename __mocks__/ably/{promises => }/index.ts (92%) diff --git a/__mocks__/ably/promises/index.ts b/__mocks__/ably/index.ts similarity index 92% rename from __mocks__/ably/promises/index.ts rename to __mocks__/ably/index.ts index 61248461..05fdc678 100644 --- a/__mocks__/ably/promises/index.ts +++ b/__mocks__/ably/index.ts @@ -1,4 +1,4 @@ -import Ably, { Types } from 'ably/promises'; +import { PresenceMessage, Rest, ErrorInfo } from 'ably'; const MOCK_CLIENT_ID = 'MOCK_CLIENT_ID'; @@ -7,7 +7,7 @@ const methodReturningVoidPromise = () => mockPromisify((() => {})()); function createMockPresence() { return { - get: () => mockPromisify([]), + get: () => mockPromisify([]), update: () => mockPromisify(undefined), enter: methodReturningVoidPromise, leave: methodReturningVoidPromise, @@ -93,6 +93,6 @@ class MockRealtime { // maintain the PresenceMessage class so tests can initialise it directly using // PresenceMessage.fromValues. -MockRealtime.PresenceMessage = Ably.Rest.PresenceMessage; +MockRealtime.PresenceMessage = Rest.PresenceMessage; export { MockRealtime as Realtime }; diff --git a/demo/api/ably-token-request/index.ts b/demo/api/ably-token-request/index.ts index 56b09990..9e077434 100644 --- a/demo/api/ably-token-request/index.ts +++ b/demo/api/ably-token-request/index.ts @@ -1,5 +1,5 @@ import * as dotenv from 'dotenv'; -import * as Ably from 'ably/promises'; +import * as Ably from 'ably'; import { HandlerEvent } from '@netlify/functions'; dotenv.config(); diff --git a/demo/src/main.tsx b/demo/src/main.tsx index 743cbbff..30c02d54 100644 --- a/demo/src/main.tsx +++ b/demo/src/main.tsx @@ -15,7 +15,7 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) const clientId = nanoid(); -const client = new Realtime.Promise({ +const client = new Realtime({ authUrl: `/api/ably-token-request?clientId=${clientId}`, clientId, }); diff --git a/examples/avatar-stack.ts b/examples/avatar-stack.ts index d847be67..60c1a615 100644 --- a/examples/avatar-stack.ts +++ b/examples/avatar-stack.ts @@ -4,7 +4,7 @@ import { Realtime } from 'ably'; import { renderAvatars, renderNotification } from './my-application'; // Create Ably client -const client = new Realtime.Promise({ authUrl: '', clientId: '' }); +const client = new Realtime({ authUrl: '', clientId: '' }); // Initialize the Spaces SDK with an Ably client const spaces = new Spaces(client); diff --git a/examples/live-cursors.ts b/examples/live-cursors.ts index a319ff15..3866866c 100644 --- a/examples/live-cursors.ts +++ b/examples/live-cursors.ts @@ -4,7 +4,7 @@ import { Realtime } from 'ably'; import renderCursor from './my-application'; // Create Ably client -const client = new Realtime.Promise({ authUrl: '', clientId: '' }); +const client = new Realtime({ authUrl: '', clientId: '' }); // Initialize the Spaces SDK with an Ably client const spaces = new Spaces(client); diff --git a/examples/location.ts b/examples/location.ts index ba04835c..197caed6 100644 --- a/examples/location.ts +++ b/examples/location.ts @@ -4,7 +4,7 @@ import { Realtime } from 'ably'; import updateLocationsForMember from './my-application'; // Create Ably client -const client = new Realtime.Promise({ authUrl: '', clientId: '' }); +const client = new Realtime({ authUrl: '', clientId: '' }); // Initialize the Spaces SDK with an Ably client const spaces = new Spaces(client); diff --git a/examples/locking.ts b/examples/locking.ts index 13ff339c..41fb8148 100644 --- a/examples/locking.ts +++ b/examples/locking.ts @@ -4,7 +4,7 @@ import { Realtime } from 'ably'; import { enableLocationEditing, lockId } from './my-application'; // Create Ably client -const client = new Realtime.Promise({ authUrl: '', clientId: '' }); +const client = new Realtime({ authUrl: '', clientId: '' }); // Initialize the Spaces SDK with an Ably client const spaces = new Spaces(client); diff --git a/examples/locks.ts b/examples/locks.ts index 81c685df..686f8a5b 100644 --- a/examples/locks.ts +++ b/examples/locks.ts @@ -1,6 +1,6 @@ // An example of members locating at and locking elements in a slides // application. -import Ably from 'ably/promises'; +import Ably from 'ably'; import Spaces from '../dist/cjs/Spaces.js'; import { Lock, LockAttributes } from '../dist/cjs/index.js'; @@ -32,7 +32,7 @@ class SlideElement { // define a main async function since we can't use await at the top-level. const main = async () => { info('initialising Ably client'); - const client = new Ably.Realtime.Promise({ + const client = new Ably.Realtime({ key: process.env.ABLY_API_KEY, clientId: 'Alice', }); diff --git a/src/CursorBatching.ts b/src/CursorBatching.ts index 58e4f507..d782b786 100644 --- a/src/CursorBatching.ts +++ b/src/CursorBatching.ts @@ -1,4 +1,4 @@ -import { Types } from 'ably'; +import { RealtimeChannel } from 'ably'; import { CURSOR_UPDATE } from './CursorConstants.js'; import type { CursorUpdate } from './types.js'; @@ -27,7 +27,7 @@ export default class CursorBatching { this.batchTime = outboundBatchInterval; } - pushCursorPosition(channel: Types.RealtimeChannelPromise, cursor: Pick) { + pushCursorPosition(channel: RealtimeChannel, cursor: Pick) { // Ignore the cursor update if there is no one listening if (!this.shouldSend) return; @@ -60,14 +60,14 @@ export default class CursorBatching { this.outgoingBuffer.push(value); } - private async publishFromBuffer(channel: Types.RealtimeChannelPromise, eventName: string) { + private async publishFromBuffer(channel: RealtimeChannel, eventName: string) { if (!this.isRunning) { this.isRunning = true; await this.batchToChannel(channel, eventName); } } - private async batchToChannel(channel: Types.RealtimeChannelPromise, eventName: string) { + private async batchToChannel(channel: RealtimeChannel, eventName: string) { if (!this.hasMovement) { this.isRunning = false; return; diff --git a/src/CursorDispensing.ts b/src/CursorDispensing.ts index 3cb215b9..8af50385 100644 --- a/src/CursorDispensing.ts +++ b/src/CursorDispensing.ts @@ -1,5 +1,5 @@ import { type CursorUpdate } from './types.js'; -import { type RealtimeMessage } from './utilities/types.js'; +import { type RealtimeInboundMessage } from './utilities/types.js'; export default class CursorDispensing { private buffer: Record = {}; @@ -32,7 +32,7 @@ export default class CursorDispensing { ); } - processBatch(message: RealtimeMessage) { + processBatch(message: RealtimeInboundMessage) { const updates: { cursor: CursorUpdate; offset: number }[] = message.data || []; updates.forEach((update: { cursor: CursorUpdate; offset: number }) => { diff --git a/src/CursorHistory.ts b/src/CursorHistory.ts index 8a8e532a..268fc784 100644 --- a/src/CursorHistory.ts +++ b/src/CursorHistory.ts @@ -1,4 +1,4 @@ -import { Types } from 'ably'; +import { InboundMessage, PaginatedResult, RealtimeChannel } from 'ably'; import type { CursorUpdate } from './types.js'; import type { CursorsOptions } from './types.js'; @@ -29,14 +29,14 @@ export default class CursorHistory { private allCursorUpdates( connections: ConnectionsLastPosition, - page: Types.PaginatedResult, + page: PaginatedResult, ): ConnectionsLastPosition { return Object.fromEntries( Object.entries(connections).map(([connectionId, cursors]) => { const lastMessage = page.items.find((item) => item.connectionId === connectionId); if (!lastMessage) return [connectionId, cursors]; - const { data = [], clientId }: { data: OutgoingBuffer[] } & Pick = lastMessage; + const { data = [], clientId }: { data: OutgoingBuffer[] } & Pick = lastMessage; const lastPositionSet = data[data.length - 1]?.cursor; const lastUpdate = lastPositionSet @@ -54,7 +54,7 @@ export default class CursorHistory { } async getLastCursorUpdate( - channel: Types.RealtimeChannelPromise, + channel: RealtimeChannel, paginationLimit: CursorsOptions['paginationLimit'], ): Promise { const members = await channel.presence.get(); diff --git a/src/Cursors.test.ts b/src/Cursors.test.ts index 3f1daa60..383f4ae8 100644 --- a/src/Cursors.test.ts +++ b/src/Cursors.test.ts @@ -1,5 +1,5 @@ import { it, describe, expect, vi, beforeEach, vitest, afterEach } from 'vitest'; -import { Realtime, Types } from 'ably/promises'; +import { Realtime, RealtimeClient, RealtimeChannel } from 'ably'; import Space from './Space.js'; import Cursors from './Cursors.js'; @@ -10,22 +10,22 @@ import CursorDispensing from './CursorDispensing.js'; import CursorHistory from './CursorHistory.js'; import type { CursorUpdate, SpaceMember } from './types.js'; -import type { RealtimeMessage } from './utilities/types.js'; +import type { RealtimeInboundMessage } from './utilities/types.js'; interface CursorsTestContext { - client: Types.RealtimePromise; + client: RealtimeClient; space: Space; cursors: Cursors; - channel: Types.RealtimeChannelPromise; + channel: RealtimeChannel; batching: CursorBatching; dispensing: CursorDispensing; history: CursorHistory; selfStub: SpaceMember; lastCursorPositionsStub: Record; - fakeMessageStub: RealtimeMessage; + fakeMessageStub: RealtimeInboundMessage; } -vi.mock('ably/promises'); +vi.mock('ably'); function createPresenceCount(length: number) { return async () => Array.from({ length }, (_, i) => createPresenceMessage('enter', { clientId: '' + i })); @@ -39,7 +39,7 @@ describe('Cursors', () => { context.cursors = context.space.cursors; // This will set the channel context.cursors.subscribe('update', () => {}); - context.channel = context.cursors['channel'] as Types.RealtimeChannelPromise; + context.channel = context.cursors.channel!; context.batching = context.space.cursors['cursorBatching']; context.dispensing = context.space.cursors['cursorDispensing']; context.history = context.space.cursors['cursorHistory']; diff --git a/src/Cursors.ts b/src/Cursors.ts index 5760663b..7548f463 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -1,4 +1,4 @@ -import { Types } from 'ably'; +import { RealtimeChannel } from 'ably'; import Space from './Space.js'; import CursorBatching from './CursorBatching.js'; @@ -8,7 +8,7 @@ import CursorHistory from './CursorHistory.js'; import { CURSOR_UPDATE } from './CursorConstants.js'; import type { CursorsOptions, CursorUpdate } from './types.js'; -import type { RealtimeMessage } from './utilities/types.js'; +import type { RealtimeInboundMessage } from './utilities/types.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; /** @@ -44,7 +44,7 @@ export default class Cursors extends EventEmitter { /** * The [realtime channel](https://ably.com/docs/channels) instance that this `Cursors` instance uses for transmitting and receiving data. */ - public channel?: Types.RealtimeChannelPromise; + public channel?: RealtimeChannel; /** @internal */ constructor(private space: Space) { @@ -90,11 +90,11 @@ export default class Cursors extends EventEmitter { this.cursorBatching.pushCursorPosition(channel, cursor); } - private getChannel(): Types.RealtimeChannelPromise { + private getChannel(): RealtimeChannel { return this.channel ?? (this.channel = this.initializeCursorsChannel()); } - private initializeCursorsChannel(): Types.RealtimeChannelPromise { + private initializeCursorsChannel(): RealtimeChannel { const channel = this.space.client.channels.get(this.channelName); channel.presence.subscribe(this.onPresenceUpdate.bind(this)); channel.presence.enter(); @@ -111,7 +111,7 @@ export default class Cursors extends EventEmitter { private isUnsubscribed() { const channel = this.getChannel(); - interface ChannelWithSubscriptions extends Types.RealtimeChannelPromise { + interface ChannelWithSubscriptions extends RealtimeChannel { subscriptions: EventEmitter<{}>; } @@ -185,7 +185,7 @@ export default class Cursors extends EventEmitter { const channel = this.getChannel(); channel.subscribe(CURSOR_UPDATE, (message) => { - this.cursorDispensing.processBatch(message as RealtimeMessage); + this.cursorDispensing.processBatch(message as RealtimeInboundMessage); }); } } diff --git a/src/Locations.test.ts b/src/Locations.test.ts index b6c488d3..c5509e3d 100644 --- a/src/Locations.test.ts +++ b/src/Locations.test.ts @@ -1,5 +1,5 @@ import { it, describe, expect, vi, beforeEach } from 'vitest'; -import { Realtime, Types } from 'ably/promises'; +import { PresenceMessage, Realtime, RealtimeClient, RealtimePresence } from 'ably'; import Space from './Space.js'; @@ -11,13 +11,13 @@ import { } from './utilities/test/fakes.js'; interface SpaceTestContext { - client: Types.RealtimePromise; + client: RealtimeClient; space: Space; - presence: Types.RealtimePresencePromise; - presenceMap: Map; + presence: RealtimePresence; + presenceMap: Map; } -vi.mock('ably/promises'); +vi.mock('ably'); vi.mock('nanoid'); describe('Locations', () => { diff --git a/src/Locks.test.ts b/src/Locks.test.ts index 6cfaf670..409bf00a 100644 --- a/src/Locks.test.ts +++ b/src/Locks.test.ts @@ -1,18 +1,18 @@ import { it, describe, expect, vi, beforeEach } from 'vitest'; -import { Realtime, Types } from 'ably/promises'; +import { PresenceMessage, Realtime, RealtimeClient, RealtimePresence } from 'ably'; import Space from './Space.js'; import type { SpaceMember, LockStatus } from './types.js'; import { createPresenceMessage } from './utilities/test/fakes.js'; interface SpaceTestContext { - client: Types.RealtimePromise; + client: RealtimeClient; space: Space; - presence: Types.RealtimePresencePromise; - presenceMap: Map; + presence: RealtimePresence; + presenceMap: Map; } -vi.mock('ably/promises'); +vi.mock('ably'); describe('Locks', () => { beforeEach((context) => { diff --git a/src/Locks.ts b/src/Locks.ts index 2a20b4f9..8d73a21f 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -1,4 +1,4 @@ -import { Types } from 'ably'; +import { PresenceMessage } from 'ably'; import Space from './Space.js'; import type { Lock, SpaceMember } from './types.js'; @@ -350,7 +350,7 @@ export default class Locks extends EventEmitter { } /** @internal */ - async processPresenceMessage(message: Types.PresenceMessage) { + async processPresenceMessage(message: PresenceMessage) { const member = await this.space.members.getByConnectionId(message.connectionId); if (!member) return; diff --git a/src/Members.test.ts b/src/Members.test.ts index 08203d23..2cb1b853 100644 --- a/src/Members.test.ts +++ b/src/Members.test.ts @@ -1,18 +1,18 @@ import { it, describe, expect, vi, beforeEach, afterEach } from 'vitest'; -import { Types, Realtime } from 'ably/promises'; +import { PresenceMessage, Realtime, RealtimeClient, RealtimePresence } from 'ably'; import Space from './Space.js'; import { createPresenceEvent, createSpaceMember, createProfileUpdate } from './utilities/test/fakes.js'; interface SpaceTestContext { - client: Types.RealtimePromise; + client: RealtimeClient; space: Space; - presence: Types.RealtimePresencePromise; - presenceMap: Map; + presence: RealtimePresence; + presenceMap: Map; } -vi.mock('ably/promises'); +vi.mock('ably'); vi.mock('nanoid'); describe('Members', () => { diff --git a/src/Space.test.ts b/src/Space.test.ts index 7a69ad01..b6adfaa3 100644 --- a/src/Space.test.ts +++ b/src/Space.test.ts @@ -1,5 +1,5 @@ import { it, describe, expect, vi, beforeEach, expectTypeOf } from 'vitest'; -import { Realtime, Types } from 'ably/promises'; +import { PresenceMessage, Realtime, RealtimeClient, RealtimePresence } from 'ably'; import Space from './Space.js'; import Locations from './Locations.js'; @@ -14,13 +14,13 @@ import { } from './utilities/test/fakes.js'; interface SpaceTestContext { - client: Types.RealtimePromise; + client: RealtimeClient; space: Space; - presence: Types.RealtimePresencePromise; - presenceMap: Map; + presence: RealtimePresence; + presenceMap: Map; } -vi.mock('ably/promises'); +vi.mock('ably'); vi.mock('nanoid'); describe('Space', () => { @@ -62,7 +62,7 @@ describe('Space', () => { describe('enter', () => { beforeEach(({ presence }) => { vi.spyOn(presence, 'subscribe').mockImplementation( - async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => { + async (_, listener?: (presenceMessage: PresenceMessage) => void) => { listener!( createPresenceMessage('enter' /* arbitrarily chosen */, { clientId: 'MOCK_CLIENT_ID', connectionId: '1' }), ); @@ -99,7 +99,7 @@ describe('Space', () => { const unsubscribeSpy = vi.spyOn(presence, 'unsubscribe'); vi.spyOn(presence, 'subscribe').mockImplementation( - async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => { + async (_, listener?: (presenceMessage: PresenceMessage) => void) => { listener!(createPresenceMessage('enter' /* arbitrarily chosen */, presenceMessageData)); }, ); diff --git a/src/Space.ts b/src/Space.ts index 1cd15f59..e3a40fe0 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -1,4 +1,4 @@ -import Ably, { Types } from 'ably'; +import { PresenceMessage, Realtime, RealtimeClient, RealtimeChannel } from 'ably'; import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import Locations from './Locations.js'; @@ -85,7 +85,7 @@ class Space extends EventEmitter { /** * @internal */ - readonly client: Types.RealtimePromise; + readonly client: RealtimeClient; private readonly channelName: string; /** * @internal @@ -110,7 +110,7 @@ class Space extends EventEmitter { /** * The [realtime channel instance](https://ably.com/docs/channels) that this `Space` instance uses for transmitting and receiving data. */ - readonly channel: Types.RealtimeChannelPromise; + readonly channel: RealtimeChannel; /** * An instance of {@link Locks}. */ @@ -121,7 +121,7 @@ class Space extends EventEmitter { readonly name: string; /** @internal */ - constructor(name: string, client: Types.RealtimePromise, options?: Subset) { + constructor(name: string, client: RealtimeClient, options?: Subset) { super(); this.client = client; @@ -144,21 +144,21 @@ class Space extends EventEmitter { if (!extras) { return this.channel.presence.update(data); } - return this.channel.presence.update(Ably.Realtime.PresenceMessage.fromValues({ data, extras })); + return this.channel.presence.update(Realtime.PresenceMessage.fromValues({ data, extras })); }; private presenceEnter = ({ data, extras }: SpacePresenceData) => { if (!extras) { return this.channel.presence.enter(data); } - return this.channel.presence.enter(Ably.Realtime.PresenceMessage.fromValues({ data, extras })); + return this.channel.presence.enter(Realtime.PresenceMessage.fromValues({ data, extras })); }; private presenceLeave = ({ data, extras }: SpacePresenceData) => { if (!extras) { return this.channel.presence.leave(data); } - return this.channel.presence.leave(Ably.Realtime.PresenceMessage.fromValues({ data, extras })); + return this.channel.presence.leave(Realtime.PresenceMessage.fromValues({ data, extras })); }; private setOptions(options?: Subset): SpaceOptions { @@ -209,7 +209,7 @@ class Space extends EventEmitter { return new Promise((resolve) => { const presence = this.channel.presence; - const presenceListener = async (presenceMessage: Types.PresenceMessage) => { + const presenceListener = async (presenceMessage: PresenceMessage) => { if ( !( presenceMessage.clientId == this.client.auth.clientId && diff --git a/src/SpaceUpdate.ts b/src/SpaceUpdate.ts index 11cebaec..91e646a8 100644 --- a/src/SpaceUpdate.ts +++ b/src/SpaceUpdate.ts @@ -1,5 +1,5 @@ import { nanoid } from 'nanoid'; -import { Types } from 'ably'; +import { PresenceMessage } from 'ably'; import type { SpaceMember, ProfileData } from './types.js'; import type { PresenceMember } from './utilities/types.js'; @@ -11,9 +11,9 @@ export interface SpacePresenceData { class SpaceUpdate { private self: SpaceMember | null; - private extras: Types.PresenceMessage['extras']; + private extras: PresenceMessage['extras']; - constructor({ self, extras }: { self: SpaceMember | null; extras?: Types.PresenceMessage['extras'] }) { + constructor({ self, extras }: { self: SpaceMember | null; extras?: PresenceMessage['extras'] }) { this.self = self; this.extras = extras; } diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index 41d80dc3..326fa4c8 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -1,5 +1,5 @@ import { it, describe, expect, expectTypeOf, vi, beforeEach } from 'vitest'; -import { Realtime, Types } from 'ably/promises'; +import { Realtime, RealtimeClient } from 'ably'; import Spaces, { type ClientWithOptions } from './Spaces.js'; @@ -7,16 +7,16 @@ interface SpacesTestContext { client: ClientWithOptions; } -vi.mock('ably/promises'); +vi.mock('ably'); describe('Spaces', () => { beforeEach((context) => { context.client = new Realtime({ key: 'asd' }) as ClientWithOptions; }); - it('expects the injected client to be of the type RealtimePromise', ({ client }) => { + it('expects the injected client to be of the type RealtimeClient', ({ client }) => { const spaces = new Spaces(client); - expectTypeOf(spaces.client).toMatchTypeOf(); + expectTypeOf(spaces.client).toMatchTypeOf(); }); it('creates and retrieves spaces successfully', async ({ client }) => { diff --git a/src/Spaces.ts b/src/Spaces.ts index 6117aff1..bd213966 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -1,4 +1,4 @@ -import { Types } from 'ably'; +import { RealtimeClient, Connection } from 'ably'; import { ERR_SPACE_NAME_MISSING } from './Errors.js'; import Space from './Space.js'; @@ -8,7 +8,7 @@ import type { Subset } from './utilities/types.js'; import { VERSION } from './version.js'; -export interface ClientWithOptions extends Types.RealtimePromise { +export interface ClientWithOptions extends RealtimeClient { options: { agents?: Record; }; @@ -24,11 +24,11 @@ class Spaces { /** * Instance of the [Ably realtime client](https://ably.com/docs/getting-started/setup) client that was passed to the {@link constructor}. */ - client: Types.RealtimePromise; + client: RealtimeClient; /** * Instance of the [Ably realtime client](https://ably.com/docs/getting-started/setup) connection, belonging to the client that was passed to the {@link constructor}. */ - connection: Types.ConnectionPromise; + connection: Connection; /** * Version of the Spaces library. @@ -42,7 +42,7 @@ class Spaces { * * @param client An instance of the Ably prmise-based realtime client. */ - constructor(client: Types.RealtimePromise) { + constructor(client: RealtimeClient) { this.client = client; this.connection = client.connection; this.addAgent((this.client as ClientWithOptions)['options']); diff --git a/src/react/useChannelState.ts b/src/react/useChannelState.ts index a3a39b12..44c1bdf0 100644 --- a/src/react/useChannelState.ts +++ b/src/react/useChannelState.ts @@ -1,22 +1,22 @@ import { useState } from 'react'; import { useEventListener } from './useEventListener.js'; -import type { Types } from 'ably'; +import type { ChannelState, ChannelStateChange, ErrorInfo, EventEmitter } from 'ably'; -type ChannelStateListener = (stateChange: Types.ChannelStateChange) => void; +type ChannelStateListener = (stateChange: ChannelStateChange) => void; -const failedStateEvents: Types.ChannelState[] = ['suspended', 'failed', 'detached']; -const successStateEvents: Types.ChannelState[] = ['attached']; +const failedStateEvents: ChannelState[] = ['suspended', 'failed', 'detached']; +const successStateEvents: ChannelState[] = ['attached']; /** * todo use `ably/react` hooks instead */ -export const useChannelState = ( - emitter?: Types.EventEmitter, +export const useChannelState = ( + emitter?: EventEmitter, ) => { - const [channelError, setChannelError] = useState(null); + const [channelError, setChannelError] = useState(null); - useEventListener( + useEventListener( emitter, (stateChange) => { if (stateChange.reason) { @@ -26,7 +26,7 @@ export const useChannelState = ( + useEventListener( emitter, () => { setChannelError(null); diff --git a/src/react/useConnectionState.ts b/src/react/useConnectionState.ts index ff8271f6..4b5315b9 100644 --- a/src/react/useConnectionState.ts +++ b/src/react/useConnectionState.ts @@ -1,19 +1,19 @@ import { useState } from 'react'; import { useEventListener } from './useEventListener.js'; -import type { Types } from 'ably'; +import type { ConnectionState, ConnectionStateChange, ErrorInfo, EventEmitter } from 'ably'; -type ConnectionStateListener = (stateChange: Types.ConnectionStateChange) => void; +type ConnectionStateListener = (stateChange: ConnectionStateChange) => void; -const failedStateEvents: Types.ConnectionState[] = ['suspended', 'failed', 'disconnected']; -const successStateEvents: Types.ConnectionState[] = ['connected', 'closed']; +const failedStateEvents: ConnectionState[] = ['suspended', 'failed', 'disconnected']; +const successStateEvents: ConnectionState[] = ['connected', 'closed']; -export const useConnectionState = ( - emitter?: Types.EventEmitter, +export const useConnectionState = ( + emitter?: EventEmitter, ) => { - const [connectionError, setConnectionError] = useState(null); + const [connectionError, setConnectionError] = useState(null); - useEventListener( + useEventListener( emitter, (stateChange) => { if (stateChange.reason) { @@ -23,7 +23,7 @@ export const useConnectionState = ( + useEventListener( emitter, () => { setConnectionError(null); diff --git a/src/react/useCursors.test.tsx b/src/react/useCursors.test.tsx index 8337b7e3..766df2af 100644 --- a/src/react/useCursors.test.tsx +++ b/src/react/useCursors.test.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; -import { Realtime } from 'ably/promises'; +import { Realtime } from 'ably'; import { it, beforeEach, describe, expect, vi } from 'vitest'; import { waitFor, renderHook, act } from '@testing-library/react'; import { SpacesProvider } from './contexts/SpacesContext.js'; @@ -12,15 +12,15 @@ import Spaces from '../index.js'; import Space from '../Space.js'; import { createPresenceEvent } from '../utilities/test/fakes.js'; import { useCursors } from './useCursors.js'; -import type { Types } from 'ably'; +import type { PresenceMessage } from 'ably'; interface SpaceTestContext { spaces: Spaces; space: Space; - presenceMap: Map; + presenceMap: Map; } -vi.mock('ably/promises'); +vi.mock('ably'); vi.mock('nanoid'); describe('useCursors', () => { diff --git a/src/react/useEventListener.ts b/src/react/useEventListener.ts index 4b69665b..d999e9b8 100644 --- a/src/react/useEventListener.ts +++ b/src/react/useEventListener.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; -import type { Types } from 'ably'; +import type { ChannelState, ChannelStateChange, ConnectionState, ConnectionStateChange, EventEmitter } from 'ably'; type EventListener = (stateChange: T) => void; @@ -8,10 +8,10 @@ type EventListener = (stateChange: T) => void; * todo use `ably/react` hooks instead */ export const useEventListener = < - S extends Types.ConnectionState | Types.ChannelState, - C extends Types.ConnectionStateChange | Types.ChannelStateChange, + S extends ConnectionState | ChannelState, + C extends ConnectionStateChange | ChannelStateChange, >( - emitter?: Types.EventEmitter, C, S>, + emitter?: EventEmitter, C, S>, listener?: EventListener, event?: S | S[], ) => { diff --git a/src/react/useLocations.test.tsx b/src/react/useLocations.test.tsx index a569379c..2167571e 100644 --- a/src/react/useLocations.test.tsx +++ b/src/react/useLocations.test.tsx @@ -3,12 +3,12 @@ */ import React from 'react'; -import { Realtime } from 'ably/promises'; +import { Realtime } from 'ably'; import { it, beforeEach, describe, expect, vi } from 'vitest'; import { waitFor, renderHook } from '@testing-library/react'; import { SpacesProvider } from './contexts/SpacesContext.js'; import { SpaceProvider } from './contexts/SpaceContext.js'; -import type { Types } from 'ably'; +import type { PresenceMessage } from 'ably'; import Spaces from '../index.js'; import { createLocationUpdate, createPresenceEvent } from '../utilities/test/fakes.js'; import Space from '../Space.js'; @@ -17,10 +17,10 @@ import { useLocations } from './useLocations.js'; interface SpaceTestContext { spaces: Spaces; space: Space; - presenceMap: Map; + presenceMap: Map; } -vi.mock('ably/promises'); +vi.mock('ably'); vi.mock('nanoid'); describe('useLocations', () => { diff --git a/src/react/useLocks.test.tsx b/src/react/useLocks.test.tsx index 8ad080d7..ba27abfd 100644 --- a/src/react/useLocks.test.tsx +++ b/src/react/useLocks.test.tsx @@ -3,12 +3,12 @@ */ import React from 'react'; -import { Realtime } from 'ably/promises'; +import { Realtime } from 'ably'; import { it, beforeEach, describe, expect, vi } from 'vitest'; import { waitFor, renderHook } from '@testing-library/react'; import { SpacesProvider } from './contexts/SpacesContext.js'; import { SpaceProvider } from './contexts/SpaceContext.js'; -import type { Types } from 'ably'; +import type { PresenceMessage } from 'ably'; import Spaces from '../index.js'; import { createPresenceEvent } from '../utilities/test/fakes.js'; import Space from '../Space.js'; @@ -17,10 +17,10 @@ import { useLocks } from './useLocks.js'; interface SpaceTestContext { spaces: Spaces; space: Space; - presenceMap: Map; + presenceMap: Map; } -vi.mock('ably/promises'); +vi.mock('ably'); vi.mock('nanoid'); describe('useLocks', () => { diff --git a/src/react/useMembers.test.tsx b/src/react/useMembers.test.tsx index 00653f4e..44cf7cc7 100644 --- a/src/react/useMembers.test.tsx +++ b/src/react/useMembers.test.tsx @@ -3,12 +3,12 @@ */ import React from 'react'; -import { Realtime } from 'ably/promises'; +import { Realtime } from 'ably'; import { it, beforeEach, describe, expect, vi } from 'vitest'; import { waitFor, renderHook } from '@testing-library/react'; import { SpacesProvider } from './contexts/SpacesContext.js'; import { SpaceProvider } from './contexts/SpaceContext.js'; -import type { Types } from 'ably'; +import type { PresenceMessage } from 'ably'; import Spaces from '../index.js'; import { useMembers } from './useMembers.js'; import { createLocationUpdate, createPresenceEvent } from '../utilities/test/fakes.js'; @@ -17,10 +17,10 @@ import Space from '../Space.js'; interface SpaceTestContext { spaces: Spaces; space: Space; - presenceMap: Map; + presenceMap: Map; } -vi.mock('ably/promises'); +vi.mock('ably'); vi.mock('nanoid'); describe('useMembers', () => { diff --git a/src/react/useSpace.test.tsx b/src/react/useSpace.test.tsx index af6baf93..af12bd6a 100644 --- a/src/react/useSpace.test.tsx +++ b/src/react/useSpace.test.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; -import { Realtime } from 'ably/promises'; +import { Realtime } from 'ably'; import { it, beforeEach, describe, expect, vi } from 'vitest'; import { waitFor, renderHook } from '@testing-library/react'; import { SpacesProvider } from './contexts/SpacesContext.js'; @@ -15,7 +15,7 @@ interface SpaceTestContext { spaces: Spaces; } -vi.mock('ably/promises'); +vi.mock('ably'); vi.mock('nanoid'); describe('useSpace', () => { diff --git a/src/types.ts b/src/types.ts index afdfff5a..e5866a48 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Types } from 'ably'; +import { ErrorInfo, PresenceAction } from 'ably'; import type { LockAttributes } from './Locks.js'; /** @@ -143,7 +143,7 @@ export interface SpaceMember { /** * The most recent event emitted by the member. */ - name: Types.PresenceAction; + name: PresenceAction; /** * The timestamp of the most recently emitted event. */ @@ -201,5 +201,5 @@ export type Lock = { /** * The reason why the lock status is {@link LockStatuses.Unlocked | `unlocked`}. */ - reason?: Types.ErrorInfo; + reason?: ErrorInfo; }; diff --git a/src/utilities/test/fakes.ts b/src/utilities/test/fakes.ts index 5d3d59ba..789ec52d 100644 --- a/src/utilities/test/fakes.ts +++ b/src/utilities/test/fakes.ts @@ -1,4 +1,4 @@ -import { Types } from 'ably'; +import { PresenceMessage } from 'ably'; import Space from '../../Space.js'; @@ -8,7 +8,7 @@ import type { PresenceMember } from '../types.js'; // import { nanoidId } from '../../../__mocks__/nanoid/index.js'; const nanoidId = 'NanoidID'; -const enterPresenceMessage: Types.PresenceMessage = { +const enterPresenceMessage: PresenceMessage = { clientId: '1', data: { profileUpdate: { @@ -29,12 +29,12 @@ const enterPresenceMessage: Types.PresenceMessage = { timestamp: 1, }; -const updatePresenceMessage: Types.PresenceMessage = { +const updatePresenceMessage: PresenceMessage = { ...enterPresenceMessage, action: 'update', }; -const leavePresenceMessage: Types.PresenceMessage = { +const leavePresenceMessage: PresenceMessage = { ...enterPresenceMessage, action: 'leave', }; @@ -60,7 +60,7 @@ const createPresenceMessage = (type: T, override?: P const createPresenceEvent = async ( space: Space, - presenceMap: Map, + presenceMap: Map, type: T, override?: Partial, ) => { diff --git a/src/utilities/types.ts b/src/utilities/types.ts index e3e59768..d5c376c4 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -1,4 +1,4 @@ -import type { Types } from 'ably'; +import type { InboundMessage, PresenceMessage } from 'ably'; import type { ProfileData, Lock } from '../types.js'; @@ -17,7 +17,7 @@ export type PresenceMember = { extras?: { locks: Lock[]; }; -} & Omit; +} & Omit; /** * Given an object type `T`, `Subset` represents an object which has the same shape as `T`, but with some keys (at any level of nesting) potentially absent. @@ -28,6 +28,6 @@ export type Subset = { [attr in keyof T]?: T[attr] extends object ? Subset : T[attr]; }; -export type RealtimeMessage = Omit & { +export type RealtimeInboundMessage = Omit & { connectionId: string; }; diff --git a/test/cdn-bundle/resources/test.html b/test/cdn-bundle/resources/test.html index a2526d08..556d75f5 100644 --- a/test/cdn-bundle/resources/test.html +++ b/test/cdn-bundle/resources/test.html @@ -19,7 +19,7 @@ const key = await createSandboxAblyAPIKey(); // The next two statements are based on the example given in the "Using a CDN" section of the README. - const client = new Ably.Realtime.Promise({ + const client = new Ably.Realtime({ key, environment: 'sandbox', clientId: 'myClientId', diff --git a/test/integration/utilities/setup.ts b/test/integration/utilities/setup.ts index 6400e953..8e8a9fb4 100644 --- a/test/integration/utilities/setup.ts +++ b/test/integration/utilities/setup.ts @@ -24,7 +24,7 @@ export async function createClients({ count }: { count: number }) { return Array.from({ length: count }, () => { const clientId = nanoid(); - const realtime = new Realtime.Promise({ + const realtime = new Realtime({ environment: 'sandbox', key: sandboxKey, clientId: clientId, From 20e580e7bad964561eb9f19350294d8147026c5f Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:01:09 +0100 Subject: [PATCH 178/191] Update docs re ably-js v2 API --- README.md | 4 ++-- docs/connection-and-channel-management.md | 4 ++-- docs/react.md | 2 +- docs/usage.md | 8 ++++---- src/Spaces.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 54dfbb00..bc7553fb 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ To instantiate the Spaces SDK, create an [Ably client](https://ably.com/docs/get import Spaces from '@ably/spaces'; import { Realtime } from 'ably'; -const client = new Realtime.Promise({ key: "", clientId: "" }); +const client = new Realtime({ key: "", clientId: "" }); const spaces = new Spaces(client); ``` You can use [basic authentication](https://ably.com/docs/auth/basic) i.e. the API Key directly for testing purposes, however it is strongly recommended that you use [token authentication](https://ably.com/docs/auth/token) in production environments. @@ -95,7 +95,7 @@ You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): After this, instantiate the SDK in the same way as in the NPM option above: ```ts -const client = new Ably.Realtime.Promise({ key: "", clientId: "" }); +const client = new Ably.Realtime({ key: "", clientId: "" }); const spaces = new Spaces(client); ``` diff --git a/docs/connection-and-channel-management.md b/docs/connection-and-channel-management.md index d72dd016..1c64aec2 100644 --- a/docs/connection-and-channel-management.md +++ b/docs/connection-and-channel-management.md @@ -9,7 +9,7 @@ This document describes how to access a connection and channels on Spaces, and w When initializing the Spaces SDK, an Ably client is passed as a required argument: ```ts -const client = new Realtime.Promise({ key: "", clientId: "" }); +const client = new Realtime({ key: "", clientId: "" }); const spaces = new Spaces(client); ``` @@ -50,4 +50,4 @@ mySpace.cursors.subscribe('update', () => {}); - [Channels](https://ably.com/docs/channels) - [Channel states](https://ably.com/docs/channels#states) -- [Handle channel failure](https://ably.com/docs/channels#failure) \ No newline at end of file +- [Handle channel failure](https://ably.com/docs/channels#failure) diff --git a/docs/react.md b/docs/react.md index 69b320ee..87b55ac0 100644 --- a/docs/react.md +++ b/docs/react.md @@ -43,7 +43,7 @@ import { Realtime } from "ably"; import Spaces from "@ably/spaces"; import { SpacesProvider, SpaceProvider } from "@ably/spaces/react"; -const ably = new Realtime.Promise({ key: "your-ably-api-key", clientId: 'me' }); +const ably = new Realtime({ key: "your-ably-api-key", clientId: 'me' }); const spaces = new Spaces(ably); root.render( diff --git a/docs/usage.md b/docs/usage.md index 02c47d95..c48c4059 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -39,19 +39,19 @@ You can also use Spaces with a CDN like [unpkg](https://www.unpkg.com/): > **Note** > -> If you do this, then replace the call to `new Realtime.Promise` in the next section with `new Ably.Realtime.Promise`. +> If you do this, then replace the call to `new Realtime` in the next section with `new Ably.Realtime`. ## Authentication and instantiation -Spaces use an [Ably promise-based realtime client](https://github.com/ably/ably-js#using-the-async-api-style). You can pass an Ably client directly to the spaces constructor. +You can pass an Ably client directly to the spaces constructor. The Ably client instantiation accepts client options. You will need at minimum an [Ably API key](#ably-api-key) and a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascripts). A clientId represents an identity of an connection. In most cases this will something like the id of a user: ```ts -import { Realtime } from 'ably/promise'; +import { Realtime } from 'ably'; import Spaces from '@ably/spaces'; -const client = new Realtime.Promise({ key: "", clientId: "" }); +const client = new Realtime({ key: "", clientId: "" }); const spaces = new Spaces(client); ``` diff --git a/src/Spaces.ts b/src/Spaces.ts index bd213966..1ffe3bef 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -36,7 +36,7 @@ class Spaces { readonly version = VERSION; /** - * Create a new instance of the Spaces SDK by passing an instance of the [Ably promise-based realtime client](https://ably.com/docs/getting-started/setup). A [`clientId`](https://ably.com/docs/auth/identified-clients) is required. + * Create a new instance of the Spaces SDK by passing an instance of the [Ably realtime client](https://ably.com/docs/getting-started/setup). A [`clientId`](https://ably.com/docs/auth/identified-clients) is required. * * An Ably API key is needed to authenticate. [Basic authentication](https://ably.com/docs/auth/basic) may be used for convenience, however Ably strongly recommends you use [token authentication](https://ably.com/docs/auth/token) in a production environment. * From 6db9f27fc130b4395fa97765b1fccb88aadabd5c Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:01:55 +0100 Subject: [PATCH 179/191] Fix 'ably' mock does not export ErrorInfo --- __mocks__/ably/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__mocks__/ably/index.ts b/__mocks__/ably/index.ts index 05fdc678..a3c455ba 100644 --- a/__mocks__/ably/index.ts +++ b/__mocks__/ably/index.ts @@ -91,8 +91,10 @@ class MockRealtime { } } +class MockErrorInfo extends ErrorInfo {} + // maintain the PresenceMessage class so tests can initialise it directly using // PresenceMessage.fromValues. MockRealtime.PresenceMessage = Rest.PresenceMessage; -export { MockRealtime as Realtime }; +export { MockRealtime as Realtime, MockErrorInfo as ErrorInfo }; From e6ef190e2c61d63aacda8195f5facc02accae7f7 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:04:43 +0100 Subject: [PATCH 180/191] Fix 'clientId' is undefined for RealtimeMessage `clientId` prop was made optional in ably-js v2 [1] to be consistent with the feature spec. However, spaces SDK requires `clientId` to be always set, so we can expect it to be present. [1] https://github.com/ably/ably-js/commit/4e3733f427fe70754dc36a6a9928182c79e81d72 --- src/CursorDispensing.ts | 2 +- src/utilities/types.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CursorDispensing.ts b/src/CursorDispensing.ts index 8af50385..45f0718a 100644 --- a/src/CursorDispensing.ts +++ b/src/CursorDispensing.ts @@ -36,7 +36,7 @@ export default class CursorDispensing { const updates: { cursor: CursorUpdate; offset: number }[] = message.data || []; updates.forEach((update: { cursor: CursorUpdate; offset: number }) => { - const enhancedMsg = { + const enhancedMsg: { cursor: CursorUpdate; offset: number } = { cursor: { clientId: message.clientId, connectionId: message.connectionId, diff --git a/src/utilities/types.ts b/src/utilities/types.ts index d5c376c4..22eada31 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -28,6 +28,7 @@ export type Subset = { [attr in keyof T]?: T[attr] extends object ? Subset : T[attr]; }; -export type RealtimeInboundMessage = Omit & { +export type RealtimeInboundMessage = Omit & { + clientId: string; connectionId: string; }; From 8d11078758467b9a0ccd931fd05ff93edc54d578 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:10:13 +0100 Subject: [PATCH 181/191] Update react demo app to use ChannelProvider --- demo/src/components/Paragraph.tsx | 29 +++++++++++++++++++--- demo/src/components/Title.tsx | 34 ++++++++++++++++++++++++-- demo/src/hooks/useTextComponentLock.ts | 13 +++++++--- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/demo/src/components/Paragraph.tsx b/demo/src/components/Paragraph.tsx index 02512f06..fd8888dd 100644 --- a/demo/src/components/Paragraph.tsx +++ b/demo/src/components/Paragraph.tsx @@ -1,10 +1,13 @@ import React, { useRef } from 'react'; import cn from 'classnames'; -import { getMemberFirstName, getOutlineClasses } from '../utils'; +import { ChannelProvider } from 'ably/react'; + +import { generateSpaceName, getMemberFirstName, getOutlineClasses, getParamValueFromUrl } from '../utils'; import { StickyLabel } from './StickyLabel'; import { LockFilledSvg } from './svg/LockedFilled.tsx'; import { EditableText } from './EditableText.tsx'; import { useTextComponentLock } from '../hooks/useTextComponentLock.ts'; +import { buildLockId } from '../utils/locking.ts'; interface Props extends React.HTMLAttributes { id: string; @@ -14,18 +17,38 @@ interface Props extends React.HTMLAttributes { maxlength?: number; } -export const Paragraph = ({ +export const Paragraph = (props: Props) => { + const spaceName = getParamValueFromUrl('space', generateSpaceName); + const lockId = buildLockId(props.slide, props.id); + const channelName = `${spaceName}${lockId}`; + + return ( + + + + ); +}; + +const ParagraphChild = ({ variant = 'regular', id, slide, className, children, maxlength = 300, + channelName, ...props -}: Props) => { +}: Props & { channelName: string }) => { const containerRef = useRef(null); const { content, activeMember, locked, lockedByYou, editIsNotAllowed, handleSelect, handleContentUpdate } = useTextComponentLock({ + channelName, id, slide, defaultText: children, diff --git a/demo/src/components/Title.tsx b/demo/src/components/Title.tsx index 2c24d714..47dfa38e 100644 --- a/demo/src/components/Title.tsx +++ b/demo/src/components/Title.tsx @@ -1,11 +1,13 @@ import React, { useRef } from 'react'; import cn from 'classnames'; +import { ChannelProvider } from 'ably/react'; -import { getMemberFirstName, getOutlineClasses } from '../utils'; +import { generateSpaceName, getMemberFirstName, getOutlineClasses, getParamValueFromUrl } from '../utils'; import { LockFilledSvg } from './svg/LockedFilled.tsx'; import { StickyLabel } from './StickyLabel.tsx'; import { EditableText } from './EditableText.tsx'; import { useTextComponentLock } from '../hooks/useTextComponentLock.ts'; +import { buildLockId } from '../utils/locking.ts'; interface Props extends React.HTMLAttributes { id: string; @@ -15,10 +17,38 @@ interface Props extends React.HTMLAttributes { maxlength?: number; } -export const Title = ({ variant = 'h1', className, id, slide, children, maxlength = 70, ...props }: Props) => { +export const Title = (props: Props) => { + const spaceName = getParamValueFromUrl('space', generateSpaceName); + const lockId = buildLockId(props.slide, props.id); + const channelName = `${spaceName}${lockId}`; + + return ( + + + + ); +}; + +const TitleChild = ({ + variant = 'h1', + className, + id, + slide, + children, + maxlength = 70, + channelName, + ...props +}: Props & { channelName: string }) => { const containerRef = useRef(null); const { content, activeMember, locked, lockedByYou, editIsNotAllowed, handleSelect, handleContentUpdate } = useTextComponentLock({ + channelName, id, slide, defaultText: children, diff --git a/demo/src/hooks/useTextComponentLock.ts b/demo/src/hooks/useTextComponentLock.ts index c175d67a..b75761a2 100644 --- a/demo/src/hooks/useTextComponentLock.ts +++ b/demo/src/hooks/useTextComponentLock.ts @@ -2,28 +2,33 @@ import { MutableRefObject, useCallback } from 'react'; import { useChannel } from 'ably/react'; import { useMembers, useLock } from '@ably/spaces/react'; import sanitize from 'sanitize-html'; -import { findActiveMember, generateSpaceName, getParamValueFromUrl } from '../utils'; +import { findActiveMember } from '../utils'; import { buildLockId } from '../utils/locking.ts'; import { usePreview } from '../components/PreviewContext.tsx'; import { useClearOnFailedLock, useClickOutside, useElementSelect } from './useElementSelect.ts'; import { useSlideElementContent } from './useSlideElementContent.ts'; interface UseTextComponentLockArgs { + channelName: string; id: string; slide: string; defaultText: string; containerRef: MutableRefObject; } -export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: UseTextComponentLockArgs) => { - const spaceName = getParamValueFromUrl('space', generateSpaceName); +export const useTextComponentLock = ({ + channelName, + id, + slide, + defaultText, + containerRef, +}: UseTextComponentLockArgs) => { const { members, self } = useMembers(); const activeMember = findActiveMember(id, slide, members); const lockId = buildLockId(slide, id); const { status, member } = useLock(lockId); const locked = status === 'locked'; const lockedByYou = locked && self?.connectionId === member?.connectionId; - const channelName = `[?rewind=1]${spaceName}${lockId}`; const [content, updateContent] = useSlideElementContent(lockId, defaultText); const preview = usePreview(); From 759603e1867af22298b942b0399b64928c2c2f37 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:12:51 +0100 Subject: [PATCH 182/191] Fix PaginatedResult.next() usage PaginatedResult.next() type was updated in ably-js v2 [1] to return null in case of no page. [1] https://github.com/ably/ably-js/commit/8bd17068f8e444f36ad86ce13ffecf0b4ba04456 --- src/CursorHistory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CursorHistory.ts b/src/CursorHistory.ts index 268fc784..e0d9aa57 100644 --- a/src/CursorHistory.ts +++ b/src/CursorHistory.ts @@ -76,7 +76,8 @@ export default class CursorHistory { pageNo++; while (pageNo <= paginationLimit && this.positionsMissing(connections) && history.hasNext()) { - page = await history.next(); + // assert result of .next() is non null as we've checked .hasNext() before + page = (await history.next())!; connections = this.allCursorUpdates(connections, page); pageNo++; } From 551f397c8fe422b723b8d55168d10b2e2345be74 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:15:10 +0100 Subject: [PATCH 183/191] Fix `Message.data` is undefined `Message.data` was made optional in ably-js v2 [1] [1] https://github.com/ably/ably-js/commit/4e3733f427fe70754dc36a6a9928182c79e81d72 --- src/CursorHistory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CursorHistory.ts b/src/CursorHistory.ts index e0d9aa57..580226d1 100644 --- a/src/CursorHistory.ts +++ b/src/CursorHistory.ts @@ -36,7 +36,7 @@ export default class CursorHistory { const lastMessage = page.items.find((item) => item.connectionId === connectionId); if (!lastMessage) return [connectionId, cursors]; - const { data = [], clientId }: { data: OutgoingBuffer[] } & Pick = lastMessage; + const { data = [], clientId }: { data?: OutgoingBuffer[] } & Pick = lastMessage; const lastPositionSet = data[data.length - 1]?.cursor; const lastUpdate = lastPositionSet From 3d46b66684132e6460d9018ece06e41dc3a82730 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:52:00 +0100 Subject: [PATCH 184/191] Update types in integration.test.ts --- test/integration/integration.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index b4072878..5fa0f12e 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -1,11 +1,12 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { createClients } from './utilities/setup.js'; import { LocationsEventMap } from '../../src/Locations.js'; -import { SpaceEventMap } from '../../src/Space.js'; +import Space, { SpaceEventMap } from '../../src/Space.js'; import { MembersEventMap } from '../../src/Members.js'; import { CursorsEventMap } from '../../src/Cursors.js'; import { nanoid } from 'nanoid'; import { LocksEventMap } from '../../src/Locks.js'; +import Spaces from '../../src/Spaces.js'; /* * These tests have one `describe` for each area of functionality, each of these `describe`s then containing multiple `it`s. @@ -146,11 +147,11 @@ describe( }); describe('cursors', () => { - let performerSpaces; - let observerSpaces; - let performerClientId; - let performerSpace; - let observerSpace; + let performerSpaces: Spaces; + let observerSpaces: Spaces; + let performerClientId: string; + let performerSpace: Space; + let observerSpace: Space; beforeAll(async () => { [{ spaces: performerSpaces, clientId: performerClientId }, { spaces: observerSpaces }] = await createClients({ From 8a70ad8a660421df3fb4bbeb11883d3ed5466b75 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 12:52:47 +0100 Subject: [PATCH 185/191] Fix integration.test.ts used deprecated callback for RealtimeChannel.whenState() Callback API for RealtimeChannel.whenState() was removed in ably-js v2 [1] [1] https://github.com/ably/ably-js/commit/2a2ed49e40e6ce9a8b1a119b66b2f3eeaf235054 --- test/integration/integration.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 5fa0f12e..d0f1f388 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -187,7 +187,7 @@ describe( it('scenario 2.2: set cursor position', async () => { // Before calling `performerSpace.cursors.set()` below, we want to be sure that `performerSpace.cursors` has found out about the presence enter operations triggered by calling `performerSpace.cursors.set()` in scenario 2.1, and by calling `observerSpace.cursors.subscribe()` below, so that it doesn’t drop the cursor updates that we pass to `set()`. So here we set up a promise which will resolve once `performerSpace.cursors.channel` sees two clients (i.e. `performerSpaces` and `observerSpaces`) as present. const performerCursorsChannelObserverPresentPromise = new Promise((resolve) => { - const presence = performerSpace.cursors.channel.presence; + const presence = performerSpace.cursors.channel!.presence; const listener = async () => { const members = await presence.get(); if (members.length === 2) { @@ -215,9 +215,7 @@ describe( // To be sure that the `observerSpace.cursors.subscribe()` listener will receive the cursor positions sent by the calls to `performerSpace.cursors.set()` below, we need to know that the cursors channel attach operation triggered by calling `observerSpace.cursors.subscribe()` has completed. The `cursors.subscribe()` API does not currently provide any direct way for the user to know that this attach operation has completed, so here we do so by directly observing the channel. // // We should consider exposing the completion of the attach operation via the `cursors.subscribe()` API, the same way as ably-js exposes it through `presence.subscribe()`. I’m not convinced of the necessity though — not sure how useful it’d be for an average user, and we can work around it in tests (as I have here). - const observerCursorsChannelAttachedPromise = new Promise((resolve) => { - observerSpace.cursors.channel.whenState('attached', resolve); - }); + const observerCursorsChannelAttachedPromise = observerSpace.cursors.channel!.whenState('attached'); await Promise.all([performerCursorsChannelObserverPresentPromise, observerCursorsChannelAttachedPromise]); From b39b1eb9ff63b2a4eadc96e6f755d95e09c966ad Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 16:29:01 +0100 Subject: [PATCH 186/191] chore: bump version for 0.4.0 release --- demo/package-lock.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index e126d4cd..0064c36d 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -41,7 +41,7 @@ }, "..": { "name": "@ably/spaces", - "version": "0.3.1", + "version": "0.4.0", "license": "ISC", "dependencies": { "nanoid": "^3.3.7" diff --git a/package-lock.json b/package-lock.json index 918d8e4a..974abc5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ably/spaces", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ably/spaces", - "version": "0.3.1", + "version": "0.4.0", "license": "ISC", "dependencies": { "nanoid": "^3.3.7" diff --git a/package.json b/package.json index 18c90331..597d9c29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ably/spaces", - "version": "0.3.1", + "version": "0.4.0", "description": "", "main": "dist/cjs/index.js", "module": "dist/mjs/index.js", diff --git a/src/version.ts b/src/version.ts index 02e02a02..95bd727f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ // Manually update when bumping version -const VERSION = '0.3.1'; +const VERSION = '0.4.0'; export { VERSION }; From a1e1bb21102f0234e9edba475c99b64ffe71d91c Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 17 Jul 2024 16:41:27 +0100 Subject: [PATCH 187/191] chore: update changelog for 0.4.0 release --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9e5cee..cc82495a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## v0.4.0 + +Breaking changes in this release: + +* Upgrade to using Ably JavaScript SDK v2 [\#325](https://github.com/ably/spaces/pull/325) + +With this release the Spaces SDK now requires Ably JavaScript SDK v2 to be installed and used with the Spaces client. Please refer to [Ably JavaScript SDK v2](https://github.com/ably/ably-js/releases/tag/2.0.0) GitHub release notes for the list of breaking changes and the corresponding migration guide. + +**Full Changelog**: https://github.com/ably/spaces/compare/0.3.1...0.4.0 + ## v0.3.1 No breaking changes were introduced in this release. From 50d88999ecee80dcdbac51b1da2fde1ca6609208 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:11:02 +0200 Subject: [PATCH 188/191] Update CDN versions to 2+ and 0.4 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bc7553fb..52b804f9 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ To use Spaces you must also set a [`clientId`](https://ably.com/docs/auth/identi You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): ```html - - + + ``` After this, instantiate the SDK in the same way as in the NPM option above: From 12467ddc62b982acd4c1a065e0d51e74c11a3b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dias?= <112943738+JoaoDiasAbly@users.noreply.github.com> Date: Wed, 18 Sep 2024 08:29:57 +0100 Subject: [PATCH 189/191] Change cursors batch time formula on presence update (#333) --- src/Cursors.test.ts | 4 ++-- src/Cursors.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Cursors.test.ts b/src/Cursors.test.ts index 383f4ae8..1f0c8802 100644 --- a/src/Cursors.test.ts +++ b/src/Cursors.test.ts @@ -116,7 +116,7 @@ describe('Cursors', () => { vi.spyOn(channel.presence, 'get').mockImplementation(createPresenceCount(2)); await cursors['onPresenceUpdate'](); expect(batching.shouldSend).toBeTruthy(); - expect(batching.batchTime).toEqual(50); + expect(batching.batchTime).toEqual(25); }); it('batchTime is updated when multiple people are present', async ({ @@ -126,7 +126,7 @@ describe('Cursors', () => { }) => { vi.spyOn(channel.presence, 'get').mockImplementation(createPresenceCount(2)); await cursors['onPresenceUpdate'](); - expect(batching.batchTime).toEqual(50); + expect(batching.batchTime).toEqual(25); }); describe('pushCursorPosition', () => { diff --git a/src/Cursors.ts b/src/Cursors.ts index 7548f463..8cf17a16 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -105,7 +105,11 @@ export default class Cursors extends EventEmitter { const channel = this.getChannel(); const cursorsMembers = await channel.presence.get(); this.cursorBatching.setShouldSend(cursorsMembers.length > 1); - this.cursorBatching.setBatchTime(cursorsMembers.length * this.options.outboundBatchInterval); + /** + * Since server-side batching is automically enabled for cursors channels, we can now adjust the client-side batching interval more granularly. + * E.g. multiply the configured outboundBatchInterval by groups of 100 members instead of the total number of members. + */ + this.cursorBatching.setBatchTime(Math.ceil(cursorsMembers.length / 100) * this.options.outboundBatchInterval); } private isUnsubscribed() { From 9b7420933f659c3dc4d3e5dd495aa3d8c4fe5725 Mon Sep 17 00:00:00 2001 From: Evgeny Khokhlov Date: Wed, 25 Sep 2024 14:28:06 +0100 Subject: [PATCH 190/191] docs: update CDN link (#334) Update broken CDN link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52b804f9..af620ec8 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ You can also use Spaces with a CDN, such as [unpkg](https://www.unpkg.com/): ```html - + ``` After this, instantiate the SDK in the same way as in the NPM option above: From 23f85ace7748f76c8e62abca7080a1917342d09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dias?= <112943738+JoaoDiasAbly@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:41:11 +0000 Subject: [PATCH 191/191] Adjust cursors default outboundBatchInterval (#337) --- src/Cursors.test.ts | 4 ++-- src/Space.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cursors.test.ts b/src/Cursors.test.ts index 1f0c8802..383f4ae8 100644 --- a/src/Cursors.test.ts +++ b/src/Cursors.test.ts @@ -116,7 +116,7 @@ describe('Cursors', () => { vi.spyOn(channel.presence, 'get').mockImplementation(createPresenceCount(2)); await cursors['onPresenceUpdate'](); expect(batching.shouldSend).toBeTruthy(); - expect(batching.batchTime).toEqual(25); + expect(batching.batchTime).toEqual(50); }); it('batchTime is updated when multiple people are present', async ({ @@ -126,7 +126,7 @@ describe('Cursors', () => { }) => { vi.spyOn(channel.presence, 'get').mockImplementation(createPresenceCount(2)); await cursors['onPresenceUpdate'](); - expect(batching.batchTime).toEqual(25); + expect(batching.batchTime).toEqual(50); }); describe('pushCursorPosition', () => { diff --git a/src/Space.ts b/src/Space.ts index e3a40fe0..d0d041fb 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -20,7 +20,7 @@ const SPACE_CHANNEL_TAG = '::$space'; const SPACE_OPTIONS_DEFAULTS = { offlineTimeout: 120_000, cursors: { - outboundBatchInterval: 25, + outboundBatchInterval: 50, paginationLimit: 5, }, };