@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 +68 -0
- package/package.json +45 -0
- package/src/build.js +6 -0
- package/src/client.js +97 -0
- package/src/collection.js +69 -0
- package/src/index.js +5 -0
- package/src/room.js +50 -0
- package/src/schema.js +125 -0
- package/src/server.js +257 -0
- package/src/utils.js +46 -0
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
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
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
|
+
}
|