teamplay 0.3.29 → 0.3.34

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.
@@ -0,0 +1,81 @@
1
+ // client-side PubSub implementation for cross-tab communication using BroadcastChannel API.
2
+ const PubSub = require('sharedb').PubSub
3
+
4
+ const subscribedChannels = new Map()
5
+ const NAMESPACE = 'sharedb-crosstab-pubsub'
6
+
7
+ function namespaceChannel (channel) {
8
+ return `${NAMESPACE}-${channel}`
9
+ }
10
+
11
+ function CrossTabPubSub ({ onMessage, ...options } = {}) {
12
+ if (!(this instanceof CrossTabPubSub)) return new CrossTabPubSub({ onMessage, ...options })
13
+ PubSub.call(this, options)
14
+ this._onMessage = onMessage
15
+ }
16
+
17
+ module.exports = CrossTabPubSub
18
+
19
+ CrossTabPubSub.prototype = Object.create(PubSub.prototype)
20
+
21
+ CrossTabPubSub.prototype.close = function (callback) {
22
+ if (!callback) {
23
+ callback = function (err) {
24
+ if (err) throw err
25
+ }
26
+ }
27
+
28
+ PubSub.prototype.close.call(this, (err) => {
29
+ if (err) return callback(err)
30
+ for (const bc of subscribedChannels.values()) bc.close()
31
+ subscribedChannels.clear()
32
+ callback?.()
33
+ })
34
+ }
35
+
36
+ CrossTabPubSub.prototype._subscribe = function (channel, callback) {
37
+ if (subscribedChannels.has(channel)) {
38
+ return callback?.(new AlreadySubscribedError(channel))
39
+ }
40
+ const bc = new BroadcastChannel(namespaceChannel(channel))
41
+ subscribedChannels.set(channel, bc)
42
+ bc.addEventListener('message', ({ data }) => {
43
+ this._emit(channel, data)
44
+ this._onMessage?.(channel, data)
45
+ })
46
+ callback?.()
47
+ }
48
+
49
+ CrossTabPubSub.prototype._unsubscribe = function (channel, callback) {
50
+ const bc = subscribedChannels.get(channel)
51
+ if (!bc) return callback?.(new NotSubscribedError(channel))
52
+ bc.close()
53
+ subscribedChannels.delete(channel)
54
+ callback?.()
55
+ }
56
+
57
+ CrossTabPubSub.prototype._publish = function (channels, data, callback) {
58
+ for (const channel of (channels || [])) {
59
+ if (this.subscribed[channel]) {
60
+ const bc = subscribedChannels.get(channel)
61
+ if (!bc) return callback?.(new NotSubscribedError(channel))
62
+ bc.postMessage(data)
63
+ this._emit(channel, data)
64
+ }
65
+ }
66
+ callback?.()
67
+ }
68
+
69
+ class AlreadySubscribedError extends Error {
70
+ constructor (channel) {
71
+ super(`[sharedb-crosstab-pubsub] Already subscribed to channel: ${channel}`)
72
+ this.name = 'AlreadySubscribedError'
73
+ }
74
+ }
75
+
76
+ class NotSubscribedError extends Error {
77
+ constructor (channel) {
78
+ super(`[sharedb-crosstab-pubsub] Not subscribed to channel: ${channel}`)
79
+ this.name = 'NotSubscribedError'
80
+ }
81
+ }
@@ -0,0 +1,124 @@
1
+ // Offline 'connect' implementation with persistence to local storage.
2
+ // This creates a full sharedb server with mingo database in the browser or react-native app.
3
+ import ShareDbMingo from '@startupjs/sharedb-mingo-memory'
4
+ import ShareBackend from 'sharedb'
5
+ import { connection, setConnection } from '../../orm/connection.js'
6
+
7
+ const STORAGE_NAMESPACE = 'teamplay-offline'
8
+ const DOCS_PREFIX = `${STORAGE_NAMESPACE}:docs:`
9
+ const LAST_OP_PREFIX = `${STORAGE_NAMESPACE}:last-op:`
10
+
11
+ export default function createConnectWithPersistence ({ storage, createPubsub } = {}) {
12
+ if (!storage) throw new Error('[connect-offline] storage is required')
13
+ return async function connect () {
14
+ if (connection) return
15
+ const db = new ShareDbMingo()
16
+ const options = { db }
17
+ const { pubsub } = (await init(db, storage, createPubsub)) || {}
18
+ if (pubsub) options.pubsub = pubsub
19
+ const backend = new ShareBackend(options)
20
+ setConnection(backend.connect())
21
+ }
22
+ }
23
+
24
+ async function init (db, storage, createPubsub) {
25
+ await loadData(db, storage)
26
+ addPersistence(db, storage)
27
+ globalThis.db = db
28
+ const pubsub = createPubsub
29
+ ? createPubsub((channel, data) => {
30
+ if (!(data?.c && data?.d)) return
31
+ updateDocInDb(db, storage, data.c, data.d)
32
+ })
33
+ : null
34
+ return { pubsub }
35
+ }
36
+
37
+ // do same thing as in loadData but for a single doc
38
+ async function updateDocInDb (db, storage, collection, docId) {
39
+ try {
40
+ const snapshot = await storage.getItem(getDocsKey(collection, docId))
41
+ if (!snapshot) return
42
+
43
+ if (!db.docs[collection]) {
44
+ db.docs[collection] = {}
45
+ db.ops[collection] = {}
46
+ }
47
+ if (snapshot && typeof snapshot === 'object' && snapshot.v == null) snapshot.v = 0
48
+ db.docs[collection][docId] = snapshot
49
+ if (!db.ops[collection]) db.ops[collection] = {}
50
+ const lastOp = await storage.getItem(getLastOpKey(collection, docId))
51
+ db.ops[collection][docId] = buildOpsArray(lastOp)
52
+ } catch (err) {
53
+ console.error('Error updating doc from storage:', err)
54
+ }
55
+ }
56
+
57
+ async function loadData (db, storage) {
58
+ const docsToLoad = []
59
+ await storage.iterate((value, key) => {
60
+ const parsedKey = parseStorageKey(key)
61
+ if (!parsedKey || parsedKey.type !== 'docs') return
62
+ const { collection, docId } = parsedKey
63
+ if (!db.docs[collection]) {
64
+ db.docs[collection] = {}
65
+ db.ops[collection] = {}
66
+ }
67
+ // We don't support multiplayer in offline-only mode.
68
+ // Note: if you have multiple tabs open in browser then the last operation wins.
69
+ if (value && typeof value === 'object' && value.v == null) value.v = 0
70
+ db.docs[collection][docId] = value
71
+ if (!db.ops[collection][docId]) db.ops[collection][docId] = []
72
+ docsToLoad.push({ collection, docId })
73
+ })
74
+ for (const { collection, docId } of docsToLoad) {
75
+ const lastOp = await storage.getItem(getLastOpKey(collection, docId))
76
+ if (!db.ops[collection]) db.ops[collection] = {}
77
+ db.ops[collection][docId] = buildOpsArray(lastOp)
78
+ }
79
+ console.log('DB data was loaded from storage to shareDbMingo')
80
+ }
81
+
82
+ function addPersistence (db, storage) {
83
+ const originalCommit = db.commit
84
+
85
+ db.commit = function (collection, docId, op, snapshot, options, callback) {
86
+ originalCommit.call(this, collection, docId, op, snapshot, options, async err => {
87
+ if (err) return callback(err)
88
+
89
+ try {
90
+ await storage.setItem(getDocsKey(collection, docId), snapshot)
91
+ await storage.setItem(getLastOpKey(collection, docId), op)
92
+ } catch (err) {
93
+ throw Error('Error saving to storage:\n', err.message)
94
+ }
95
+
96
+ callback(null, true)
97
+ })
98
+ }
99
+ }
100
+
101
+ function parseStorageKey (key) {
102
+ const [namespace, type, collection, ...docSegments] = key.split(':')
103
+ if (namespace !== STORAGE_NAMESPACE) return null
104
+ return { type, collection, docId: docSegments.join(':') }
105
+ }
106
+
107
+ function getDocsKey (collection, docId) {
108
+ return `${DOCS_PREFIX}${collection}:${docId}`
109
+ }
110
+
111
+ function getLastOpKey (collection, docId) {
112
+ return `${LAST_OP_PREFIX}${collection}:${docId}`
113
+ }
114
+
115
+ function buildOpsArray (lastOp) {
116
+ if (!lastOp) return []
117
+ const version = lastOp.v
118
+ if (Number.isFinite(version) && version > 0) {
119
+ const ops = new Array(version + 1)
120
+ ops[version] = lastOp
121
+ return ops
122
+ }
123
+ return [lastOp]
124
+ }
@@ -0,0 +1,28 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage'
2
+ import createConnectWithPersistence from './index.js'
3
+
4
+ async function getItem (key) {
5
+ const value = await AsyncStorage.getItem(key)
6
+ if (value == null) return null
7
+ try {
8
+ return JSON.parse(value)
9
+ } catch {
10
+ return value
11
+ }
12
+ }
13
+
14
+ async function setItem (key, value) {
15
+ return AsyncStorage.setItem(key, JSON.stringify(value))
16
+ }
17
+
18
+ async function iterate (iterator) {
19
+ const keys = await AsyncStorage.getAllKeys()
20
+ for (const key of keys) {
21
+ const value = await getItem(key)
22
+ await iterator(value, key)
23
+ }
24
+ }
25
+
26
+ export const storage = { getItem, setItem, iterate }
27
+
28
+ export default createConnectWithPersistence({ storage })
@@ -0,0 +1,15 @@
1
+ import localforage from 'localforage'
2
+ import SharedbCrosstabPubsub from '../lib/sharedb-crosstab-pubsub.cjs'
3
+ import createConnectWithPersistence from './index.js'
4
+
5
+ export const storage = {
6
+ getItem: key => localforage.getItem(key),
7
+ setItem: (key, value) => localforage.setItem(key, value),
8
+ iterate: iterator => localforage.iterate(iterator)
9
+ }
10
+
11
+ export function createPubsub (onMessage) {
12
+ return new SharedbCrosstabPubsub({ onMessage })
13
+ }
14
+
15
+ export default createConnectWithPersistence({ storage, createPubsub })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.3.29",
3
+ "version": "0.3.34",
4
4
  "description": "Full-stack signals ORM with multiplayer",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -9,6 +9,10 @@
