@cero-base/rpc 0.0.1

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/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @cero-base/rpc
2
+
3
+ RPC client/server for [@cero-base/core](../core).
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm install @cero-base/rpc
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Client (renderer / UI process)
14
+
15
+ ```js
16
+ import { Client, getRpc } from '@cero-base/rpc'
17
+
18
+ const rpc = getRpc(ipcStream, spec)
19
+ const client = new Client(rpc, schema, spec)
20
+ await client.ready()
21
+
22
+ // Profile
23
+ const profile = await client.profile.get()
24
+ client.profile.set({ name: 'Alice' })
25
+
26
+ // Rooms
27
+ const room = await client.rooms.open(null, { name: 'My Room' })
28
+ const messages = room.collection('messages')
29
+ await messages.put({ id: '1', text: 'Hello' })
30
+
31
+ // Devices
32
+ const devices = await client.devices.get()
33
+ const invite = await client.devices.invite()
34
+ ```
35
+
36
+ ### Server (worker / backend process)
37
+
38
+ ```js
39
+ import { Server, getRpc } from '@cero-base/rpc'
40
+
41
+ const rpc = getRpc(ipcStream, spec)
42
+ const server = new Server(db, rpc)
43
+ await server.ready()
44
+ ```
45
+
46
+ ### Build (generate RPC spec)
47
+
48
+ ```js
49
+ import { build } from '@cero-base/rpc'
50
+
51
+ await build('./spec', schema)
52
+ ```
53
+
54
+ ## Exports
55
+
56
+ | Export | Description |
57
+ | ------------ | ------------------------------------------------------- |
58
+ | `Client` | RPC client proxy (rooms, collections, profile, devices) |
59
+ | `Server` | RPC server that wires handlers to a CeroBase db |
60
+ | `Room` | Client-side room with collections, members, invites |
61
+ | `Collection` | Client-side collection with put/get/del/sub |
62
+ | `getRpc` | Create an RPC instance from an IPC stream + spec |
63
+ | `rpc` | Register RPC types into a hyperschema build |
64
+ | `build` | Build spec with RPC types included |
65
+
66
+ ## License
67
+
68
+ Apache-2.0
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@cero-base/rpc",
3
+ "version": "0.0.1",
4
+ "description": "RPC client/server for cero-base",
5
+ "files": [
6
+ "src",
7
+ "README.md"
8
+ ],
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "type": "module",
13
+ "main": "src/index.js",
14
+ "exports": {
15
+ ".": "./src/index.js",
16
+ "./client": "./src/client.js",
17
+ "./utils": "./src/utils.js",
18
+ "./server": "./src/server.js",
19
+ "./build": "./src/build.js"
20
+ },
21
+ "scripts": {
22
+ "build:test": "rm -rf test/fixture/spec && node test/fixture/build.js",
23
+ "pretest": "npm run build:test",
24
+ "test": "brittle test/*.js"
25
+ },
26
+ "dependencies": {
27
+ "@cero-base/core": "^0.0.1",
28
+ "compact-encoding": "^2.19.1",
29
+ "framed-stream": "^1.0.1",
30
+ "ms": "^2.1.3",
31
+ "ready-resource": "^1.0.2",
32
+ "safety-catch": "^1.0.2",
33
+ "streamx": "^2.22.0"
34
+ },
35
+ "devDependencies": {
36
+ "brittle": "^3.7.0",
37
+ "duplex-through": "^1.0.2"
38
+ },
39
+ "license": "Apache-2.0",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/lekinox/cero-base.git",
43
+ "directory": "packages/rpc"
44
+ }
45
+ }
package/src/build.js ADDED
@@ -0,0 +1,6 @@
1
+ import { build as coreBuild } from '@cero-base/core/builder'
2
+ import { rpc } from './schema.js'
3
+
4
+ export async function build(dir, schema) {
5
+ return coreBuild(dir, schema, { rpc })
6
+ }
package/src/client.js ADDED
@@ -0,0 +1,97 @@
1
+ import ReadyResource from 'ready-resource'
2
+ import safetyCatch from 'safety-catch'
3
+ import { useTimeout, pipeStream } from './utils.js'
4
+ import { Room } from './room.js'
5
+ import { Collection } from './collection.js'
6
+
7
+ const SEED_WORD_COUNT = 12
8
+
9
+ export class Client extends ReadyResource {
10
+ constructor(rpc, schema, spec) {
11
+ super()
12
+ this.rpc = rpc
13
+ this.schema = schema
14
+ this.spec = spec
15
+ this.id = null
16
+ this.deviceId = null
17
+
18
+ this.profile = {
19
+ get: async () => {
20
+ const { data } = await rpc.getProfile({})
21
+ return data || null
22
+ },
23
+ set: (data) => rpc.setProfile({ data }),
24
+ sub: () => pipeStream(rpc.watchProfile({}), ({ data }) => data || null)
25
+ }
26
+
27
+ this.devices = {
28
+ get: async () => {
29
+ const { data } = await rpc.getDevices({})
30
+ return data || []
31
+ },
32
+ sub: () => pipeStream(rpc.watchDevices({}), ({ data }) => data || []),
33
+ invite: async () => {
34
+ const { invite } = await rpc.createDeviceInvite({})
35
+ return invite
36
+ }
37
+ }
38
+
39
+ this.rooms = {
40
+ get: async () => {
41
+ const { data } = await rpc.getRooms({})
42
+ return data || []
43
+ },
44
+ open: (arg, opts = {}) =>
45
+ useTimeout(async () => {
46
+ let res
47
+ if (!arg) {
48
+ res = await rpc.createRoom({ name: opts?.name || '' })
49
+ } else if (arg.length > 64) {
50
+ res = await rpc.joinRoom({ invite: arg })
51
+ } else {
52
+ res = await rpc.openRoom({ roomId: arg })
53
+ }
54
+ return new Room(rpc, schema, spec, res.data.id)
55
+ }, opts),
56
+ sub: () => pipeStream(rpc.watchRooms({}), ({ data }) => data || [])
57
+ }
58
+
59
+ this.ready().catch(safetyCatch)
60
+ }
61
+
62
+ async _open() {
63
+ const res = await this.rpc.ready({})
64
+ this.id = res.id
65
+ this.deviceId = res.deviceId
66
+ const updates = this.rpc.updatesSubscribe({})
67
+ updates.on('data', () => this.emit('update'))
68
+ if (this.rpc.resume) this.rpc.resume()
69
+ }
70
+
71
+ async _close() {
72
+ this.rpc.destroy?.()
73
+ }
74
+
75
+ seed() {
76
+ return this.rpc.getSeed({}).then(({ seed }) => seed || null)
77
+ }
78
+
79
+ async join(input, opts = {}) {
80
+ return useTimeout(async () => {
81
+ const isSeed =
82
+ typeof input === 'string' && input.trim().split(/\s+/).length >= SEED_WORD_COUNT
83
+ if (isSeed) {
84
+ await this.rpc.recover({ seed: input })
85
+ } else {
86
+ await this.rpc.pairDevice({ invite: input })
87
+ }
88
+ const info = await this.rpc.getInfo({})
89
+ this.id = info.id
90
+ this.deviceId = info.deviceId
91
+ }, opts)
92
+ }
93
+
94
+ collection(name) {
95
+ return new Collection(this.rpc, this.schema, this.spec, name, null)
96
+ }
97
+ }
@@ -0,0 +1,69 @@
1
+ import c from 'compact-encoding'
2
+ import { pipeStream } from './utils.js'
3
+
4
+ export class Collection {
5
+ constructor(rpc, schema, spec, name, roomId) {
6
+ this.rpc = rpc
7
+ this.schema = schema
8
+ this.spec = spec
9
+ this.name = name
10
+ this.roomId = roomId || null
11
+
12
+ const { spec: key, path } = schema.resolve(name)
13
+ this._enc = spec[key]
14
+ this._path = path
15
+
16
+ const { spec: inputKey, path: inputPath } = schema.resolve(name, 'input')
17
+ this._inputEnc = spec[inputKey]
18
+ this._inputPath = inputPath
19
+ }
20
+
21
+ async put(doc) {
22
+ const { data } = await this.rpc.collectionInsert({
23
+ collection: this.name,
24
+ roomId: this.roomId,
25
+ doc: this._inputEnc.encode(this._inputPath, doc)
26
+ })
27
+ return this._enc.decode(this._path, data)
28
+ }
29
+
30
+ async get(query = {}, opts = {}) {
31
+ const encoding = this._enc.getEncoding(this._path)
32
+ const { data } = await this.rpc.collectionFind({
33
+ collection: this.name,
34
+ roomId: this.roomId,
35
+ query: Object.keys(query).length > 0 ? c.encode(c.json, query) : null,
36
+ limit: opts.limit || null,
37
+ reverse: opts.reverse || null
38
+ })
39
+ return c.decode(c.array(encoding), data)
40
+ }
41
+
42
+ async count() {
43
+ const results = await this.get()
44
+ return results.length
45
+ }
46
+
47
+ async del(query) {
48
+ const { ok } = await this.rpc.collectionDelete({
49
+ collection: this.name,
50
+ roomId: this.roomId,
51
+ query: c.encode(c.json, query)
52
+ })
53
+ return ok
54
+ }
55
+
56
+ sub(query = {}, opts = {}) {
57
+ const encoding = this._enc.getEncoding(this._path)
58
+ return pipeStream(
59
+ this.rpc.collectionSubscribe({
60
+ collection: this.name,
61
+ roomId: this.roomId,
62
+ query: Object.keys(query).length > 0 ? c.encode(c.json, query) : null,
63
+ limit: opts.limit || null,
64
+ reverse: opts.reverse || null
65
+ }),
66
+ ({ data }) => c.decode(c.array(encoding), data)
67
+ )
68
+ }
69
+ }
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './client.js'
2
+ export * from './room.js'
3
+ export * from './collection.js'
4
+ export * from './utils.js'
5
+ export * from './schema.js'
package/src/room.js ADDED
@@ -0,0 +1,50 @@
1
+ import { pipeStream } from './utils.js'
2
+ import { Collection } from './collection.js'
3
+
4
+ export class Room {
5
+ constructor(rpc, schema, spec, id) {
6
+ this.rpc = rpc
7
+ this.schema = schema
8
+ this.spec = spec
9
+ this.id = id
10
+
11
+ this.profile = null // TODO: add RPC methods for room profile
12
+
13
+ this.members = {
14
+ get: async () => {
15
+ const { data } = await rpc.getMembers({ roomId: id })
16
+ return data || []
17
+ },
18
+ sub: (query) =>
19
+ pipeStream(rpc.watchMembers({ roomId: id }), ({ data }) => {
20
+ let members = data || []
21
+ if (query) {
22
+ const entries = Object.entries(query)
23
+ members = members.filter((m) => entries.every(([k, v]) => m[k] === v))
24
+ }
25
+ return members
26
+ })
27
+ }
28
+
29
+ this.invites = {
30
+ create: async () => {
31
+ const { invite } = await rpc.createInvite({ roomId: id })
32
+ return { invite }
33
+ },
34
+ get: async () => []
35
+ }
36
+ }
37
+
38
+ collection(name) {
39
+ return new Collection(this.rpc, this.schema, this.spec, name, this.id)
40
+ }
41
+
42
+ async invite() {
43
+ const { invite } = await this.rpc.createInvite({ roomId: this.id })
44
+ return invite
45
+ }
46
+
47
+ async close() {
48
+ await this.rpc.closeRoom({ roomId: this.id })
49
+ }
50
+ }
package/src/schema.js ADDED
@@ -0,0 +1,125 @@
1
+ export function rpc({ register, ns = 'cero' }) {
2
+ const ref = (name) => `@${ns}/${name}`
3
+
4
+ // === Collection types ===
5
+
6
+ register.type({
7
+ name: 'ok-result',
8
+ compact: false,
9
+ fields: [{ name: 'ok', type: 'bool', required: true }]
10
+ })
11
+
12
+ register.type({
13
+ name: 'collection-insert',
14
+ compact: false,
15
+ fields: [
16
+ { name: 'collection', type: 'string', required: true },
17
+ { name: 'roomId', type: 'string', required: false },
18
+ { name: 'doc', type: 'buffer', required: true }
19
+ ]
20
+ })
21
+
22
+ register.type({
23
+ name: 'collection-query',
24
+ compact: false,
25
+ fields: [
26
+ { name: 'collection', type: 'string', required: true },
27
+ { name: 'roomId', type: 'string', required: false },
28
+ { name: 'query', type: 'buffer', required: false },
29
+ { name: 'limit', type: 'uint', required: false },
30
+ { name: 'reverse', type: 'bool', required: false }
31
+ ]
32
+ })
33
+
34
+ register.type({
35
+ name: 'collection-update',
36
+ compact: false,
37
+ fields: [
38
+ { name: 'collection', type: 'string', required: true },
39
+ { name: 'roomId', type: 'string', required: false },
40
+ { name: 'query', type: 'buffer', required: true },
41
+ { name: 'changes', type: 'buffer', required: true }
42
+ ]
43
+ })
44
+
45
+ register.type({
46
+ name: 'document-result',
47
+ compact: false,
48
+ fields: [{ name: 'data', type: 'buffer', required: false }]
49
+ })
50
+ register.type({
51
+ name: 'document-list',
52
+ compact: false,
53
+ fields: [{ name: 'data', type: 'buffer', required: true }]
54
+ })
55
+
56
+ // === Override cero's profile types ===
57
+
58
+ register.type({
59
+ name: 'profile-data',
60
+ compact: false,
61
+ fields: [
62
+ { name: 'name', type: 'string', required: false },
63
+ { name: 'theme', type: 'string', required: false }
64
+ ]
65
+ })
66
+ register.type({
67
+ name: 'profile-info',
68
+ compact: false,
69
+ fields: [{ name: 'data', type: ref('profile-data'), required: false }]
70
+ })
71
+ register.type({
72
+ name: 'profile-update',
73
+ compact: false,
74
+ fields: [{ name: 'data', type: ref('profile-data'), required: true }]
75
+ })
76
+
77
+ // === Update signal ===
78
+
79
+ register.type({
80
+ name: 'update-signal',
81
+ compact: false,
82
+ fields: [{ name: 'type', type: 'string', required: false }]
83
+ })
84
+
85
+ // === Collection RPCs ===
86
+
87
+ register.rpc({
88
+ name: 'collection-insert',
89
+ request: { name: ref('collection-insert') },
90
+ response: { name: ref('document-result') }
91
+ })
92
+ register.rpc({
93
+ name: 'collection-find',
94
+ request: { name: ref('collection-query') },
95
+ response: { name: ref('document-list') }
96
+ })
97
+ register.rpc({
98
+ name: 'collection-get',
99
+ request: { name: ref('collection-query') },
100
+ response: { name: ref('document-result') }
101
+ })
102
+ register.rpc({
103
+ name: 'collection-update',
104
+ request: { name: ref('collection-update') },
105
+ response: { name: ref('document-result') }
106
+ })
107
+ register.rpc({
108
+ name: 'collection-delete',
109
+ request: { name: ref('collection-query') },
110
+ response: { name: ref('ok-result') }
111
+ })
112
+ register.rpc({
113
+ name: 'collection-subscribe',
114
+ request: { name: ref('collection-query') },
115
+ response: { name: ref('document-list'), stream: true }
116
+ })
117
+
118
+ // === Updates ===
119
+
120
+ register.rpc({
121
+ name: 'updates-subscribe',
122
+ request: { name: ref('empty-data') },
123
+ response: { name: ref('update-signal'), stream: true }
124
+ })
125
+ }
package/src/server.js ADDED
@@ -0,0 +1,257 @@
1
+ import c from 'compact-encoding'
2
+ import { Room } from '@cero-base/core'
3
+
4
+ export class Server {
5
+ constructor(db, rpc) {
6
+ this.db = db
7
+ this.rpc = rpc
8
+ this.rooms = new Map()
9
+ this._registerHandlers()
10
+ }
11
+
12
+ async ready() {
13
+ await this.db.ready()
14
+ this.rpc.resume()
15
+ return this
16
+ }
17
+
18
+ async close() {
19
+ for (const room of this.rooms.values()) {
20
+ await room.close()
21
+ }
22
+ this.rooms.clear()
23
+ this.rpc.destroy?.()
24
+ await this.db.close()
25
+ }
26
+
27
+ _enc(collection, prefix) {
28
+ const { spec: key, path } = this.db.schema.resolve(collection, prefix)
29
+ return { spec: this.db.spec[key], path }
30
+ }
31
+
32
+ _registerHandlers() {
33
+ const { db, rpc, rooms } = this
34
+
35
+ // === System ===
36
+
37
+ rpc.onReady(async () => {
38
+ return { id: db.id, deviceId: db.deviceId }
39
+ })
40
+
41
+ // === Collection ===
42
+
43
+ rpc.onCollectionInsert(async ({ collection, roomId, doc }) => {
44
+ const { spec: iSpec, path: iPath } = this._enc(collection, 'input')
45
+ const { spec, path } = this._enc(collection)
46
+ const owner = roomId ? rooms.get(roomId) : db
47
+ const col = owner.collection(collection)
48
+ const input = iSpec.decode(iPath, doc)
49
+ for (const k in input) {
50
+ if (input[k] == null) delete input[k]
51
+ }
52
+ const result = await col.put(input)
53
+ return { data: spec.encode(path, result) }
54
+ })
55
+
56
+ rpc.onCollectionFind(async ({ collection, roomId, query: queryBuf, limit, reverse }) => {
57
+ const { spec, path } = this._enc(collection)
58
+ const owner = roomId ? rooms.get(roomId) : db
59
+ const query = queryBuf ? c.decode(c.json, queryBuf) : {}
60
+ const findOpts = {}
61
+ if (limit) findOpts.limit = limit
62
+ if (reverse) findOpts.reverse = reverse
63
+ const results = await owner.collection(collection).get(query, findOpts)
64
+ return { data: c.encode(c.array(spec.getEncoding(path)), results) }
65
+ })
66
+
67
+ rpc.onCollectionGet(async ({ collection, roomId, query: queryBuf }) => {
68
+ const { spec, path } = this._enc(collection)
69
+ const owner = roomId ? rooms.get(roomId) : db
70
+ const query = queryBuf ? c.decode(c.json, queryBuf) : {}
71
+ const results = await owner.collection(collection).get(query)
72
+ return { data: results.length ? spec.encode(path, results[0]) : null }
73
+ })
74
+
75
+ rpc.onCollectionUpdate(async ({ collection, roomId, query: queryBuf, changes: changesBuf }) => {
76
+ const { spec, path } = this._enc(collection)
77
+ const owner = roomId ? rooms.get(roomId) : db
78
+ const col = owner.collection(collection)
79
+ const query = c.decode(c.json, queryBuf)
80
+ const changes = c.decode(c.json, changesBuf)
81
+ const [existing] = await col.get(query)
82
+ if (!existing) return { data: null }
83
+ const updated = { ...existing, ...changes }
84
+ if (col.timestamps) updated.updatedAt = Date.now()
85
+ await col._write(`set-${col.type}`, updated)
86
+ return { data: spec.encode(path, updated) }
87
+ })
88
+
89
+ rpc.onCollectionDelete(async ({ collection, roomId, query: queryBuf }) => {
90
+ const owner = roomId ? rooms.get(roomId) : db
91
+ const query = queryBuf ? c.decode(c.json, queryBuf) : {}
92
+ const ok = await owner.collection(collection).del(query)
93
+ return { ok }
94
+ })
95
+
96
+ rpc.onCollectionSubscribe(async (stream) => {
97
+ const { collection, roomId, query: queryBuf, limit, reverse } = stream.data
98
+ const { spec, path } = this._enc(collection)
99
+ const owner = roomId ? rooms.get(roomId) : db
100
+ const query = queryBuf ? c.decode(c.json, queryBuf) : {}
101
+ const findOpts = {}
102
+ if (limit) findOpts.limit = limit
103
+ if (reverse) findOpts.reverse = reverse
104
+
105
+ const sub = owner.collection(collection).sub(query, findOpts)
106
+ sub.on('data', (docs) => {
107
+ stream.write({ data: c.encode(c.array(spec.getEncoding(path)), docs) })
108
+ })
109
+ stream.on('close', () => sub.destroy())
110
+ })
111
+
112
+ // === Rooms ===
113
+
114
+ rpc.onCreateRoom(async ({ name }) => {
115
+ const room = await Room.from(db, undefined, name ? { name } : {})
116
+ rooms.set(room.id, room)
117
+ return { data: { id: room.id, name } }
118
+ })
119
+
120
+ rpc.onJoinRoom(async ({ invite }) => {
121
+ const room = await Room.from(db, invite)
122
+ rooms.set(room.id, room)
123
+ return { data: { id: room.id } }
124
+ })
125
+
126
+ rpc.onOpenRoom(async ({ roomId }) => {
127
+ const existing = rooms.get(roomId)
128
+ if (existing) return { data: { id: existing.id } }
129
+ const room = await Room.from(db, roomId)
130
+ rooms.set(room.id, room)
131
+ return { data: { id: room.id } }
132
+ })
133
+
134
+ rpc.onCloseRoom(({ roomId }) => {
135
+ const room = rooms.get(roomId)
136
+ if (room) {
137
+ rooms.delete(roomId)
138
+ room.close()
139
+ }
140
+ })
141
+
142
+ rpc.onGetRooms(async () => {
143
+ const list = await db.rooms.list()
144
+ return { data: list || [] }
145
+ })
146
+
147
+ rpc.onWatchRooms(async (stream) => {
148
+ const load = async () => {
149
+ const list = await db.rooms.list()
150
+ stream.write({ data: list || [] })
151
+ }
152
+ load()
153
+ db.on('rooms:added', load)
154
+ db.on('rooms:removed', load)
155
+ stream.on('close', () => {
156
+ db.removeListener('rooms:added', load)
157
+ db.removeListener('rooms:removed', load)
158
+ })
159
+ })
160
+
161
+ // === Profile ===
162
+
163
+ rpc.onGetProfile(async () => {
164
+ return { data: (await db.profile?.get()) || null }
165
+ })
166
+
167
+ rpc.onSetProfile(({ data }) => {
168
+ db.profile.set(data)
169
+ })
170
+
171
+ rpc.onWatchProfile(async (stream) => {
172
+ const sub = db.profile.sub()
173
+ sub.on('data', (profile) => stream.write({ data: profile || null }))
174
+ stream.on('close', () => sub.destroy())
175
+ })
176
+
177
+ // === Members ===
178
+
179
+ rpc.onGetMembers(async ({ roomId }) => {
180
+ const room = rooms.get(roomId)
181
+ if (!room) return { data: [] }
182
+ const members = await room.members.get()
183
+ return { data: members || [] }
184
+ })
185
+
186
+ rpc.onWatchMembers(async (stream) => {
187
+ const { roomId } = stream.data
188
+ const room = rooms.get(roomId)
189
+ if (!room) return
190
+
191
+ const sub = room.members.sub()
192
+ sub.on('data', (members) => stream.write({ data: members || [] }))
193
+ stream.on('close', () => sub.destroy())
194
+ })
195
+
196
+ // === Invites ===
197
+
198
+ rpc.onCreateInvite(async ({ roomId }) => {
199
+ const room = rooms.get(roomId)
200
+ const { invite } = await room.invites.create()
201
+ return { invite }
202
+ })
203
+
204
+ // === Identity ===
205
+
206
+ rpc.onGetInfo(async () => {
207
+ return { id: db.id, deviceId: db.deviceId }
208
+ })
209
+
210
+ rpc.onGetSeed(async () => {
211
+ return { seed: db.seed() || '' }
212
+ })
213
+
214
+ rpc.onGetDevices(async () => {
215
+ const data = await db.devices?.get()
216
+ return { data: data || [] }
217
+ })
218
+
219
+ rpc.onWatchDevices(async (stream) => {
220
+ if (!db.devices?.sub) return
221
+ const sub = db.devices.sub()
222
+ sub.on('data', (devices) => stream.write({ data: devices || [] }))
223
+ stream.on('close', () => sub.destroy())
224
+ })
225
+
226
+ rpc.onCreateDeviceInvite(async () => {
227
+ const invite = await db.devices.invite()
228
+ return { invite }
229
+ })
230
+
231
+ rpc.onPairDevice(async ({ invite }) => {
232
+ await db.join(invite)
233
+ return { id: db.id, deviceId: db.deviceId }
234
+ })
235
+
236
+ rpc.onRecover(async ({ seed }) => {
237
+ await db.join(seed)
238
+ return { id: db.id, deviceId: db.deviceId }
239
+ })
240
+
241
+ // === Updates ===
242
+
243
+ rpc.onUpdatesSubscribe(async (stream) => {
244
+ const onUpdate = () => stream.write({ type: 'update' })
245
+ const onAdded = () => stream.write({ type: 'rooms:added' })
246
+ const onRemoved = () => stream.write({ type: 'rooms:removed' })
247
+ db.on('update', onUpdate)
248
+ db.on('rooms:added', onAdded)
249
+ db.on('rooms:removed', onRemoved)
250
+ stream.on('close', () => {
251
+ db.removeListener('update', onUpdate)
252
+ db.removeListener('rooms:added', onAdded)
253
+ db.removeListener('rooms:removed', onRemoved)
254
+ })
255
+ })
256
+ }
257
+ }
package/src/utils.js ADDED
@@ -0,0 +1,46 @@
1
+ import ms from 'ms'
2
+ import { Readable } from 'streamx'
3
+ import FramedStream from 'framed-stream'
4
+
5
+ export function getRpc(ipc, spec) {
6
+ const stream = new FramedStream(ipc)
7
+ stream.pause()
8
+ const rpc = new spec.rpc(stream)
9
+ rpc.resume = () => stream.resume()
10
+ return rpc
11
+ }
12
+
13
+ if (typeof AbortSignal !== 'undefined' && !AbortSignal.timeout) {
14
+ AbortSignal.timeout = (ms) => {
15
+ const controller = new AbortController()
16
+ setTimeout(() => controller.abort(new Error('Timeout')), ms)
17
+ return controller.signal
18
+ }
19
+ }
20
+
21
+ export function useTimeout(fn, opts = {}) {
22
+ const timeout = typeof opts.timeout === 'string' ? ms(opts.timeout) : opts.timeout
23
+ if (!timeout && !opts.signal) return fn()
24
+ if (typeof AbortSignal !== 'undefined' && AbortSignal.timeout) {
25
+ const signal = opts.signal || AbortSignal.timeout(timeout)
26
+ return new Promise((resolve, reject) => {
27
+ if (signal.aborted) return reject(signal.reason)
28
+ signal.addEventListener('abort', () => reject(signal.reason), { once: true })
29
+ fn().then(resolve, reject)
30
+ })
31
+ }
32
+ return new Promise((resolve, reject) => {
33
+ const timer = setTimeout(() => reject(new Error('Timeout')), timeout)
34
+ fn()
35
+ .then(resolve, reject)
36
+ .finally(() => clearTimeout(timer))
37
+ })
38
+ }
39
+
40
+ export function pipeStream(source, transform) {
41
+ const readable = new Readable()
42
+ source.on('data', (d) => readable.push(transform(d)))
43
+ source.on('close', () => readable.destroy())
44
+ readable.on('close', () => source.destroy())
45
+ return readable
46
+ }