Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 106 additions & 1 deletion endpoints/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,111 @@ import { localStorageIO } from 'type-r/endpoints/localStorage'
}
```

### websocketIO( url, options? )

Basic WebSocket endpoint for collection live updates. It implements `subscribe()` and `unsubscribe()` and does not implement REST-like CRUD/list methods. Use it when a collection is loaded by another endpoint or when you only need live updates.

Several collections using the same `url`, `WebSocket` constructor, and `protocols` share one WebSocket connection. Each collection still sends its own subscribe/unsubscribe messages.

```javascript
import { websocketIO } from '@type-r/endpoints'

@define class User extends Model {
static endpoint = websocketIO( 'ws://localhost:3000/live', {
subscribeMessage : () => ({
type : 'subscribe',
channel : 'users'
}),

unsubscribeMessage : () => ({
type : 'unsubscribe',
channel : 'users'
}),

// Route only messages for this collection.
match : message => message.channel === 'users'
});

static attributes = {
name : ''
}
}

const users = new User.Collection();
users.liveUpdates( true );
```

Supported update messages:

```javascript
{
channel : 'users',
type : 'updated',
payload : {
id : '1',
name : 'Ann'
}
}
```

Supported remove messages:

```javascript
{
channel : 'users',
type : 'removed',
payload : '1'
}
```

Compact envelopes are also supported:

```javascript
{ updated : { id : '1', name : 'Ann' } }
{ removed : '1' }
```

`websocketIO()` is intentionally small. It does not provide reconnect, heartbeat, subscribe acknowledgements, cursors, event replay, or resync.

### restfulWebsocketIO( url, websocketUrl, options? )

Combined endpoint using `restfulIO` semantics for CRUD/list operations and basic WebSocket subscription for collection live updates.

```javascript
import { restfulWebsocketIO } from '@type-r/endpoints'

@define class User extends Model {
static endpoint = restfulWebsocketIO(
'http://localhost:3000/api/users',
'ws://localhost:3000/live',
{
subscribeMessage : () => ({
type : 'subscribe',
channel : 'users'
}),

unsubscribeMessage : () => ({
type : 'unsubscribe',
channel : 'users'
}),

match : message => message.channel === 'users'
}
);

static attributes = {
name : ''
}
}

const users = new User.Collection();

// Fetches the REST snapshot and enables WebSocket live updates.
users.fetch({ liveUpdates : true });
```

When `fetch({ liveUpdates : true })` is used, Type-R subscribes to live updates first and then performs the REST `list()` request. The basic WebSocket endpoint considers subscription ready when the socket is open and the subscribe message has been sent.

### attributesIO()

Endpoint for I/O composition. Redirects model's `fetch()` request to its attributes and returns the combined abortable promise. Does not enable any other I/O methods and can be used with `model.fetch()` only.
Expand Down Expand Up @@ -186,4 +291,4 @@ const abortablePromise = createIOPromise( ( resolve, reject, onAbort ) =>{
reject( 'I/O Aborted' );
});
});
```
```
134 changes: 134 additions & 0 deletions endpoints/WEBSOCKET.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Basic WebSocket endpoints

`@type-r/endpoints` includes a small WebSocket endpoint intended for simple live collection updates.

Use it when you need:

- one WebSocket connection shared by several collections;
- simple `subscribe` / `unsubscribe` messages;
- `updated` and `removed` events applied to a collection;
- no reconnect, replay, heartbeat, cursor, or protocol versioning.

Production realtime protocols can add reconnect, `since`, event replay, heartbeat, structured errors, and resync on top of this basic endpoint.

## Basic collection endpoint

```ts
import { auto, CollectionConstructor, define, Record } from '@type-r/models'
import { websocketIO } from '@type-r/endpoints'

@define
class User extends Record {
static Collection: CollectionConstructor<User>

static endpoint = websocketIO('ws://localhost:3000/live', {
subscribeMessage: () => ({
type: 'subscribe',
channel: 'users'
}),

unsubscribeMessage: () => ({
type: 'unsubscribe',
channel: 'users'
}),

match: message => message.channel === 'users'
})

@auto name: string
}

const users = new User.Collection()

await users.liveUpdates(true)
```

Supported update envelope:

```json
{
"channel": "users",
"type": "updated",
"payload": {
"id": "1",
"name": "Ann"
}
}
```

Supported remove envelope:

```json
{
"channel": "users",
"type": "removed",
"payload": "1"
}
```

Compact envelopes are supported too:

```json
{ "updated": { "id": "1", "name": "Ann" } }
```

```json
{ "removed": "1" }
```

## REST + basic WebSocket

`restfulWebsocketIO` combines REST CRUD with basic WebSocket live updates.

```ts
import { restfulWebsocketIO } from '@type-r/endpoints'