9
9
  "./connect": "./connect/index.js",
10
10
  "./server": "./server.js",
11
11
  "./connect-test": "./connect/test.js",
12
+ "./connect-offline": {
13
+ "react-native": "./connect/offline/react-native.js",
14
+ "default": "./connect/offline/web.js"
15
+ },
12
16
  "./cache": "./cache.js",
13
17
  "./schema": "./schema.js"
14
18
  },
@@ -23,21 +27,23 @@
23
27
  },
24
28
  "dependencies": {
25
29
  "@nx-js/observer-util": "^4.1.3",
26
- "@teamplay/backend": "^0.3.29",
27
- "@teamplay/cache": "^0.3.29",
28
- "@teamplay/channel": "^0.3.29",
29
- "@teamplay/debug": "^0.3.29",
30
- "@teamplay/schema": "^0.3.29",
31
- "@teamplay/utils": "^0.3.29",
30
+ "@startupjs/sharedb-mingo-memory": "^4.0.0-2",
31
+ "@teamplay/backend": "^0.3.34",
32
+ "@teamplay/cache": "^0.3.34",
33
+ "@teamplay/channel": "^0.3.34",
34
+ "@teamplay/debug": "^0.3.34",
35
+ "@teamplay/schema": "^0.3.34",
36
+ "@teamplay/utils": "^0.3.34",
32
37
  "diff-match-patch": "^1.0.5",
33
38
  "events": "^3.3.0",
34
39
  "json0-ot-diff": "^1.1.2",
40
+ "localforage": "^1.10.0",
35
41
  "lodash": "^4.17.20",
36
- "sharedb": "^5.0.0"
42
+ "sharedb": "^5.0.0",
43
+ "stream": "npm:readable-stream@^4.7.0"
37
44
  },
