teamplay 0.1.3 → 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 CHANGED
@@ -1,3 +1,32 @@
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
+
18
+ # v0.1.4 (Thu May 23 2024)
19
+
20
+ #### 🐛 Bug Fix
21
+
22
+ - fix: rename old references to startupjs packages into teamplay packages ([@cray0000](https://github.com/cray0000))
23
+
24
+ #### Authors: 1
25
+
26
+ - Pavel Zhukov ([@cray0000](https://github.com/cray0000))
27
+
28
+ ---
29
+
1
30
  # v0.1.3 (Thu May 23 2024)
2
31
 
3
32
  #### 🐛 Bug Fix
package/README.md CHANGED
@@ -30,14 +30,37 @@ import connect from 'teamplay/connect'
30
30
  connect()
31
31
  ```
32
32
 
33
- And on the server, manually create a [ShareDB's backend](https://share.github.io/sharedb/api/backend) and create a connection handler for WebSockets:
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 { upgrade } = initConnection(backend) // ShareDB's Backend instance
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 onClick = () => $points.set($points.get() + 1)
66
- return el('button', { onClick }, 'Points: ' + $points.get())
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 { ShareDB, initConnection } from 'teamplay/server'
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 = new ShareDB()
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/connect/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import Socket from '@startupjs/channel'
1
+ import Socket from '@teamplay/channel'
2
2
  import Connection from './sharedbConnection.cjs'
3
3
  import { connection, setConnection } from '../orm/connection.js'
4
4
 
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 * from './schema/associations.js'
20
- export { default as GUID_PATTERN } from './schema/GUID_PATTERN.js'
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/orm/connection.js CHANGED
@@ -20,7 +20,7 @@ const ERRORS = {
20
20
  You must set the initialized ShareDB connection before using subscriptions.
21
21
  You've probably forgotten to call connect() in your app:
22
22
 
23
- import connect from '@startupjs/signals-orm/connect'
23
+ import connect from 'teamplay/connect'
24
24
  connect({ baseUrl: 'http://localhost:3000' })
25
25
  `
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.1.3",
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/cache": "^0.1.3",
26
- "@teamplay/channel": "^0.1.3",
27
- "@teamplay/debug": "^0.1.3",
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": "e0ed8221ff2e4691a2990813d5f07e128b11ab7a"
67
+ "gitHead": "ad85edf62088d1e8d28e9b5e22f2dfb5dfc00bec"
62
68
  }
package/schema.js ADDED
@@ -0,0 +1 @@
1
+ export * from '@teamplay/schema'
package/server.js CHANGED
@@ -1,12 +1,19 @@
1
- import createChannel from '@startupjs/channel/server'
1
+ 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 default function initConnection (backend, options) {
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(true)
17
+ setFetchOnly(fetchOnly)
11
18
  return createChannel(backend, options)
12
19
  }
@@ -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}$'
@@ -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
- }
@@ -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
- }