static endpoint = restfulWebsocketIO(
'http://localhost:3000/api/users',
'ws://localhost:3000/live',
{
subscribeMessage: () => ({ type: 'subscribe', channel: 'users' }),
unsubscribeMessage: () => ({ type: 'unsubscribe', channel: 'users' }),
match: message => message.channel === 'users'
}
)
```

`fetch({ liveUpdates: true })` subscribes first, then performs the REST list request. The basic endpoint resolves subscription as soon as the WebSocket is open and the subscribe message has been sent.

```ts
const users = new User.Collection()

await users.fetch({ liveUpdates: true })
```

## Options

```ts
interface WebSocketEndpointOptions {
WebSocket?: WebSocketConstructor
protocols?: string | string[]
match?: (message: any, collection?: any) => boolean
subscribeMessage?: object | ((collection?: any) => any)
unsubscribeMessage?: object | ((collection?: any) => any)
}
```

`match` is important when several collections share the same socket. Without it, every message is delivered to every subscription on that socket.

The basic endpoint uses JSON messages only: outgoing subscribe/unsubscribe messages are encoded with `JSON.stringify()`, and incoming string messages are decoded with `JSON.parse()`.

## Limitations

The basic endpoint intentionally does not implement:

- subscribe acknowledgements;
- reconnect and resubscribe;
- heartbeat;
- `since` cursors;
- event log replay;
- out-of-order detection;
- structured protocol errors;
- server shutdown handling.

Use a custom endpoint when you need these features.
2 changes: 1 addition & 1 deletion endpoints/dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion endpoints/dist/index.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions endpoints/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './memory';
export * from './proxy';
export * from './localStorage';
export * from './attributes';
export * from './websocket';
1 change: 1 addition & 0 deletions endpoints/lib/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion endpoints/lib/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 22 additions & 4 deletions endpoints/lib/restful.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { IOEndpoint, IOOptions, Transactional } from '@type-r/models';
import { MemoryEndpoint } from './memory';
export declare type UrlTemplate = (options: any, model?: any) => string;
import { WebSocketEndpoint, WebSocketEndpointOptions } from './websocket';
export type UrlTemplate = (options: any, model?: any) => string;
export declare function create(url: string | UrlTemplate, fetchOptions?: Partial<RestfulFetchOptions>): RestfulEndpoint;
export { create as restfulIO };
export declare type HttpMethod = 'GET' | 'POST' | 'UPDATE' | 'DELETE' | 'PUT';
export declare function restfulWebsocketIO(url: string | UrlTemplate, websocketUrl: string, options?: RestfulWebSocketEndpointOptions): RestfulWebSocketEndpoint;
export type HttpMethod = 'GET' | 'POST' | 'UPDATE' | 'DELETE' | 'PUT';
export interface RestfulIOOptions extends IOOptions {
params?: object;
options?: RequestInit;
idempotencyKey?: string;
expectedVersion?: string | number;
}
export declare type RestfulFetchOptions = {
export type RestfulFetchOptions = {
cache?: RequestCache;
credentials?: RequestCredentials;
mode?: RequestMode;
Expand All @@ -17,6 +21,7 @@ export declare type RestfulFetchOptions = {
mockData?: any;
simulateDelay?: number;
};
export type RestfulWebSocketEndpointOptions = Partial<RestfulFetchOptions> & WebSocketEndpointOptions;
export declare class RestfulEndpoint implements IOEndpoint {
url: string | UrlTemplate;
constructor(url: string | UrlTemplate, { mockData, simulateDelay, ...fetchOptions }?: RestfulFetchOptions);
Expand All @@ -34,7 +39,20 @@ export declare class RestfulEndpoint implements IOEndpoint {
protected objectUrl(model: Transactional, id: string, options?: RestfulIOOptions): string;
protected collectionUrl(collection: Transactional, options?: RestfulIOOptions): string;
protected buildRequestOptions(method: string, options?: RequestInit, body?: any): RequestInit;
protected request(method: HttpMethod, url: string, { options }: RestfulIOOptions, body?: any): Promise<any>;
protected request(method: HttpMethod, url: string, ioOptions: RestfulIOOptions, body?: any): Promise<any>;
}
export declare class RestfulWebSocketEndpoint implements IOEndpoint {
options: RestfulWebSocketEndpointOptions;
restful: RestfulEndpoint;
websocket: WebSocketEndpoint;
constructor(url: string | UrlTemplate, websocketUrl: string, options?: RestfulWebSocketEndpointOptions);
create(json: any, options: RestfulIOOptions, model: any): Promise<any>;
update(id: any, json: any, options: RestfulIOOptions, model: any): Promise<any>;
read(id: any, options: IOOptions, model: any): Promise<any>;
destroy(id: any, options: RestfulIOOptions, model: any): Promise<any>;
list(options: RestfulIOOptions, collection: any): Promise<any>;
subscribe(events: any, collection?: any): import("@type-r/models").IOPromise<any>;
unsubscribe(events: any, collection?: any): void;
}
export declare class UrlBuilder {
private url;
Expand Down
Loading