wirejs-resources 0.1.59 → 0.1.61-realtime
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.js +44 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/overrides.d.ts +2 -0
- package/dist/resources/distributed-table.js +0 -45
- package/dist/services/realtime.d.ts +11 -5
- package/dist/services/realtime.js +5 -40
- package/package.json +4 -2
package/dist/client/index.js
CHANGED
|
@@ -52,6 +52,50 @@ async function callApi(INTERNAL_API_URL, method, ...args) {
|
|
|
52
52
|
throw new Error(error);
|
|
53
53
|
}
|
|
54
54
|
const value = body[0].data;
|
|
55
|
+
if (typeof value === 'object' && value.__wjstype === 'realtime') {
|
|
56
|
+
const subscribers = [];
|
|
57
|
+
const ws = new WebSocket(value.url, value.protocol);
|
|
58
|
+
ws.onmessage = (event) => {
|
|
59
|
+
const data = JSON.parse(event.data);
|
|
60
|
+
if (data.data) {
|
|
61
|
+
for (const subscriber of subscribers) {
|
|
62
|
+
try {
|
|
63
|
+
subscriber.onmessage(data.data);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error('Error in subscriber onmessage:', error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
ws.onclose = event => {
|
|
72
|
+
for (const subscriber of subscribers) {
|
|
73
|
+
if (subscriber.onclose) {
|
|
74
|
+
try {
|
|
75
|
+
subscriber.onclose();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('Error in subscriber onclose:', error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
console.log('closed', event);
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
subscribe(subscriber) {
|
|
86
|
+
subscribers.push(subscriber);
|
|
87
|
+
return () => {
|
|
88
|
+
const index = subscribers.indexOf(subscriber);
|
|
89
|
+
if (index !== -1) {
|
|
90
|
+
subscribers.splice(index, 1);
|
|
91
|
+
}
|
|
92
|
+
if (subscribers.length === 0) {
|
|
93
|
+
ws.close();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
55
99
|
return value;
|
|
56
100
|
}
|
|
57
101
|
;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { Secret } from './resources/secret.js';
|
|
|
9
9
|
export { DistributedTable, PassThruParser, matchesFilter, indexName } from './resources/distributed-table.js';
|
|
10
10
|
export * from './types/index.js';
|
|
11
11
|
export * from './derived-types.js';
|
|
12
|
+
export * from './services/realtime.js';
|
package/dist/overrides.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FileService } from "./services/file";
|
|
2
2
|
import type { AuthenticationService } from "./services/authentication";
|
|
3
3
|
import type { Secret } from "./resources/secret";
|
|
4
|
+
import type { RealtimeService } from "./services/realtime";
|
|
4
5
|
/**
|
|
5
6
|
* Used by hosting providers to provide service overrides.
|
|
6
7
|
*/
|
|
@@ -9,4 +10,5 @@ export declare const overrides: {
|
|
|
9
10
|
DistributedTable?: typeof AuthenticationService;
|
|
10
11
|
FileService?: typeof FileService;
|
|
11
12
|
Secret?: typeof Secret;
|
|
13
|
+
RealtimeService?: typeof RealtimeService;
|
|
12
14
|
};
|
|
@@ -158,48 +158,3 @@ export class DistributedTable extends Resource {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
// export type Todo = {
|
|
162
|
-
// id: string;
|
|
163
|
-
// text: string;
|
|
164
|
-
// order: number;
|
|
165
|
-
// };
|
|
166
|
-
// const userTodos = new DistributedTable('app', 'userTodos', {
|
|
167
|
-
// parse: PassThruParser<Todo & { userId: string }>,
|
|
168
|
-
// key: {
|
|
169
|
-
// partition: { field: 'userId', type: 'string' },
|
|
170
|
-
// sort: { field: 'order', type: 'number' }
|
|
171
|
-
// // sort: { field: 'id', type: 'string' }
|
|
172
|
-
// // sort: { }
|
|
173
|
-
// },
|
|
174
|
-
// indexes: [
|
|
175
|
-
// {
|
|
176
|
-
// partition: { field: 'text', type: 'string' },
|
|
177
|
-
// sort: { field: 'userId', type: 'string' }
|
|
178
|
-
// // sort: { field: 'order', type: 'number' }
|
|
179
|
-
// }
|
|
180
|
-
// ]
|
|
181
|
-
// });
|
|
182
|
-
// type I = typeof userTodos['key'];
|
|
183
|
-
// type T1 = typeof userTodos['key']['partition']['field'];
|
|
184
|
-
// type T2 = typeof userTodos['key']['sort']['field'];
|
|
185
|
-
// type T3 = typeof userTodos['indexes'];
|
|
186
|
-
// type TTT1 = AllIndexes<typeof userTodos>;
|
|
187
|
-
// type TTT2 = AllIndexesByName<typeof userTodos>;
|
|
188
|
-
// type AnyIndex = Index<any>;
|
|
189
|
-
// type TEST1 = I extends Index<any> ? 'yes' : 'no';
|
|
190
|
-
// userTodos.query({
|
|
191
|
-
// by: 'userId-order',
|
|
192
|
-
// where: {
|
|
193
|
-
// // maybe the key here can be the actual name of the field.
|
|
194
|
-
// // maybe even follow the filter pattern, but only expose the `eq` operation:
|
|
195
|
-
// // { [field]: { eq: T } }
|
|
196
|
-
// userId: { eq: 'something' },
|
|
197
|
-
// // id: {
|
|
198
|
-
// // eq: 'something'
|
|
199
|
-
// // }
|
|
200
|
-
// // same here. name of the actual sort key field.
|
|
201
|
-
// // id: {
|
|
202
|
-
// // beginsWith: 'something'
|
|
203
|
-
// // },
|
|
204
|
-
// },
|
|
205
|
-
// })
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { Resource } from '../resource.js';
|
|
2
|
+
export type MessageStream<T = any> = {
|
|
3
|
+
/**
|
|
4
|
+
* Returns a function to close the subscription.
|
|
5
|
+
*/
|
|
6
|
+
subscribe(subscriber: {
|
|
7
|
+
onmessage: (data: T) => void;
|
|
8
|
+
onclose?: () => void;
|
|
9
|
+
}): () => void;
|
|
10
|
+
};
|
|
2
11
|
export declare class RealtimeService<T = any> extends Resource {
|
|
3
12
|
#private;
|
|
4
13
|
constructor(scope: Resource | string, id: string);
|
|
@@ -6,9 +15,6 @@ export declare class RealtimeService<T = any> extends Resource {
|
|
|
6
15
|
* The address the client will need to connect to.
|
|
7
16
|
*/
|
|
8
17
|
get address(): string;
|
|
9
|
-
publish(channel: string, data: T): void
|
|
10
|
-
|
|
11
|
-
url: string;
|
|
12
|
-
protocol: string;
|
|
13
|
-
}>;
|
|
18
|
+
publish(channel: string, data: T): Promise<void>;
|
|
19
|
+
getStream(channel: string): Promise<MessageStream<T>>;
|
|
14
20
|
}
|
|
@@ -3,44 +3,6 @@ import { WebSocketServer, WebSocket } from 'ws';
|
|
|
3
3
|
import { Resource } from '../resource.js';
|
|
4
4
|
import { Secret } from '../resources/secret.js';
|
|
5
5
|
import { overrides } from '../overrides.js';
|
|
6
|
-
// TODO: this interface is very much a work in progress.
|
|
7
|
-
// currently thinking that we will want to expose a couple different
|
|
8
|
-
// "realtime" services or resources that are purpose built for three
|
|
9
|
-
// distinct use-cases:
|
|
10
|
-
//
|
|
11
|
-
// 1. a "p2p" rt service as a fallback for WebRTC messaging.
|
|
12
|
-
// this would put the full burden of validating messages on the
|
|
13
|
-
// client. we could still gate onsubscribe, but the intent would
|
|
14
|
-
// be that clients can then send messages freely. goal is hyper-efficient
|
|
15
|
-
// messaging between clients with minimal server overhead.
|
|
16
|
-
//
|
|
17
|
-
// 2. sending messages from server to client.
|
|
18
|
-
// the implication is that the client subscribes to specific channels,
|
|
19
|
-
// such as updates to a specific table. the server, again, gates
|
|
20
|
-
// subscriptions to the channel. and messages *to* the channel can
|
|
21
|
-
// originate *only* from the server, ensuring they are delivered in the
|
|
22
|
-
// expected format and that they can be trusted.
|
|
23
|
-
//
|
|
24
|
-
// 3. low-trust p2p messaging.
|
|
25
|
-
// this would be a hybrid of the first two. clients can send messages
|
|
26
|
-
// to each other, but the server is responsible for validating the
|
|
27
|
-
// messages. unless option 1, which is more of a fallback when WebRTC
|
|
28
|
-
// cannot be established, this option accepts much higher overhead,
|
|
29
|
-
// latency, and cloud costs at the benefit of server-side validation.
|
|
30
|
-
// this allows the client to trust messages, and allows p2p messages
|
|
31
|
-
// to have side effects, such as leaving behind a durable record.
|
|
32
|
-
//
|
|
33
|
-
// notably, option 3 can be mimicked with option 2 + the regular API's we
|
|
34
|
-
// already support. so, it should be the last option we put serious time
|
|
35
|
-
// and consideration to. but, we should at least know how we might achieve it.
|
|
36
|
-
//
|
|
37
|
-
// debatable whether each of these use-cases should be fulfilled with a
|
|
38
|
-
// single constructor or triggered by purpose-built constructors.
|
|
39
|
-
//
|
|
40
|
-
// using purpose-built constructors might help clarify the optimizations
|
|
41
|
-
// each use-case asks for from hosting providers. might also help streamline
|
|
42
|
-
// development of the service, as we can focus on a single use-case at a time.
|
|
43
|
-
//
|
|
44
6
|
const servers = new Map();
|
|
45
7
|
const channels = new Map();
|
|
46
8
|
export class RealtimeService extends Resource {
|
|
@@ -149,7 +111,7 @@ export class RealtimeService extends Resource {
|
|
|
149
111
|
throw new Error('Server address is not available');
|
|
150
112
|
}
|
|
151
113
|
}
|
|
152
|
-
publish(channel, data) {
|
|
114
|
+
async publish(channel, data) {
|
|
153
115
|
this.#requireServer();
|
|
154
116
|
this.#validateChannelName(channel);
|
|
155
117
|
console.log('Publishing to channel:', channel, data);
|
|
@@ -159,7 +121,7 @@ export class RealtimeService extends Resource {
|
|
|
159
121
|
}
|
|
160
122
|
});
|
|
161
123
|
}
|
|
162
|
-
async
|
|
124
|
+
async getStream(channel) {
|
|
163
125
|
this.#requireServer();
|
|
164
126
|
this.#validateChannelName(channel);
|
|
165
127
|
const channelString = Buffer.from(channel).toString('base64');
|
|
@@ -169,7 +131,10 @@ export class RealtimeService extends Resource {
|
|
|
169
131
|
.setIssuedAt()
|
|
170
132
|
.setExpirationTime(`10s`)
|
|
171
133
|
.sign(new TextEncoder().encode(await this.#secret.read()));
|
|
134
|
+
// The type we return is not at all what we actually return.
|
|
135
|
+
// It's the metadata needed to satisfy the contract client-side.
|
|
172
136
|
return {
|
|
137
|
+
__wjstype: 'realtime',
|
|
173
138
|
url: `${this.address}/${channelString}`,
|
|
174
139
|
protocol: jwt
|
|
175
140
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wirejs-resources",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.61-realtime",
|
|
4
4
|
"description": "Basic services and server-side resources for wirejs apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
},
|
|
35
35
|
"homepage": "https://github.com/svidgen/create-wirejs-app#readme",
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"jose": "^5.9.6"
|
|
37
|
+
"jose": "^5.9.6",
|
|
38
|
+
"ws": "^8.18.2"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
41
|
+
"@types/ws": "^8.18.1",
|
|
40
42
|
"typescript": "^5.7.3"
|
|
41
43
|
},
|
|
42
44
|
"files": [
|