38
45
  "devDependencies": {
39
46
  "@jest/globals": "^29.7.0",
40
- "@startupjs/sharedb-mingo-memory": "^4.0.0-1",
41
47
  "@testing-library/react": "^15.0.7",
42
48
  "jest": "^29.7.0",
43
49
  "jest-environment-jsdom": "^29.7.0",
@@ -46,11 +52,15 @@
46
52
  "react-dom": "^18.3.1"
47
53
  },
48
54
  "peerDependencies": {
49
- "@startupjs/sharedb-mingo-memory": "*",
50
- "react": "*"
55
+ "@react-native-async-storage/async-storage": "*",
56
+ "react": "*",
57
+ "react-native": "*"
51
58
  },
52
59
  "peerDependenciesMeta": {
53
- "@startupjs/sharedb-mingo-memory": {
60
+ "@react-native-async-storage/async-storage": {
61
+ "optional": true
62
+ },
63
+ "react-native": {
54
64
  "optional": true
55
65
  }
56
66
  },
@@ -64,5 +74,5 @@
64
74
  ]
65
75
  },
66
76
  "license": "MIT",
67
- "gitHead": "9915cfe479d8d218c47706394d51a296a0271477"
77
+ "gitHead": "5532c9c47d6817e1291420ec505a65482072872b"
68
78
  }