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.
@@ -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
@@ -10,3 +10,4 @@ export { DistributedTable, PassThruParser, matchesFilter, indexName } from './re
10
10
  export type * from './resources/distributed-table.js';
11
11
  export * from './types/index.js';
12
12
  export * from './derived-types.js';
13
+ export * from './services/realtime.js';
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';
@@ -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
- buildSubscriptionAuthorization(channel: string): Promise<{
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 buildSubscriptionAuthorization(channel) {
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.59",
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": [