@logux/client
Advanced tools
Comparing version 0.9.3 to 0.10.0
@@ -14,2 +14,2 @@ import { Client } from '../client/index.js' | ||
*/ | ||
export function attention (client: Client): () => void | ||
export function attention(client: Client): () => void |
@@ -1,2 +0,2 @@ | ||
function attention (client) { | ||
export function attention(client) { | ||
let doc = document | ||
@@ -59,3 +59,1 @@ let originTitle = false | ||
} | ||
export { attention } |
import { Client } from '../client/index.js' | ||
export type BadgeMessages = { | ||
export interface BadgeMessages { | ||
synchronized: string | ||
@@ -14,3 +14,3 @@ disconnected: string | ||
export type BadgeStyles = { | ||
export interface BadgeStyles { | ||
base: object | ||
@@ -35,3 +35,3 @@ text: object | ||
type BadgeOptions = { | ||
interface BadgeOptions { | ||
/** | ||
@@ -88,3 +88,3 @@ * Widget text for different states. | ||
*/ | ||
export function badge (client: Client, opts: BadgeOptions): () => void | ||
export function badge(client: Client, opts: BadgeOptions): () => void | ||
@@ -91,0 +91,0 @@ /** |
import { status } from '../status/index.js' | ||
function injectStyles (element, styles) { | ||
function injectStyles(element, styles) { | ||
for (let i in styles) { | ||
@@ -9,3 +9,3 @@ element.style[i] = styles[i] | ||
function setPosition (element, position) { | ||
function setPosition(element, position) { | ||
let style = element.style | ||
@@ -44,3 +44,3 @@ if (position === 'middle-center' || position === 'center-middle') { | ||
function badge (client, opts) { | ||
export function badge(client, opts) { | ||
let messages = opts.messages | ||
@@ -105,3 +105,3 @@ let position = opts.position || 'bottom-right' | ||
let badgeRu = { | ||
export let badgeRu = { | ||
synchronized: 'Ваши данные сохранены', | ||
@@ -117,3 +117,3 @@ disconnected: 'Нет интернета', | ||
let badgeEn = { | ||
export let badgeEn = { | ||
synchronized: 'Your data has been saved', | ||
@@ -128,3 +128,1 @@ disconnected: 'No Internet connection', | ||
} | ||
export { badge, badgeEn, badgeRu } |
@@ -6,3 +6,3 @@ import refresh from './refresh.svg' | ||
let badgeStyles = { | ||
export let badgeStyles = { | ||
base: { | ||
@@ -54,3 +54,1 @@ position: 'fixed', | ||
} | ||
export { badgeStyles } |
@@ -10,3 +10,4 @@ import { Unsubscribe } from 'nanoevents' | ||
Meta, | ||
TokenGenerator | ||
TokenGenerator, | ||
AnyAction | ||
} from '@logux/core' | ||
@@ -16,7 +17,7 @@ | ||
export interface ClientActionListener { | ||
(action: Action, meta: ClientMeta): void | ||
export interface ClientActionListener<ListenAction extends Action> { | ||
(action: ListenAction, meta: ClientMeta): void | ||
} | ||
export type ClientMeta = Meta & { | ||
export interface ClientMeta extends Meta { | ||
/** | ||
@@ -38,3 +39,3 @@ * Action should be visible only for browser tab with the same `client.tabId`. | ||
export type ClientOptions = { | ||
export interface ClientOptions { | ||
/** | ||
@@ -130,7 +131,10 @@ * Server URL. | ||
*/ | ||
export class Client<H extends object = {}, L extends Log = Log<ClientMeta>> { | ||
export class Client< | ||
Headers extends object = {}, | ||
ClientLog extends Log = Log<ClientMeta> | ||
> { | ||
/** | ||
* @param opts Client options. | ||
*/ | ||
constructor (opts: ClientOptions) | ||
constructor(opts: ClientOptions) | ||
@@ -176,3 +180,3 @@ /** | ||
*/ | ||
log: L | ||
log: ClientLog | ||
@@ -186,5 +190,24 @@ /** | ||
*/ | ||
node: ClientNode<H, L> | ||
node: ClientNode<Headers, ClientLog> | ||
/** | ||
* Leader tab synchronization state. It can differs | ||
* from `client.node.state` (because only the leader tab keeps connection). | ||
* | ||
* ```js | ||
* client.on('state', () => { | ||
* if (client.state === 'disconnected' && client.state === 'sending') { | ||
* showCloseWarning() | ||
* } | ||
* }) | ||
* ``` | ||
*/ | ||
state: ClientNode['state'] | ||
/** | ||
* Is leader tab connected to server. | ||
*/ | ||
connected: boolean | ||
/** | ||
* Connect to server and reconnect on any connection problem. | ||
@@ -196,5 +219,48 @@ * | ||
*/ | ||
start (): void | ||
start(): void | ||
/** | ||
* Send action to the server (by setting `meta.sync` and adding to the log) | ||
* and track server processing. | ||
* | ||
* ```js | ||
* showLoader() | ||
* client.sync( | ||
* { type: 'CHANGE_NAME', name } | ||
* ).then(() => { | ||
* hideLoader() | ||
* }).catch(error => { | ||
* hideLoader() | ||
* showError(error.action.reason) | ||
* }) | ||
* ``` | ||
* | ||
* @param action The action | ||
* @param meta Optional meta. | ||
* @returns Promise for server processing. | ||
*/ | ||
sync(action: AnyAction, meta?: Partial<ClientMeta>): Promise<ClientMeta> | ||
/** | ||
* Add listener for adding action with specific type. | ||
* Works faster than `on('add', cb)` with `if`. | ||
* | ||
* ```js | ||
* client.type('rename', (action, meta) => { | ||
* name = action.name | ||
* }) | ||
* ``` | ||
* | ||
* @param type Action’s type. | ||
* @param ActionListener The listener function. | ||
* @param event | ||
* @returns Unbind listener from event. | ||
*/ | ||
type<TypeAction extends Action = Action>( | ||
type: TypeAction['type'], | ||
listener: ClientActionListener<TypeAction>, | ||
opts?: { id?: string; event?: 'preadd' | 'add' | 'clean' } | ||
): Unsubscribe | ||
/** | ||
* Subscribe for synchronization events. It implements Nano Events API. | ||
@@ -208,2 +274,4 @@ * Supported events: | ||
* | ||
* Note, that `Log#type()` will work faster than `on` event with `if`. | ||
* | ||
* ```js | ||
@@ -219,7 +287,8 @@ * client.on('add', (action, meta) => { | ||
*/ | ||
on ( | ||
on(event: 'state', listener: () => void): Unsubscribe | ||
on( | ||
event: 'preadd' | 'add' | 'clean', | ||
listener: ClientActionListener | ||
listener: ClientActionListener<Action> | ||
): Unsubscribe | ||
on (event: 'user', listener: (userId: string) => void): Unsubscribe | ||
on(event: 'user', listener: (userId: string) => void): Unsubscribe | ||
@@ -244,5 +313,17 @@ /** | ||
*/ | ||
changeUser (userId: string, token?: string): void | ||
changeUser(userId: string, token?: string): void | ||
/** | ||
* Wait for specific state of the leader tab. | ||
* | ||
* ```js | ||
* await client.waitFor('synchronized') | ||
* hideLoader() | ||
* ``` | ||
* | ||
* @param state State name | ||
*/ | ||
waitFor(state: ClientNode['state']): Promise<void> | ||
/** | ||
* Disconnect and stop synchronization. | ||
@@ -256,3 +337,3 @@ * | ||
*/ | ||
destroy (): void | ||
destroy(): void | ||
@@ -270,3 +351,3 @@ /** | ||
*/ | ||
clean (): Promise<void> | ||
clean(): Promise<void> | ||
} |
import { createNanoEvents } from 'nanoevents' | ||
import { isFirstOlder } from '@logux/core/is-first-older' | ||
import { WsConnection } from '@logux/core/ws-connection' | ||
import { MemoryStore } from '@logux/core/memory-store' | ||
import { ClientNode } from '@logux/core/client-node' | ||
import { Reconnect } from '@logux/core/reconnect' | ||
import { parseId } from '@logux/core/parse-id' | ||
import { | ||
isFirstOlder, | ||
WsConnection, | ||
MemoryStore, | ||
ClientNode, | ||
Reconnect, | ||
parseId, | ||
Log | ||
} from '@logux/core' | ||
import { nanoid } from 'nanoid' | ||
import { Log } from '@logux/core/log' | ||
import { LoguxUndoError } from '../logux-undo-error/index.js' | ||
import { track } from '../track/index.js' | ||
let ALLOWED_META = ['id', 'time', 'subprotocol'] | ||
function tabPing (c) { | ||
function tabPing(c) { | ||
localStorage.setItem(c.options.prefix + ':tab:' + c.tabId, Date.now()) | ||
} | ||
function cleanTabActions (client, id) { | ||
function cleanTabActions(client, id) { | ||
client.log.removeReason('tab' + id).then(() => { | ||
@@ -25,4 +30,4 @@ if (client.isLocalStorage) { | ||
class Client { | ||
constructor (opts = {}) { | ||
export class Client { | ||
constructor(opts = {}) { | ||
this.options = opts | ||
@@ -110,27 +115,38 @@ | ||
unsubscribing[meta.id] = action | ||
} else if (type === 'logux/processed' && unsubscribing[action.id]) { | ||
let unsubscription = unsubscribing[action.id] | ||
json = JSON.stringify({ ...unsubscription, type: 'logux/subscribe' }) | ||
let subscribers = this.subscriptions[json] | ||
if (subscribers) { | ||
if (subscribers === 1) { | ||
delete this.subscriptions[json] | ||
} else if (type === 'logux/processed') { | ||
if (unsubscribing[action.id]) { | ||
let unsubscription = unsubscribing[action.id] | ||
json = JSON.stringify({ ...unsubscription, type: 'logux/subscribe' }) | ||
let subscribers = this.subscriptions[json] | ||
if (subscribers) { | ||
if (subscribers === 1) { | ||
delete this.subscriptions[json] | ||
} else { | ||
this.subscriptions[json] = subscribers - 1 | ||
} | ||
} | ||
} | ||
if (subscribing[action.id]) { | ||
let subscription = subscribing[action.id] | ||
delete subscribing[action.id] | ||
json = JSON.stringify(subscription) | ||
if (this.subscriptions[json]) { | ||
this.subscriptions[json] += 1 | ||
} else { | ||
this.subscriptions[json] = subscribers - 1 | ||
this.subscriptions[json] = 1 | ||
} | ||
last = this.last[subscription.channel] | ||
if (!last || isFirstOlder(last, meta)) { | ||
this.last[subscription.channel] = { id: meta.id, time: meta.time } | ||
} | ||
} | ||
} else if (type === 'logux/processed' && subscribing[action.id]) { | ||
let subscription = subscribing[action.id] | ||
delete subscribing[action.id] | ||
json = JSON.stringify(subscription) | ||
if (this.subscriptions[json]) { | ||
this.subscriptions[json] += 1 | ||
} else { | ||
this.subscriptions[json] = 1 | ||
if (type === 'logux/processed' && this.processing[action.id]) { | ||
this.processing[action.id][1](meta) | ||
delete this.processing[action.id] | ||
} | ||
last = this.last[subscription.channel] | ||
if (!last || isFirstOlder(last, meta)) { | ||
this.last[subscription.channel] = { id: meta.id, time: meta.time } | ||
} else if (type === 'logux/undo') { | ||
if (this.processing[action.id]) { | ||
this.processing[action.id][2](new LoguxUndoError(action)) | ||
delete this.processing[action.id] | ||
} | ||
} else if (type === 'logux/undo') { | ||
delete subscribing[action.id] | ||
@@ -202,2 +218,11 @@ delete unsubscribing[action.id] | ||
if (!this.options.time) { | ||
if (typeof this.options.timeout === 'undefined') { | ||
this.options.timeout = 20000 | ||
} | ||
if (typeof this.options.ping === 'undefined') { | ||
this.options.ping = 5000 | ||
} | ||
} | ||
this.node = new ClientNode(this.nodeId, this.log, connection, { | ||
@@ -207,2 +232,3 @@ subprotocol: this.options.subprotocol, | ||
timeout: this.options.timeout, | ||
fixTime: !this.options.time, | ||
outMap, | ||
@@ -256,5 +282,15 @@ token: this.options.token, | ||
} | ||
this.processing = {} | ||
} | ||
start () { | ||
get state() { | ||
return this.node.state | ||
} | ||
get connected() { | ||
return this.state !== 'disconnected' && this.state !== 'connecting' | ||
} | ||
start() { | ||
this.cleanPrevActions() | ||
@@ -264,4 +300,20 @@ this.node.connection.connect() | ||
on (event, listener) { | ||
if (event === 'user') { | ||
sync(action, meta = {}) { | ||
meta.sync = true | ||
if (typeof meta.id === 'undefined') { | ||
meta.id = this.log.generateId() | ||
} | ||
this.log.add(action, meta) | ||
return track(this, meta.id) | ||
} | ||
type(type, listener, opts) { | ||
return this.log.type(type, listener, opts) | ||
} | ||
on(event, listener) { | ||
if (event === 'state') { | ||
return this.node.emitter.on(event, listener) | ||
} else if (event === 'user') { | ||
return this.emitter.on(event, listener) | ||
@@ -273,3 +325,3 @@ } else { | ||
changeUser (userId, token) { | ||
changeUser(userId, token) { | ||
if (process.env.NODE_ENV !== 'production') { | ||
@@ -300,3 +352,17 @@ if (typeof userId !== 'string') { | ||
destroy () { | ||
waitFor(state) { | ||
if (this.state === state) { | ||
return Promise.resolve() | ||
} | ||
return new Promise(resolve => { | ||
let unbind = this.on('state', () => { | ||
if (this.state === state) { | ||
unbind() | ||
resolve() | ||
} | ||
}) | ||
}) | ||
} | ||
destroy() { | ||
this.onUnload() | ||
@@ -310,3 +376,3 @@ this.node.destroy() | ||
clean () { | ||
clean() { | ||
this.destroy() | ||
@@ -316,3 +382,3 @@ return this.log.store.clean ? this.log.store.clean() : Promise.resolve() | ||
cleanPrevActions () { | ||
cleanPrevActions() { | ||
if (!this.isLocalStorage) return | ||
@@ -331,11 +397,9 @@ | ||
onUnload () { | ||
onUnload() { | ||
if (this.pinging) cleanTabActions(this, this.tabId) | ||
} | ||
getClientId () { | ||
getClientId() { | ||
return nanoid(8) | ||
} | ||
} | ||
export { Client } |
@@ -14,2 +14,2 @@ import { Client } from '../client/index.js' | ||
*/ | ||
export function confirm (client: Client): () => void | ||
export function confirm(client: Client): () => void |
@@ -1,2 +0,2 @@ | ||
function block (e) { | ||
function block(e) { | ||
e.returnValue = 'unsynced' | ||
@@ -6,3 +6,3 @@ return 'unsynced' | ||
function confirm (client) { | ||
export function confirm(client) { | ||
let disconnected = client.state === 'disconnected' | ||
@@ -51,3 +51,1 @@ let wait = false | ||
} | ||
export { confirm } |
@@ -0,3 +1,3 @@ | ||
import { Action, Log } from '@logux/core' | ||
import { Unsubscribe } from 'nanoevents' | ||
import { Log } from '@logux/core' | ||
@@ -28,5 +28,5 @@ import { Client, ClientActionListener, ClientMeta } from '../client/index.js' | ||
export class CrossTabClient< | ||
H extends object = {}, | ||
L extends Log = Log<ClientMeta> | ||
> extends Client<H, L> { | ||
Headers extends object = {}, | ||
ClientLog extends Log = Log<ClientMeta> | ||
> extends Client<Headers, ClientLog> { | ||
/** | ||
@@ -45,21 +45,8 @@ * Current tab role. Only `leader` tab connects to server. `followers` just | ||
/** | ||
* Leader tab synchronization state. It can differs | ||
* from `client.node.state` (because only the leader tab keeps connection). | ||
* | ||
* ```js | ||
* client.on('state', () => { | ||
* if (client.state === 'disconnected' && client.state === 'sending') { | ||
* showCloseWarning() | ||
* } | ||
* }) | ||
* ``` | ||
* Cache for localStorage detection. Can be overriden to disable leader tab | ||
* election in tests. | ||
*/ | ||
state: 'disconnected' | 'connecting' | 'sending' | 'synchronized' | ||
isLocalStorage: boolean | ||
/** | ||
* Is leader tab connected to server. | ||
*/ | ||
connected: boolean | ||
/** | ||
* Subscribe for synchronization events. It implements nanoevents API. | ||
@@ -85,22 +72,8 @@ * Supported events: | ||
*/ | ||
on (event: 'role' | 'state', listener: () => void): Unsubscribe | ||
on (event: 'user', listener: (userId: string) => void): Unsubscribe | ||
on ( | ||
on(event: 'role' | 'state', listener: () => void): Unsubscribe | ||
on(event: 'user', listener: (userId: string) => void): Unsubscribe | ||
on( | ||
event: 'preadd' | 'add' | 'clean', | ||
listener: ClientActionListener | ||
listener: ClientActionListener<Action> | ||
): Unsubscribe | ||
/** | ||
* Wait for specific state of the leader tab. | ||
* | ||
* ```js | ||
* await client.waitFor('synchronized') | ||
* hideLoader() | ||
* ``` | ||
* | ||
* @param state State name | ||
*/ | ||
waitFor ( | ||
state: 'disconnected' | 'connecting' | 'sending' | 'synchronized' | ||
): Promise<void> | ||
} |
@@ -1,10 +0,10 @@ | ||
import { LoguxError } from '@logux/core/logux-error' | ||
import { LoguxError, actionEvents } from '@logux/core' | ||
import { Client } from '../client/index.js' | ||
function storageKey (client, name) { | ||
function storageKey(client, name) { | ||
return client.options.prefix + ':' + client.options.userId + ':' + name | ||
} | ||
function sendToTabs (client, event, data) { | ||
function sendToTabs(client, event, data) { | ||
if (!client.isLocalStorage) return | ||
@@ -24,3 +24,3 @@ let key = storageKey(client, event) | ||
function getLeader (client) { | ||
function getLeader(client) { | ||
let data = localStorage.getItem(storageKey(client, 'leader')) | ||
@@ -32,7 +32,7 @@ let json = [] | ||
function leaderPing (client) { | ||
function leaderPing(client) { | ||
sendToTabs(client, 'leader', [client.tabId, Date.now()]) | ||
} | ||
function onDeadLeader (client) { | ||
function onDeadLeader(client) { | ||
if (client.state !== 'disconnected') { | ||
@@ -44,3 +44,3 @@ setState(client, 'disconnected') | ||
function watchForLeader (client) { | ||
function watchForLeader(client) { | ||
clearTimeout(client.watching) | ||
@@ -56,26 +56,18 @@ client.watching = setTimeout(() => { | ||
function areWeOutdates (client, meta) { | ||
if (!meta.subprotocol) return false | ||
if (client.options.subprotocol === meta.subprotocol) return false | ||
let id = meta.id.split(' ')[1] | ||
let prefix = client.clientId + ':' | ||
if (id.slice(0, prefix.length) !== prefix) return false | ||
let ourParts = client.options.subprotocol.split('.') | ||
let remoteParts = meta.subprotocol.split('.') | ||
// eslint-disable-next-line | ||
for (let i = 0; i < ourParts.length; i++) { | ||
let ourNumber = parseInt(ourParts[i]) | ||
let remoteNumber = parseInt(remoteParts[i]) | ||
if (ourNumber > remoteNumber) { | ||
return false | ||
} else if (ourNumber < remoteNumber) { | ||
return true | ||
function compareSubprotocols(left, right) { | ||
let leftParts = left.split('.') | ||
let rightParts = right.split('.') | ||
for (let i = 0; i < 3; i++) { | ||
let leftNumber = parseInt(leftParts[i] || 0) | ||
let rightNumber = parseInt(rightParts[i] || 0) | ||
if (leftNumber > rightNumber) { | ||
return 1 | ||
} else if (leftNumber < rightNumber) { | ||
return -1 | ||
} | ||
} | ||
return false | ||
return 0 | ||
} | ||
function setRole (client, role) { | ||
function setRole(client, role) { | ||
if (client.role !== role) { | ||
@@ -115,3 +107,3 @@ let node = client.node | ||
function isActiveLeader (client) { | ||
function isActiveLeader(client) { | ||
let leader = getLeader(client) | ||
@@ -121,3 +113,3 @@ return leader[1] && leader[1] >= Date.now() - client.leaderTimeout | ||
function startElection (client) { | ||
function startElection(client) { | ||
leaderPing(client) | ||
@@ -136,3 +128,3 @@ setRole(client, 'candidate') | ||
function setState (client, state) { | ||
function setState(client, state) { | ||
client.state = state | ||
@@ -143,8 +135,8 @@ client.emitter.emit('state') | ||
function isMemory (store) { | ||
function isMemory(store) { | ||
return Array.isArray(store.entries) && Array.isArray(store.added) | ||
} | ||
class CrossTabClient extends Client { | ||
constructor (opts = {}) { | ||
export class CrossTabClient extends Client { | ||
constructor(opts = {}) { | ||
super(opts) | ||
@@ -159,3 +151,3 @@ | ||
this.state = this.node.state | ||
this.leaderState = this.node.state | ||
@@ -169,3 +161,3 @@ this.node.on('state', () => { | ||
this.log.on('add', (action, meta) => { | ||
this.emitter.emit('add', action, meta) | ||
actionEvents(this.emitter, 'add', action, meta) | ||
if (meta.tab !== this.tabId) { | ||
@@ -176,3 +168,3 @@ sendToTabs(this, 'add', [this.tabId, action, meta]) | ||
this.log.on('clean', (action, meta) => { | ||
this.emitter.emit('clean', action, meta) | ||
actionEvents(this.emitter, 'clean', action, meta) | ||
}) | ||
@@ -184,5 +176,20 @@ | ||
} | ||
if (this.isLocalStorage) { | ||
let subprotocolKey = storageKey(this, 'subprotocol') | ||
if (localStorage.getItem(subprotocolKey) !== this.options.subprotocol) { | ||
sendToTabs(this, 'subprotocol', this.options.subprotocol) | ||
} | ||
} | ||
} | ||
start () { | ||
get state() { | ||
return this.leaderState | ||
} | ||
set state(value) { | ||
this.leaderState = value | ||
} | ||
start() { | ||
this.cleanPrevActions() | ||
@@ -205,3 +212,3 @@ | ||
destroy () { | ||
destroy() { | ||
super.destroy() | ||
@@ -217,3 +224,3 @@ | ||
clean () { | ||
clean() { | ||
if (this.isLocalStorage) { | ||
@@ -228,3 +235,3 @@ localStorage.removeItem(storageKey(this, 'add')) | ||
changeUser (userId, token) { | ||
changeUser(userId, token) { | ||
sendToTabs(this, 'user', [this.tabId, userId]) | ||
@@ -234,3 +241,13 @@ super.changeUser(userId, token) | ||
on (event, listener) { | ||
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) | ||
} | ||
} | ||
on(event, listener) { | ||
if (event === 'preadd') { | ||
@@ -243,17 +260,3 @@ return this.log.emitter.on(event, listener) | ||
waitFor (state) { | ||
if (this.state === state) { | ||
return Promise.resolve() | ||
} | ||
return new Promise(resolve => { | ||
let unbind = this.on('state', () => { | ||
if (this.state === state) { | ||
unbind() | ||
resolve() | ||
} | ||
}) | ||
}) | ||
} | ||
onStorage (e) { | ||
onStorage(e) { | ||
if (e.newValue === null) return | ||
@@ -267,13 +270,2 @@ | ||
let meta = data[2] | ||
if (areWeOutdates(this, meta)) { | ||
let err = new LoguxError( | ||
'wrong-subprotocol', | ||
{ | ||
supported: meta.subprotocol, | ||
used: this.node.options.subprotocol | ||
}, | ||
true | ||
) | ||
this.node.emitter.emit('error', err) | ||
} | ||
if (!meta.tab || meta.tab === this.tabId) { | ||
@@ -283,3 +275,3 @@ if (isMemory(this.log.store)) { | ||
} | ||
this.emitter.emit('add', action, meta) | ||
actionEvents(this.emitter, 'add', action, meta) | ||
if (this.role === 'leader') { | ||
@@ -300,4 +292,4 @@ this.node.onAdd(action, meta) | ||
let state = JSON.parse(localStorage.getItem(e.key)) | ||
if (this.state !== state) { | ||
this.state = state | ||
if (this.leaderState !== state) { | ||
this.leaderState = state | ||
this.emitter.emit('state') | ||
@@ -310,6 +302,19 @@ } | ||
} | ||
} else if (e.key === storageKey(this, 'subprotocol')) { | ||
let other = JSON.parse(e.newValue) | ||
let compare = compareSubprotocols(this.options.subprotocol, other) | ||
if (compare === 1) { | ||
sendToTabs(this, 'subprotocol', this.options.subprotocol) | ||
} else if (compare === -1) { | ||
let err = new LoguxError( | ||
'wrong-subprotocol', | ||
{ supported: other, used: this.options.subprotocol }, | ||
true | ||
) | ||
this.node.emitter.emit('error', err) | ||
} | ||
} | ||
} | ||
onUnload () { | ||
onUnload() { | ||
if (this.role === 'leader') { | ||
@@ -322,3 +327,3 @@ this.unloading = true | ||
getClientId () { | ||
getClientId() { | ||
let key = storageKey(this, 'client') | ||
@@ -335,8 +340,2 @@ if (!this.isLocalStorage) { | ||
} | ||
get connected () { | ||
return this.state !== 'disconnected' && this.state !== 'connecting' | ||
} | ||
} | ||
export { CrossTabClient } |
import { Client } from '../client/index.js' | ||
type FaviconLinks = { | ||
interface FaviconLinks { | ||
/** | ||
@@ -36,2 +36,2 @@ * Default favicon link. By default, it will be taken from current favicon. | ||
*/ | ||
export function favicon (client: Client, links: FaviconLinks): () => void | ||
export function favicon(client: Client, links: FaviconLinks): () => void |
@@ -1,2 +0,2 @@ | ||
function favicon (client, links) { | ||
export function favicon(client, links) { | ||
let normal = links.normal | ||
@@ -11,3 +11,3 @@ let offline = links.offline | ||
function update () { | ||
function update() { | ||
if (client.connected && prevFav !== normal) { | ||
@@ -25,3 +25,3 @@ fav.href = prevFav = normal | ||
function setError () { | ||
function setError() { | ||
if (error && prevFav !== error) { | ||
@@ -66,3 +66,1 @@ fav.href = prevFav = error | ||
} | ||
export { favicon } |
@@ -8,5 +8,16 @@ export { | ||
} from './badge/index.js' | ||
export { | ||
LoguxUndoError, | ||
ChannelNotFoundError, | ||
ChannelDeniedError, | ||
ChannelServerError, | ||
ChannelError | ||
} from './logux-undo-error/index.js' | ||
export { Client, ClientMeta, ClientOptions } from './client/index.js' | ||
export { request, RequestOptions } from './request/index.js' | ||
export { encryptActions } from './encrypt-actions/index.js' | ||
export { CrossTabClient } from './cross-tab-client/index.js' | ||
export { IndexedStore } from './indexed-store/index.js' | ||
export { TestServer } from './test-server/index.js' | ||
export { TestClient } from './test-client/index.js' | ||
export { attention } from './attention/index.js' | ||
@@ -16,2 +27,3 @@ export { confirm } from './confirm/index.js' | ||
export { status } from './status/index.js' | ||
export { track } from './track/index.js' | ||
export { log } from './log/index.js' |
38
index.js
@@ -1,23 +0,15 @@ | ||
import { badge, badgeRu, badgeEn } from './badge/index.js' | ||
import { CrossTabClient } from './cross-tab-client/index.js' | ||
import { IndexedStore } from './indexed-store/index.js' | ||
import { attention } from './attention/index.js' | ||
import { confirm } from './confirm/index.js' | ||
import { favicon } from './favicon/index.js' | ||
import { Client } from './client/index.js' | ||
import { status } from './status/index.js' | ||
import { log } from './log/index.js' | ||
export { | ||
CrossTabClient, | ||
IndexedStore, | ||
attention, | ||
confirm, | ||
badgeRu, | ||
badgeEn, | ||
favicon, | ||
Client, | ||
status, | ||
badge, | ||
log | ||
} | ||
export { badge, badgeRu, badgeEn } from './badge/index.js' | ||
export { LoguxUndoError } from './logux-undo-error/index.js' | ||
export { CrossTabClient } from './cross-tab-client/index.js' | ||
export { encryptActions } from './encrypt-actions/index.js' | ||
export { IndexedStore } from './indexed-store/index.js' | ||
export { TestServer } from './test-server/index.js' | ||
export { TestClient } from './test-client/index.js' | ||
export { attention } from './attention/index.js' | ||
export { confirm } from './confirm/index.js' | ||
export { favicon } from './favicon/index.js' | ||
export { request } from './request/index.js' | ||
export { Client } from './client/index.js' | ||
export { status } from './status/index.js' | ||
export { track } from './track/index.js' | ||
export { log } from './log/index.js' |
@@ -26,3 +26,3 @@ import { LogStore } from '@logux/core' | ||
*/ | ||
constructor (name?: string) | ||
constructor(name?: string) | ||
@@ -29,0 +29,0 @@ /** |
import { isFirstOlder } from '@logux/core' | ||
const VERSION = 1 | ||
const VERSION = 2 | ||
function rejectify (request, reject) { | ||
function rejectify(request, reject) { | ||
request.onerror = e => { | ||
@@ -11,3 +11,3 @@ reject(e.target.error) | ||
function promisify (request) { | ||
function promisify(request) { | ||
return new Promise((resolve, reject) => { | ||
@@ -21,25 +21,8 @@ rejectify(request, reject) | ||
function nextEntry (request) { | ||
return cursor => { | ||
if (cursor) { | ||
cursor.value.meta.added = cursor.value.added | ||
return { | ||
entries: [[cursor.value.action, cursor.value.meta]], | ||
next () { | ||
cursor.continue() | ||
return promisify(request).then(nextEntry(request)) | ||
} | ||
} | ||
} else { | ||
return { entries: [] } | ||
} | ||
} | ||
} | ||
function isDefined (value) { | ||
function isDefined(value) { | ||
return typeof value !== 'undefined' | ||
} | ||
class IndexedStore { | ||
constructor (name = 'logux') { | ||
export class IndexedStore { | ||
constructor(name = 'logux') { | ||
this.name = name | ||
@@ -49,3 +32,3 @@ this.adding = {} | ||
init () { | ||
init() { | ||
if (this.initing) return this.initing | ||
@@ -59,11 +42,20 @@ | ||
let log = db.createObjectStore('log', { | ||
keyPath: 'added', | ||
autoIncrement: true | ||
}) | ||
log.createIndex('id', 'id', { unique: true }) | ||
log.createIndex('created', 'created', { unique: true }) | ||
log.createIndex('reasons', 'reasons', { multiEntry: true }) | ||
db.createObjectStore('extra', { keyPath: 'key' }) | ||
let log | ||
if (e.oldVersion < 1) { | ||
log = db.createObjectStore('log', { | ||
keyPath: 'added', | ||
autoIncrement: true | ||
}) | ||
log.createIndex('id', 'id', { unique: true }) | ||
log.createIndex('created', 'created', { unique: true }) | ||
log.createIndex('reasons', 'reasons', { multiEntry: true }) | ||
db.createObjectStore('extra', { keyPath: 'key' }) | ||
} | ||
if (e.oldVersion < 2) { | ||
if (!log) { | ||
/* istanbul ignore next */ | ||
log = opening.transaction.objectStore('log') | ||
} | ||
log.createIndex('indexes', 'indexes', { multiEntry: true }) | ||
} | ||
} | ||
@@ -85,15 +77,38 @@ | ||
async get (opts) { | ||
let request | ||
async get({ index, order }) { | ||
let store = await this.init() | ||
let log = store.os('log') | ||
if (opts.order === 'created') { | ||
request = log.index('created').openCursor(null, 'prev') | ||
} else { | ||
request = log.openCursor(null, 'prev') | ||
} | ||
return promisify(request).then(nextEntry(request)) | ||
return new Promise((resolve, reject) => { | ||
let log = store.os('log') | ||
let request | ||
if (index) { | ||
if (order === 'created') { | ||
request = log.index('created').openCursor(null, 'prev') | ||
} else { | ||
let keyRange = IDBKeyRange.only(index) | ||
request = log.index('indexes').openCursor(keyRange, 'prev') | ||
} | ||
} else if (order === 'created') { | ||
request = log.index('created').openCursor(null, 'prev') | ||
} else { | ||
request = log.openCursor(null, 'prev') | ||
} | ||
rejectify(request, reject) | ||
let entries = [] | ||
request.onsuccess = function (e) { | ||
let cursor = e.target.result | ||
if (!cursor) { | ||
resolve({ entries }) | ||
return | ||
} | ||
if (!index || cursor.value.indexes.includes(index)) { | ||
cursor.value.meta.added = cursor.value.added | ||
entries.unshift([cursor.value.action, cursor.value.meta]) | ||
} | ||
cursor.continue() | ||
} | ||
}) | ||
} | ||
async byId (id) { | ||
async byId(id) { | ||
let store = await this.init() | ||
@@ -108,10 +123,9 @@ let result = await promisify(store.os('log').index('id').get(id)) | ||
async remove (id) { | ||
async remove(id) { | ||
let store = await this.init() | ||
let log = store.os('log', 'write') | ||
let entry = await promisify(log.index('id').get(id)) | ||
let entry = await promisify(store.os('log').index('id').get(id)) | ||
if (!entry) { | ||
return false | ||
} else { | ||
await promisify(log.delete(entry.added)) | ||
await promisify(store.os('log', 'write').delete(entry.added)) | ||
entry.meta.added = entry.added | ||
@@ -122,3 +136,3 @@ return [entry.action, entry.meta] | ||
async add (action, meta) { | ||
async add(action, meta) { | ||
let id = meta.id.split(' ') | ||
@@ -131,2 +145,3 @@ let entry = { | ||
reasons: meta.reasons, | ||
indexes: meta.indexes || [], | ||
created: [meta.time, id[1], id[2], id[0]].join(' ') | ||
@@ -141,8 +156,7 @@ } | ||
let store = await this.init() | ||
let log = store.os('log', 'write') | ||
let exist = await promisify(log.index('id').get(meta.id)) | ||
let exist = await promisify(store.os('log').index('id').get(meta.id)) | ||
if (exist) { | ||
return false | ||
} else { | ||
let added = await promisify(log.add(entry)) | ||
let added = await promisify(store.os('log', 'write').add(entry)) | ||
delete store.adding[entry.created] | ||
@@ -154,6 +168,5 @@ meta.added = added | ||
async changeMeta (id, diff) { | ||
async changeMeta(id, diff) { | ||
let store = await this.init() | ||
let log = store.os('log', 'write') | ||
let entry = await promisify(log.index('id').get(id)) | ||
let entry = await promisify(store.os('log').index('id').get(id)) | ||
if (!entry) { | ||
@@ -164,3 +177,3 @@ return false | ||
if (diff.reasons) entry.reasons = diff.reasons | ||
await promisify(log.put(entry)) | ||
await promisify(store.os('log', 'write').put(entry)) | ||
return true | ||
@@ -170,7 +183,6 @@ } | ||
async removeReason (reason, criteria, callback) { | ||
async removeReason(reason, criteria, callback) { | ||
let store = await this.init() | ||
let log = store.os('log', 'write') | ||
if (criteria.id) { | ||
let entry = await promisify(log.index('id').get(criteria.id)) | ||
let entry = await promisify(store.os('log').index('id').get(criteria.id)) | ||
if (entry) { | ||
@@ -183,5 +195,5 @@ let index = entry.meta.reasons.indexOf(reason) | ||
callback(entry.action, entry.meta) | ||
await promisify(log.delete(entry.added)) | ||
await promisify(store.os('log', 'write').delete(entry.added)) | ||
} else { | ||
await promisify(log.put(entry)) | ||
await promisify(store.os('log', 'write').put(entry)) | ||
} | ||
@@ -191,4 +203,5 @@ } | ||
} else { | ||
let request = log.index('reasons').openCursor(reason) | ||
await new Promise((resolve, reject) => { | ||
let log = store.os('log', 'write') | ||
let request = log.index('reasons').openCursor(reason) | ||
rejectify(request, reject) | ||
@@ -243,3 +256,3 @@ request.onsuccess = function (e) { | ||
async getLastAdded () { | ||
async getLastAdded() { | ||
let store = await this.init() | ||
@@ -250,3 +263,3 @@ let cursor = await promisify(store.os('log').openCursor(null, 'prev')) | ||
async getLastSynced () { | ||
async getLastSynced() { | ||
let store = await this.init() | ||
@@ -261,6 +274,5 @@ let data = await promisify(store.os('extra').get('lastSynced')) | ||
async setLastSynced (values) { | ||
async setLastSynced(values) { | ||
let store = await this.init() | ||
let extra = store.os('extra', 'write') | ||
let data = await promisify(extra.get('lastSynced')) | ||
let data = await promisify(store.os('extra').get('lastSynced')) | ||
if (!data) data = { key: 'lastSynced', sent: 0, received: 0 } | ||
@@ -273,6 +285,6 @@ if (typeof values.sent !== 'undefined') { | ||
} | ||
await promisify(extra.put(data)) | ||
await promisify(store.os('extra', 'write').put(data)) | ||
} | ||
os (name, write) { | ||
os(name, write) { | ||
let mode = write ? 'readwrite' : 'readonly' | ||
@@ -282,3 +294,3 @@ return this.db.transaction(name, mode).objectStore(name) | ||
async clean () { | ||
async clean() { | ||
let store = await this.init() | ||
@@ -289,3 +301,1 @@ store.db.close() | ||
} | ||
export { IndexedStore } |
import { Client } from '../client/index.js' | ||
type LogMessages = { | ||
interface LogMessages { | ||
/** | ||
@@ -52,2 +52,2 @@ * Disable action messages with specific types. | ||
*/ | ||
export function log (client: Client, messages?: LogMessages): () => void | ||
export function log(client: Client, messages?: LogMessages): () => void |
@@ -1,8 +0,8 @@ | ||
import { parseId } from '@logux/core/parse-id' | ||
import { parseId } from '@logux/core' | ||
function bold (string) { | ||
function bold(string) { | ||
return '%c' + string + '%c' | ||
} | ||
function showLog (text, details) { | ||
function showLog(text, details) { | ||
text = '%cLogux%c ' + text | ||
@@ -34,3 +34,3 @@ let args = Array.from(text.match(/%c/g)).map((_, i) => { | ||
function log (client, messages = {}) { | ||
export function log(client, messages = {}) { | ||
let node = client.node | ||
@@ -92,2 +92,6 @@ | ||
} | ||
} else if (action.type === 'logux/subscribed') { | ||
showLog( | ||
'subscribed to ' + bold(action.channel) + ' channel by server' | ||
) | ||
} else if (action.type === 'logux/unsubscribe') { | ||
@@ -122,15 +126,17 @@ message = 'unsubscribed from channel ' + bold(action.channel) | ||
} else if (action.type === 'logux/undo') { | ||
message = | ||
'action ' + | ||
bold(action.id) + | ||
' was undid because of ' + | ||
bold(action.reason) | ||
let details = {} | ||
if (action.action.type === 'logux/subscribe') { | ||
message = 'subscription to ' + bold(action.action.channel) | ||
} else { | ||
message = 'action ' + bold(action.action.type) | ||
} | ||
message += ' was undone because of ' + bold(action.reason) | ||
let details = { | ||
'Reverted Action': action.action | ||
} | ||
if (Object.keys(action).length > 4) { | ||
details['Undo Action'] = action | ||
} | ||
if (sent[action.id]) { | ||
details.Action = sent[action.id] | ||
delete sent[action.id] | ||
} | ||
if (Object.keys(action).length > 3) { | ||
details.Undo = action | ||
} | ||
showLog(message, details) | ||
@@ -184,3 +190,1 @@ } else { | ||
} | ||
export { log } |
{ | ||
"name": "@logux/client", | ||
"version": "0.9.3", | ||
"version": "0.10.0", | ||
"description": "Logux base components to build web client", | ||
@@ -17,71 +17,21 @@ "keywords": [ | ||
"sideEffects": false, | ||
"type": "module", | ||
"types": "./index.d.ts", | ||
"exports": { | ||
".": "./index.js", | ||
"./package.json": "./package.json" | ||
}, | ||
"engines": { | ||
"node": ">=10.0.0" | ||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0" | ||
}, | ||
"peerDependencies": { | ||
"@logux/core": "^0.7.0" | ||
}, | ||
"dependencies": { | ||
"@logux/core": "^0.6.2", | ||
"nanoevents": "^5.1.8", | ||
"nanoid": "^3.1.12" | ||
}, | ||
"type": "module", | ||
"main": "index.cjs", | ||
"module": "index.js", | ||
"react-native": "index.js", | ||
"exports": { | ||
".": { | ||
"require": "./index.cjs", | ||
"import": "./index.js" | ||
}, | ||
"./package.json": "./package.json", | ||
"./attention/package.json": "./attention/package.json", | ||
"./attention": { | ||
"require": "./attention/index.cjs", | ||
"import": "./attention/index.js" | ||
}, | ||
"./badge/package.json": "./badge/package.json", | ||
"./badge": { | ||
"require": "./badge/index.cjs", | ||
"import": "./badge/index.js" | ||
}, | ||
"./client/package.json": "./client/package.json", | ||
"./client": { | ||
"require": "./client/index.cjs", | ||
"import": "./client/index.js" | ||
}, | ||
"./cross-tab-client/package.json": "./cross-tab-client/package.json", | ||
"./cross-tab-client": { | ||
"require": "./cross-tab-client/index.cjs", | ||
"import": "./cross-tab-client/index.js" | ||
}, | ||
"./favicon/package.json": "./favicon/package.json", | ||
"./favicon": { | ||
"require": "./favicon/index.cjs", | ||
"import": "./favicon/index.js" | ||
}, | ||
"./confirm/package.json": "./confirm/package.json", | ||
"./confirm": { | ||
"require": "./confirm/index.cjs", | ||
"import": "./confirm/index.js" | ||
}, | ||
"./indexed-store/package.json": "./indexed-store/package.json", | ||
"./indexed-store": { | ||
"require": "./indexed-store/index.cjs", | ||
"import": "./indexed-store/index.js" | ||
}, | ||
"./log/package.json": "./log/package.json", | ||
"./log": { | ||
"require": "./log/index.cjs", | ||
"import": "./log/index.js" | ||
}, | ||
"./status/package.json": "./status/package.json", | ||
"./status": { | ||
"require": "./status/index.cjs", | ||
"import": "./status/index.js" | ||
}, | ||
"./badge/styles/package.json": "./badge/styles/package.json", | ||
"./badge/styles": { | ||
"require": "./badge/styles/index.cjs", | ||
"import": "./badge/styles/index.js" | ||
} | ||
"@logux/actions": "^0.1.0", | ||
"fast-json-stable-stringify": "^2.1.0", | ||
"nanodelay": "^2.0.0", | ||
"nanoevents": "^6.0.0", | ||
"nanoid": "^3.1.22" | ||
} | ||
} | ||
} |
@@ -13,3 +13,3 @@ # Logux Client [![Cult Of Martians][cult-img]][cult] | ||
* **[Issues](https://github.com/logux/logux/issues)** | ||
and **[roadmap](https://github.com/logux/logux/projects/1)** | ||
and **[roadmap](https://github.com/orgs/logux/projects/1)** | ||
* **[Projects](https://logux.io/guide/architecture/parts/)** | ||
@@ -43,3 +43,3 @@ inside Logux ecosystem | ||
```sh | ||
npm install @logux/client | ||
npm install @logux/core @logux/client | ||
``` | ||
@@ -46,0 +46,0 @@ |
@@ -28,3 +28,3 @@ import { Action } from '@logux/core' | ||
type StatusOptions = { | ||
interface StatusOptions { | ||
/** | ||
@@ -51,3 +51,3 @@ * Synchronized state duration. Default is `3000`. | ||
*/ | ||
export function status ( | ||
export function status( | ||
client: Client, | ||
@@ -54,0 +54,0 @@ callback: StatusListener, |
@@ -1,2 +0,2 @@ | ||
function status (client, callback, options = {}) { | ||
export function status(client, callback, options = {}) { | ||
let observable = client.on ? client : client.node | ||
@@ -13,3 +13,3 @@ let disconnected = observable.state === 'disconnected' | ||
function setSynchronized () { | ||
function setSynchronized() { | ||
if (Object.keys(processing).length === 0) { | ||
@@ -28,3 +28,3 @@ if (wait) { | ||
function changeState () { | ||
function changeState() { | ||
clearTimeout(timeout) | ||
@@ -107,3 +107,1 @@ | ||
} | ||
export { status } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
4
85023
6
41
2808
+ Added@logux/actions@^0.1.0
+ Addednanodelay@^2.0.0
+ Added@logux/actions@0.1.0(transitive)
+ Added@logux/core@0.7.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addednanodelay@2.0.2(transitive)
+ Addednanoevents@6.0.2(transitive)
- Removed@logux/core@^0.6.2
- Removed@logux/core@0.6.2(transitive)
- Removednanoevents@5.1.13(transitive)
Updatednanoevents@^6.0.0
Updatednanoid@^3.1.22