teamplay 0.1.4 → 0.1.5
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/CHANGELOG.md +17 -0
- package/README.md +37 -17
- package/index.js +2 -3
- package/package.json +12 -6
- package/schema.js +1 -0
- package/server.js +9 -2
- package/schema/GUID_PATTERN.js +0 -1
- package/schema/associations.js +0 -51
- package/schema/pickFormFields.js +0 -104
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
# v0.1.5 (Mon May 27 2024)
|
|
2
|
+
|
|
3
|
+
#### 🚀 Enhancement
|
|
4
|
+
|
|
5
|
+
- feat: move backend implementation from startupjs. Add an example app. ([@cray0000](https://github.com/cray0000))
|
|
6
|
+
|
|
7
|
+
#### ⚠️ Pushed to `master`
|
|
8
|
+
|
|
9
|
+
- readme: add example readme ([@cray0000](https://github.com/cray0000))
|
|
10
|
+
- chore(example): add live reload support whenever client.js changes ([@cray0000](https://github.com/cray0000))
|
|
11
|
+
|
|
12
|
+
#### Authors: 1
|
|
13
|
+
|
|
14
|
+
- Pavel Zhukov ([@cray0000](https://github.com/cray0000))
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
1
18
|
# v0.1.4 (Thu May 23 2024)
|
|
2
19
|
|
|
3
20
|
#### 🐛 Bug Fix
|
package/README.md
CHANGED
|
@@ -30,14 +30,37 @@ import connect from 'teamplay/connect'
|
|
|
30
30
|
connect()
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
On the server you need to create the teamplay's backend and then create a connection handler for WebSockets:
|
|
34
34
|
|
|
35
35
|
```js
|
|
36
|
-
import { initConnection } from 'teamplay/server'
|
|
37
|
-
const
|
|
36
|
+
import { createBackend, initConnection } from 'teamplay/server'
|
|
37
|
+
const backend = createBackend()
|
|
38
|
+
const { upgrade } = initConnection(backend)
|
|
38
39
|
server.on('upgrade', upgrade) // Node's 'http' server instance
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
- for production use it's recommended to use MongoDB. It's gonna be automatically used if you set the env var `MONGO_URL`
|
|
43
|
+
- when deploying to a cluster with multiple instances you also have to set the env var `REDIS_URL` (Redis)
|
|
44
|
+
|
|
45
|
+
Without setting `MONGO_URL` the alternative `mingo` mock is used instead which persists data into an SQLite file `local.db` in the root of your project.
|
|
46
|
+
|
|
47
|
+
> [!NOTE]
|
|
48
|
+
> teamplay's `createBackend()` is a wrapper around creating a [ShareDB's backend](https://share.github.io/sharedb/api/backend).
|
|
49
|
+
> You can instead manually create a ShareDB backend yourself and pass it to `initConnection()`.
|
|
50
|
+
> `ShareDB` is re-exported from `teamplay/server`, you can get it as `import { ShareDB } from 'teamplay/server'`
|
|
51
|
+
|
|
52
|
+
## `initConnection(backend, options)`
|
|
53
|
+
|
|
54
|
+
**`backend`** - ShareDB backend instance
|
|
55
|
+
|
|
56
|
+
**`options`**:
|
|
57
|
+
|
|
58
|
+
### `fetchOnly` (default: `true`)
|
|
59
|
+
|
|
60
|
+
By default all subscriptions on the server are not reactive. This is strongly recommended.
|
|
61
|
+
|
|
62
|
+
If you need the subscriptions to reactively update data whenever it changes (the same way as they work on client-side), pass `{ fetchOnly: false }`.
|
|
63
|
+
|
|
41
64
|
## Usage
|
|
42
65
|
|
|
43
66
|
TBD
|
|
@@ -45,16 +68,17 @@ TBD
|
|
|
45
68
|
|
|
46
69
|
## Examples
|
|
47
70
|
|
|
71
|
+
For a simple working react app see [/example](/example)
|
|
72
|
+
|
|
48
73
|
### Simplest example with server synchronization
|
|
49
74
|
|
|
50
75
|
On the client we `connect()` to the server, and we have to wrap each React component into `observer()`:
|
|
51
76
|
|
|
52
77
|
```js
|
|
53
78
|
// client.js
|
|
79
|
+
import { createRoot } from 'react-dom/client'
|
|
54
80
|
import connect from 'teamplay/connect'
|
|
55
81
|
import { observer, $, sub } from 'teamplay'
|
|
56
|
-
import { createRoot } from 'react-dom/client'
|
|
57
|
-
import { createElement as el } from 'react'
|
|
58
82
|
|
|
59
83
|
connect()
|
|
60
84
|
|
|
@@ -62,14 +86,12 @@ const App = observer(({ userId }) => {
|
|
|
62
86
|
const $user = sub($.users[userId])
|
|
63
87
|
if (!$user.get()) throw $user.set({ points: 0 })
|
|
64
88
|
const { $points } = $user
|
|
65
|
-
const
|
|
66
|
-
return
|
|
89
|
+
const increment = () => $points.set($points.get() + 1)
|
|
90
|
+
return <button onClick={increment}>Points: {$points.get()}</button>
|
|
67
91
|
})
|
|
68
92
|
|
|
69
93
|
const container = document.body.appendChild(document.createElement('div'))
|
|
70
|
-
createRoot(container).render(
|
|
71
|
-
el(App, { userId: '_1' })
|
|
72
|
-
)
|
|
94
|
+
createRoot(container).render(<App userId='_1' />)
|
|
73
95
|
```
|
|
74
96
|
|
|
75
97
|
On the server we create the ShareDB backend and initialize the WebSocket connections handler:
|
|
@@ -77,21 +99,19 @@ On the server we create the ShareDB backend and initialize the WebSocket connect
|
|
|
77
99
|
```js
|
|
78
100
|
// server.js
|
|
79
101
|
import http from 'http'
|
|
80
|
-
import {
|
|
102
|
+
import { createBackend, initConnection } from 'teamplay/server'
|
|
81
103
|
|
|
82
104
|
const server = http.createServer() // you can pass expressApp here if needed
|
|
83
|
-
const backend =
|
|
105
|
+
const backend = createBackend()
|
|
84
106
|
const { upgrade } = initConnection(backend)
|
|
85
107
|
|
|
86
108
|
server.on('upgrade', upgrade)
|
|
87
109
|
|
|
88
|
-
server.listen(3000)
|
|
110
|
+
server.listen(3000, () => {
|
|
111
|
+
console.log('Server started. Open http://localhost:3000 in your browser')
|
|
112
|
+
})
|
|
89
113
|
```
|
|
90
114
|
|
|
91
|
-
ShareDB is a re-export of [`sharedb`](https://github.com/share/sharedb) library, check its docs for more info.
|
|
92
|
-
- for persistency and queries support pass [`sharedb-mongo`](https://github.com/share/sharedb-mongo) (which uses MongoDB) as `{ db }`
|
|
93
|
-
- when deploying to a cluster with multiple instances you also have to provide `{ pubsub }` like [`sharedb-redis-pubsub`](https://github.com/share/sharedb-redis-pubsub) (which uses Redis)
|
|
94
|
-
|
|
95
115
|
## License
|
|
96
116
|
|
|
97
117
|
MIT
|
package/index.js
CHANGED
|
@@ -16,9 +16,8 @@ export default $
|
|
|
16
16
|
export { default as sub } from './react/universalSub.js'
|
|
17
17
|
export { default as observer } from './react/observer.js'
|
|
18
18
|
export { connection, setConnection, getConnection, fetchOnly, setFetchOnly } from './orm/connection.js'
|
|
19
|
-
export
|
|
20
|
-
export {
|
|
21
|
-
export { default as pickFormFields } from './schema/pickFormFields.js'
|
|
19
|
+
export { GUID_PATTERN, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema'
|
|
20
|
+
export { aggregation, aggregationHeader } from '@teamplay/utils/aggregation'
|
|
22
21
|
|
|
23
22
|
export function getRootSignal (options) {
|
|
24
23
|
return _getRootSignal({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"./connect": "./connect/index.js",
|
|
10
10
|
"./server": "./server.js",
|
|
11
11
|
"./connect-test": "./connect/test.js",
|
|
12
|
-
"./cache": "./cache.js"
|
|
12
|
+
"./cache": "./cache.js",
|
|
13
|
+
"./schema": "./schema.js"
|
|
13
14
|
},
|
|
14
15
|
"publishConfig": {
|
|
15
16
|
"access": "public"
|
|
@@ -22,9 +23,12 @@
|
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@nx-js/observer-util": "^4.1.3",
|
|
25
|
-
"@teamplay/
|
|
26
|
-
"@teamplay/
|
|
27
|
-
"@teamplay/
|
|
26
|
+
"@teamplay/backend": "^0.1.5",
|
|
27
|
+
"@teamplay/cache": "^0.1.5",
|
|
28
|
+
"@teamplay/channel": "^0.1.5",
|
|
29
|
+
"@teamplay/debug": "^0.1.5",
|
|
30
|
+
"@teamplay/schema": "^0.1.5",
|
|
31
|
+
"@teamplay/utils": "^0.1.5",
|
|
28
32
|
"diff-match-patch": "^1.0.5",
|
|
29
33
|
"events": "^3.3.0",
|
|
30
34
|
"json0-ot-diff": "^1.1.2",
|
|
@@ -37,6 +41,8 @@
|
|
|
37
41
|
"@testing-library/react": "^15.0.7",
|
|
38
42
|
"jest": "^29.7.0",
|
|
39
43
|
"jest-environment-jsdom": "^29.7.0",
|
|
44
|
+
"react": "^18.3.1",
|
|
45
|
+
"react-dom": "^18.3.1",
|
|
40
46
|
"sharedb-mingo-memory": "^3.0.1"
|
|
41
47
|
},
|
|
42
48
|
"peerDependencies": {
|
|
@@ -58,5 +64,5 @@
|
|
|
58
64
|
]
|
|
59
65
|
},
|
|
60
66
|
"license": "MIT",
|
|
61
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "ad85edf62088d1e8d28e9b5e22f2dfb5dfc00bec"
|
|
62
68
|
}
|
package/schema.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@teamplay/schema'
|
package/server.js
CHANGED
|
@@ -2,11 +2,18 @@ import createChannel from '@teamplay/channel/server'
|
|
|
2
2
|
import { connection, setConnection, setFetchOnly } from './orm/connection.js'
|
|
3
3
|
|
|
4
4
|
export { default as ShareDB } from 'sharedb'
|
|
5
|
+
export {
|
|
6
|
+
default as createBackend,
|
|
7
|
+
mongo, mongoClient, createMongoIndex, redis, redlock, sqlite
|
|
8
|
+
} from '@teamplay/backend'
|
|
5
9
|
|
|
6
|
-
export
|
|
10
|
+
export function initConnection (backend, {
|
|
11
|
+
fetchOnly = true,
|
|
12
|
+
...options
|
|
13
|
+
} = {}) {
|
|
7
14
|
if (!backend) throw Error('backend is required')
|
|
8
15
|
if (connection) throw Error('Connection already exists')
|
|
9
16
|
setConnection(backend.connect())
|
|
10
|
-
setFetchOnly(
|
|
17
|
+
setFetchOnly(fetchOnly)
|
|
11
18
|
return createChannel(backend, options)
|
|
12
19
|
}
|
package/schema/GUID_PATTERN.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
package/schema/associations.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import GUID_PATTERN from './GUID_PATTERN.js'
|
|
2
|
-
|
|
3
|
-
export function belongsTo (collectionName) {
|
|
4
|
-
return {
|
|
5
|
-
type: 'string',
|
|
6
|
-
pattern: GUID_PATTERN,
|
|
7
|
-
$association: {
|
|
8
|
-
type: 'belongsTo',
|
|
9
|
-
collection: collectionName
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function hasMany (collectionName) {
|
|
15
|
-
return {
|
|
16
|
-
type: 'array',
|
|
17
|
-
items: {
|
|
18
|
-
type: 'string',
|
|
19
|
-
pattern: GUID_PATTERN
|
|
20
|
-
},
|
|
21
|
-
$association: {
|
|
22
|
-
type: 'hasMany',
|
|
23
|
-
collection: collectionName
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function hasManyFlags (collectionName) {
|
|
29
|
-
return {
|
|
30
|
-
type: 'object',
|
|
31
|
-
patternProperties: {
|
|
32
|
-
[GUID_PATTERN]: { type: 'boolean' }
|
|
33
|
-
},
|
|
34
|
-
additionalProperties: false,
|
|
35
|
-
$association: {
|
|
36
|
-
type: 'hasManyFlags',
|
|
37
|
-
collection: collectionName
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function hasOne (collectionName) {
|
|
43
|
-
return {
|
|
44
|
-
type: 'string',
|
|
45
|
-
pattern: GUID_PATTERN,
|
|
46
|
-
$association: {
|
|
47
|
-
type: 'hasOne',
|
|
48
|
-
collection: collectionName
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
package/schema/pickFormFields.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pick properties from json-schema to be used in a form.
|
|
3
|
-
* Supports simplified schema (just the properties object) and full schema.
|
|
4
|
-
* Performs extra transformations like auto-generating `label`.
|
|
5
|
-
* `createdAt`, `updatedAt`, `id`, `_id` fields are excluded by default.
|
|
6
|
-
* @param {Object} schema
|
|
7
|
-
* @param {Object|Array} options - exclude or include fields. If array, it's the same as passing { include: [...] }
|
|
8
|
-
* @param {Array} options.include - list of fields to pick (default: all)
|
|
9
|
-
* @param {Array} options.exclude - list of fields to exclude (default: none)
|
|
10
|
-
* @param {Boolean} options.freeze - whether to deep freeze the result (default: true)
|
|
11
|
-
*/
|
|
12
|
-
export default function pickFormFields (schema, options) {
|
|
13
|
-
try {
|
|
14
|
-
let include, exclude, freeze
|
|
15
|
-
if (Array.isArray(options)) {
|
|
16
|
-
include = options
|
|
17
|
-
} else {
|
|
18
|
-
;({ include, exclude, freeze = true } = options || {})
|
|
19
|
-
}
|
|
20
|
-
exclude ??= []
|
|
21
|
-
if (!schema) throw Error('pickFormFields: schema is required')
|
|
22
|
-
schema = JSON.parse(JSON.stringify(schema))
|
|
23
|
-
if (schema.type === 'object') {
|
|
24
|
-
schema = schema.properties
|
|
25
|
-
}
|
|
26
|
-
for (const key in schema) {
|
|
27
|
-
if (shouldIncludeField(key, schema[key], { include, exclude })) {
|
|
28
|
-
const field = schema[key]
|
|
29
|
-
if (!field.label) field.label = camelCaseToLabel(key)
|
|
30
|
-
} else {
|
|
31
|
-
delete schema[key]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (freeze) return new Proxy(schema, deepFreezeHandler)
|
|
35
|
-
return schema
|
|
36
|
-
} catch (err) {
|
|
37
|
-
throw Error(`
|
|
38
|
-
pickFormFields: ${err.message}
|
|
39
|
-
schema:\n${JSON.stringify(schema, null, 2)}
|
|
40
|
-
`)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Proxy handlers to deep freeze schema to prevent accidental mutations.
|
|
45
|
-
// For this, when we .get() a property, we also return the same recursive Proxy handler if it's an object.
|
|
46
|
-
const deepFreezeHandler = {
|
|
47
|
-
get (target, prop) {
|
|
48
|
-
const value = target[prop]
|
|
49
|
-
if (typeof value === 'object' && value !== null) {
|
|
50
|
-
return new Proxy(value, deepFreezeHandler)
|
|
51
|
-
}
|
|
52
|
-
return value
|
|
53
|
-
},
|
|
54
|
-
set () {
|
|
55
|
-
throw Error(ERRORS.schemaIsFrozen)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function shouldIncludeField (key, field, { include, exclude = [] } = {}) {
|
|
60
|
-
if (!field) throw Error(`field "${key}" does not have a schema definition`)
|
|
61
|
-
if (include?.includes(key)) return true
|
|
62
|
-
if (exclude.includes(key)) return false
|
|
63
|
-
// if field has 'input' specified then it's an explicit indicator that it can be used in forms,
|
|
64
|
-
// so the default exclusion rules don't apply
|
|
65
|
-
if (!field.input) {
|
|
66
|
-
// exclude some meta fields by default
|
|
67
|
-
if (DEFAULT_EXCLUDE_FORM_FIELDS.includes(key)) return false
|
|
68
|
-
// exclude foreign keys by default
|
|
69
|
-
// Foreign keys have a custom `$association` property set by belongsTo/hasMany/hasOne helpers
|
|
70
|
-
if (field.$association) return false
|
|
71
|
-
}
|
|
72
|
-
// if include array is not explicitly set, include all fields by default
|
|
73
|
-
if (!include) return true
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const DEFAULT_EXCLUDE_FORM_FIELDS = ['id', '_id', 'createdAt', 'updatedAt']
|
|
78
|
-
|
|
79
|
-
// split into words, capitalize first word, make others lowercase
|
|
80
|
-
function camelCaseToLabel (str) {
|
|
81
|
-
return str
|
|
82
|
-
.replace(/([A-Z])/g, ' $1')
|
|
83
|
-
.toLowerCase()
|
|
84
|
-
.replace(/^./, (s) => s.toUpperCase())
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const ERRORS = {
|
|
88
|
-
schemaIsFrozen: `
|
|
89
|
-
Form fields are immutable.
|
|
90
|
-
If you want to change them, clone them with \`JSON.parse(JSON.stringify(FORM_FIELDS))\`.
|
|
91
|
-
|
|
92
|
-
If you want to do it inside react component, you can use this pattern for the most effective cloning:
|
|
93
|
-
|
|
94
|
-
\`\`\`
|
|
95
|
-
const $fields = useValue$(useMemo(() => JSON.parse(JSON.stringify(FORM_FIELDS)), []))
|
|
96
|
-
\`\`\`
|
|
97
|
-
|
|
98
|
-
and then pass $fields to the Form component like this:
|
|
99
|
-
|
|
100
|
-
\`\`\`
|
|
101
|
-
<Form $fields={$fields} $value={$value} />
|
|
102
|
-
\`\`\`
|
|
103
|
-
`
|
|
104
|
-
}
|