@logux/client
Version:
Logux base components to build web client
208 lines (183 loc) • 5.66 kB
JavaScript
import { actionEvents, LoguxError } from '@logux/core'
import { Client } from '../client/index.js'
function storageKey(client, name) {
return client.options.prefix + ':' + client.options.userId + ':' + name
}
function sendToTabs(client, event, data) {
if (!client.isLocalStorage) return
let key = storageKey(client, event)
let json = JSON.stringify(data)
try {
localStorage.setItem(key, json)
} catch (e) {
console.error(e)
client.isLocalStorage = false
client.role = 'leader'
client.emitter.emit('role')
if (client.autoconnect) client.node.connection.connect()
}
}
function setState(client, state) {
client.state = state
client.emitter.emit('state')
sendToTabs(client, 'state', client.state)
}
function isMemory(store) {
return Array.isArray(store.entries) && Array.isArray(store.added)
}
export class CrossTabClient extends Client {
set state(value) {
this.leaderState = value
}
get state() {
return this.leaderState
}
constructor(opts = {}) {
super(opts)
this.leaderState = this.node.state
this.role = 'follower'
this.node.on('state', () => {
if (this.role === 'leader') {
setState(this, this.node.state)
}
})
this.log.on('add', (action, meta) => {
actionEvents(this.emitter, 'add', action, meta)
if (meta.tab !== this.tabId) {
sendToTabs(this, 'add', [this.tabId, action, meta])
}
})
this.log.on('clean', (action, meta) => {
actionEvents(this.emitter, 'clean', action, meta)
})
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('storage', e => this.onStorage(e))
window.addEventListener('pagehide', e => this.onUnload(e))
}
if (this.isLocalStorage) {
let subprotocolKey = storageKey(this, 'subprotocol')
if (localStorage.getItem(subprotocolKey) !== this.options.subprotocol) {
sendToTabs(this, 'subprotocol', this.options.subprotocol)
}
}
}
changeUser(userId, token) {
sendToTabs(this, 'user', [this.tabId, userId])
super.changeUser(userId, token)
}
clean() {
if (this.isLocalStorage) {
localStorage.removeItem(storageKey(this, 'add'))
localStorage.removeItem(storageKey(this, 'state'))
localStorage.removeItem(storageKey(this, 'client'))
}
return super.clean()
}
destroy() {
super.destroy()
this.role = 'follower'
this.emitter.emit('role')
if (this.unlead) this.unlead()
if (typeof window !== 'undefined' && window.removeEventListener) {
window.removeEventListener('storage', this.onStorage)
}
}
getClientId() {
let key = storageKey(this, 'client')
if (!this.isLocalStorage) {
return super.getClientId()
} else if (localStorage.getItem(key)) {
return localStorage.getItem(key)
} else {
let clientId = super.getClientId()
localStorage.setItem(key, clientId)
return clientId
}
}
on(event, listener) {
if (event === 'preadd') {
return this.log.emitter.on(event, listener)
} else {
return this.emitter.on(event, listener)
}
}
onStorage(e) {
if (e.newValue === null) return
let data
if (e.key === storageKey(this, 'add')) {
data = JSON.parse(e.newValue)
if (data[0] !== this.tabId) {
let action = data[1]
let meta = data[2]
if (!meta.tab || meta.tab === this.tabId) {
if (isMemory(this.log.store)) {
this.log.store.add(action, meta)
}
actionEvents(this.emitter, 'add', action, meta)
if (this.role === 'leader') {
this.node.onAdd(action, meta)
}
}
}
} else if (e.key === storageKey(this, 'state')) {
let state = JSON.parse(localStorage.getItem(e.key))
if (this.leaderState !== state) {
this.leaderState = state
this.emitter.emit('state')
}
} else if (e.key === storageKey(this, 'user')) {
data = JSON.parse(e.newValue)
if (data[0] !== this.tabId) {
this.emitter.emit('user', data[1])
}
} else if (e.key === storageKey(this, 'subprotocol')) {
let other = JSON.parse(e.newValue)
if (this.options.subprotocol > other) {
sendToTabs(this, 'subprotocol', this.options.subprotocol)
} else if (this.options.subprotocol < other) {
let err = new LoguxError(
'wrong-subprotocol',
{ supported: other, used: this.options.subprotocol },
true
)
this.node.emitter.emit('error', err)
}
}
}
start(connect = true) {
this.autoconnect = connect
this.cleanPrevActions()
if (
typeof navigator === 'undefined' ||
!navigator.locks ||
!this.isLocalStorage
) {
this.role = 'leader'
this.emitter.emit('role')
if (connect) this.node.connection.connect()
return
}
let json = localStorage.getItem(storageKey(this, 'state'))
if (json && json !== null && json !== '"disconnected"') {
this.state = JSON.parse(json)
this.emitter.emit('state')
}
navigator.locks.request('logux_leader', () => {
this.role = 'leader'
this.emitter.emit('role')
if (connect) this.node.connection.connect()
return new Promise(resolve => {
this.unlead = resolve
})
})
}
type(type, listener, opts = {}) {
if (opts.event === 'preadd') {
return this.log.type(type, listener, opts)
} else {
let event = opts.event || 'add'
let id = opts.id || ''
return this.emitter.on(`${event}-${type}-${id}`, listener)
}
}
}