Skip to content

Commit 2f528dd

Browse files
committed
add subscription feature to Guardian and DataServer
1 parent 7c27896 commit 2f528dd

File tree

8 files changed

+235
-182
lines changed

8 files changed

+235
-182
lines changed

packages/server/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ The Data Server is responsible for serving the bubble content and processing val
2222

2323
A Data Server is an implementation of the [`DataServer`](src/DataServer.js) interface. Requirements for the interface can be found in that file. A Data Server must pass the acceptance tests found in the [Data Server Test Suite](./test/DataServerTestSuite/). See [Testing Your Server](#testing-your-server) for more details.
2424

25+
#### Optional Features
26+
27+
Not all features of a Data Server are mandated. Implementation of the following features is optional:
28+
29+
- *Subscriptions* - the `subscribe` and `unsubscribe` methods.
2530

2631
## Example Server
2732

packages/server/src/DataServer.js

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Distributed under the MIT software license, see the accompanying
33
// file LICENSE or http://www.opensource.org/licenses/mit-license.php.
44

5-
// TODO Make sure all requirements on the implementator are given for each method.
6-
75
/**
86
* Interface for a Bubble server.
97
*
@@ -390,90 +388,94 @@ export class DataServer {
390388
// root directory).
391389
//
392390
// [req-ds-sub-2] If the subscription was successful, the data server shall resolve with a
393-
// plain object containing a unique subscriptionId field (any type) and the
394-
// long form listing of the file/directory (not directory contents).
395-
//
396-
// [req-ds-sub-3] The data server shall reject with a FILE_DOES_NOT_EXIST error if the
397-
// file does not exist.
391+
// plain object containing a unique subscriptionId field (any type) and, if
392+
// the file/directory exists, the long form listing of the file/directory
393+
// (not directory contents).
398394
//
399-
// [req-ds-sub-4] The data server shall reject with a BUBBLE_DOES_NOT_EXIST error if the
395+
// [req-ds-sub-3] The data server shall reject with a BUBBLE_DOES_NOT_EXIST error if the
400396
// bubble does not exist on the server.
401397
//
402398
// Options:
403399
//
404-
// [req-ds-sub-5] If the 'list' option is given and the subscription is for a directory, the
405-
// data server shall include, in the resolved object, a `list` field containing
400+
// [req-ds-sub-4] If the 'list' option is given and the subscription is for a directory, the
401+
// data server shall include, in the resolved object, a `data` field containing
406402
// the long form listing of the directory contents (see requirements req-ds-ls-5..10).
407403
//
408-
// [req-ds-sub-6] If the 'list' option is given and the subscription is for a file, the
404+
// [req-ds-sub-5] If the 'list' option is given and the subscription is for a file, the
409405
// data server shall omit the `data` field in all notifications.
410406
//
411-
// [req-ds-sub-7] If the 'since' option is given and the subscription is for a directory, the
412-
// data server shall include, in the resolved object, a `list` field containing
407+
// [req-ds-sub-6] If the 'since' option is given and the subscription is for a directory, the
408+
// data server shall include, in the resolved object, a `data` field containing
413409
// the long form listing of the directory contents created or updated since (but not
414410
// on) the option's timestamp (integer UNIX timestamp in ms). (See requirements
415411
// req-ds-ls-5..10 for the long form listing format).
416412
//
417-
// [req-ds-sub-8] If the 'read' option is given and the subscription is for a file, the
413+
// [req-ds-sub-7] If the 'read' option is given and the subscription is for a file, the
418414
// data server shall include, in the resolved object, a `data` field containing
419415
// the file contents.
420416
//
421417
// Notifications:
422418
//
423-
// [req-ds-sub-9] The data server shall notify the client `listener` function whenever the file
419+
// [req-ds-sub-8] The data server shall notify the client `listener` function whenever the file
424420
// or directory changes, subject to the subscription options.
425421
//
426-
// [req-ds-sub-10] By default, when a subscribed file is written to using the `write` command,
422+
// [req-ds-sub-9] By default, when a subscribed file is written to using the `write` command,
427423
// the data server shall notify the client with a `write` event and, unless the
428424
// 'list' option was given (see req-ds-sub-6), the full contents of the file.
429425
//
430-
// [req-ds-sub-11] By default, when a subscribed file is appended to using the `append` command,
426+
// [req-ds-sub-10] By default, when a subscribed file is appended to using the `append` command,
431427
// the data server shall notify the client with a `append` event and, unless the
432428
// 'list' option was given (see req-ds-sub-6), the appended data.
433429
//
434-
// [req-ds-sub-12] By default, when a subscribed file is deleted using the `delete` command,
430+
// [req-ds-sub-11] By default, when a subscribed file is deleted using the `delete` command,
435431
// the data server shall notify the client with a `delete` event.
436432
//
437-
// [req-ds-sub-13] By default, when a new directory is added to a subscribed root (via mkdir)
433+
// [req-ds-sub-12] By default, when a new directory is added to a subscribed root (via mkdir)
438434
// the data server shall notify the client of an `update` event and include a list
439435
// (array) of files that have changed.
440436
//
441-
// [req-ds-sub-14] By default, when a file is written to a subscribed directory (including the
437+
// [req-ds-sub-13] By default, when a file is written to a subscribed directory (including the
442438
// ROOT_PATH), the data server shall notify the client of an `update` event and
443439
// include the updated file in its data array in the following format:
444440
// {event: 'write', <...long format listing of the file>}
445441
//
446-
// [req-ds-sub-15] By default, when a file is appended to in a subscribed directory (including the
442+
// [req-ds-sub-14] By default, when a file is appended to in a subscribed directory (including the
447443
// ROOT_PATH), the data server shall notify the client of an `update` event and
448444
// include the updated file in its data array in the following format:
449445
// {event: 'append', <...long format listing of the file>}
450446
//
451-
// [req-ds-sub-16] By default, when a file is deleted from a subscribed directory (including the
447+
// [req-ds-sub-15] By default, when a file is deleted from a subscribed directory (including the
452448
// ROOT_PATH), the data server shall notify the client of an `update` event and
453449
// include the updated file in its data array in the following format:
454450
// {event: 'delete', file: <fileId>, type: 'file'}
455451
//
452+
// [req-ds-sub-16] By default, when a subscribed directory is created using the `mkdir` command,
453+
// the data server shall notify the client with an `mkdir` event.
454+
//
455+
// [req-ds-sub-17] By default, when a subscribed directory is deleted using the `delete` command,
456+
// the data server shall notify the client with a `delete` event.
457+
//
456458
// Notification Format:
457459
//
458-
// [req-ds-sub-17] The type of each notification shall be a plain object.
460+
// [req-ds-sub-18] The type of each notification shall be a plain object.
459461
//
460-
// [req-ds-sub-18] Each notification shall contain a `subscriptionId` field containing the id
461-
// of the subscription.
462+
// [req-ds-sub-19] Each notification shall contain an `subscriptionId` field containing the id of
463+
// the subscription.
462464
//
463-
// [req-ds-sub-19] Each notification shall contain an `event` field indicating the type of
465+
// [req-ds-sub-20] Each notification shall contain an `event` field indicating the type of
464466
// event that caused the notification: 'write', 'append', 'delete', or 'update'.
465467
//
466-
// [req-ds-sub-20] Each update, write or append notification shall contain a `file` field
468+
// [req-ds-sub-21] Each update, write or append notification shall contain a `file` field
467469
// containing the long form listing of the updated file or directory
468470
// (see requirements req-ds-ls-5..10).
469471
//
470-
// [req-ds-sub-21] Each file delete notification shall contain a `file` field containing the short
472+
// [req-ds-sub-22] Each file delete notification shall contain a `file` field containing the short
471473
// listing of the updated file, i.e. {file: <fileId>, type: 'file}.
472474
//
473-
// [req-ds-sub-22] A file notification containing file contents shall include the contents as a
475+
// [req-ds-sub-23] A file notification containing file contents shall include the contents as a
474476
// `data` field.
475477
//
476-
// [req-ds-sub-23] A directory notification shall contain a `list` field containing an array
478+
// [req-ds-sub-24] A directory notification shall contain a `list` field containing an array
477479
// of changed files in the long form listing format (see requirements req-ds-ls-5..10).
478480
//
479481
subscribe(contract, file, listener, options) {

packages/server/src/Guardian.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ export class Guardian extends BubbleProvider {
5858
* @param params the RPC params
5959
* @returns Promise to service the call resolving with data if appropriate
6060
*/
61-
async post(method, params) {
61+
async post(method, params, subscriptionListener) {
62+
63+
if (method === 'subscribe') assert.isFunction(subscriptionListener, 'subscriptionListener');
6264

6365
/**
6466
* Basic RPC field validation
@@ -97,12 +99,6 @@ export class Guardian extends BubbleProvider {
9799
if (params.data !== undefined && !assert.isString(params.data))
98100
throw new BubbleError(JSON_RPC_ERROR_INVALID_METHOD_PARAMS, 'malformed data');
99101

100-
if (params.listener !== undefined && !assert.isFunction(params.listener))
101-
throw new BubbleError(JSON_RPC_ERROR_INVALID_METHOD_PARAMS, 'malformed listener');
102-
103-
if (params.listener === undefined && method === 'subscribe')
104-
throw new BubbleError(JSON_RPC_ERROR_INVALID_METHOD_PARAMS, 'missing listener param');
105-
106102
if (params.subscriptionId === undefined && method === 'unsubscribe')
107103
throw new BubbleError(JSON_RPC_ERROR_INVALID_METHOD_PARAMS, 'missing subscriptionId param');
108104

@@ -247,7 +243,7 @@ export class Guardian extends BubbleProvider {
247243

248244
case "subscribe":
249245
if (file.permissions.canRead()) {
250-
const subscription = new ProtectedSubscription(this.blockchainProvider, params.contract, file, signatory, params.listener);
246+
const subscription = new ProtectedSubscription(this.blockchainProvider, params.contract, file, signatory, subscriptionListener);
251247
return this.dataServer.subscribe(params.contract, file.fullFilename, subscription.listener, params.options)
252248
.catch(_validateDataServerError);
253249
}
@@ -412,7 +408,7 @@ class ProtectedSubscription {
412408

413409
async listener(subscriptionId, result, error) {
414410
assert.isNotNull(subscriptionId, 'subscriptionId');
415-
const permissionBits = await getPermissions(this.blockchainProvider, this.contract, this.file, this.signatory);
411+
const permissionBits = await getPermissions(this.blockchainProvider, this.contract, this.file.getPermissionedPart(), this.signatory);
416412
const permissions = new BubblePermissions(permissionBits);
417413
if (!permissions.canRead()) {
418414
const terminatedError = new BubbleError(ErrorCodes.BUBBLE_ERROR_SUBSCRIPTION_TERMINATED, 'permission denied - subscription terminated');

packages/server/test/BubbleServerTestSuite/requirementsTests.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import '@bubble-protocol/core/test/BubbleErrorMatcher.js';
2424
* the BubbleTestPoint class will be used, which requires minimal read, write, list and
2525
* delete functions to be working on the server.
2626
*/
27-
export function testBubbleServerRequirements(web3, chainId, bubbleServerURL, bubbleProvider, testPoint) {
27+
export function testBubbleServerRequirements(web3, chainId, bubbleServerURL, bubbleProvider, testPoint, options={}) {
2828

2929
/**
3030
* Bubble Server Requirements Tests
@@ -33,7 +33,7 @@ export function testBubbleServerRequirements(web3, chainId, bubbleServerURL, bub
3333

3434
testPoint = testPoint || new BubbleTestPoint(web3, chainId, bubbleServerURL, bubbleProvider);
3535
const serverApi = new BubbleServerApi();
36-
const serverOptions = {}
36+
const serverOptions = {...options};
3737

3838

3939
beforeAll( async () => {

packages/server/test/DataServerTestSuite/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,19 @@ Add Jest as the test method in your `package.json`.
2424

2525
### Example Test Script
2626

27-
The test entry point takes two parameters: the [DataServer.js](../../src/DataServer.js) under test and your test point implementation.
27+
The test entry point takes three parameters: the [DataServer.js](../../src/DataServer.js) under test, your test point implementation and any test options.
2828

2929

3030
```javascript
3131
import { testDataServerRequirements } from '@bubble-protocol/server/test/DataServerTestSuite/requirementsTests.js';
3232

33-
testDataServerRequirements(myDataServer, myTestPoint);
33+
testDataServerRequirements(myDataServer, myTestPoint, {noSubscriptions: true});
3434
```
3535

36+
Options:
37+
38+
- `noSubscriptions: <boolean>` set to true if your data server does not support the subscriptions feature.
39+
3640
### Test Point
3741

3842
Your test point must implement the [TestPoint](TestPoint.js) interface to provide the features described in that file. The test suite uses your test point to setup and tear down each test case. Before running the requirements tests, the test suite will execute the `runTest()` method in your test point, if it exists, to confirm your test point has the necessary features.

0 commit comments

Comments
 (0)