fox-wamp
Version:
Web Application Message Router/Server WAMP/MQTT
195 lines (177 loc) • 6.25 kB
text/typescript
import * as sqlite from 'sqlite'
import { match, restoreUri, defaultParse } from '../topic_pattern'
import { KeyValueStorageAbstract, isDataEmpty, deepDataMerge, unSerializeData, makeDataSerializable } from '../realm'
import { KPQueue } from '../masterfree/kpqueue'
import { DbFactory } from './dbfactory'
import { ActorPush } from '../realm'
import { ProduceId } from '../masterfree/makeid'
export async function createKvTables (db: sqlite.Database, realmName: string) {
await db.run(
`CREATE TABLE IF NOT EXISTS update_history_${realmName} (
msg_id TEXT not null,
msg_origin TEXT not null,
msg_uri TEXT not null,
msg_oldv TEXT,
PRIMARY KEY (msg_id));`
)
await db.run(
`CREATE TABLE IF NOT EXISTS kv_${realmName} (
key TEXT not null,
value TEXT not null,
will_sid TEXT not null,
opt TEXT not null,
stamp TEXT,
PRIMARY KEY (key));`
)
await db.run(
`CREATE INDEX IF NOT EXISTS kv_sid_${realmName} on kv_${realmName} (will_sid);`
)
await db.run(
`CREATE TABLE IF NOT EXISTS set_value_${realmName} (
key TEXT not null,
msg_id TEXT not null,
will_sid TEXT not null,
set_when TEXT not null,
PRIMARY KEY (key, msg_id));`
)
await db.run(
`CREATE INDEX IF NOT EXISTS set_value_sid_${realmName} on set_value_${realmName} (will_sid);`
)
}
export async function saveUpdateHistory (db: sqlite.Database, realmName: string, id: string, origin: string, suri: string, oldv: any) {
return db.run(
`INSERT INTO update_history_${realmName} VALUES (?,?,?,?);`,
[id, origin, suri, JSON.stringify(oldv)]
)
}
export class SqliteKvFabric {
private pkq: KPQueue = new KPQueue()
private makeId: ProduceId
private dbFactory: DbFactory
constructor (dbFactory: DbFactory, makeId: ProduceId) {
this.dbFactory = dbFactory
this.makeId = makeId
}
async getDb(realmName: string): Promise<sqlite.Database> {
return this.dbFactory.getDb(realmName)
}
async eraseSessionData (realmName: string, sessionId: string, runInboundEvent: (sid: string, key: string[], will: any) => void) {
const toRemove: {key: string, opt: any}[] = []
const db = await this.getDb(realmName)
await db.each(
`SELECT key, opt FROM kv_${realmName} WHERE will_sid = ?`,
[sessionId],
(err, row) => {
toRemove.push({key: row.key, opt: JSON.parse(row.opt)})
}
)
for (let row of toRemove) {
if (row.opt.will) {
await runInboundEvent(sessionId, defaultParse(row.key), row.opt.will)
} else {
await runInboundEvent(sessionId, defaultParse(row.key), null)
}
}
// erase is expected to be done by incoming message
// await this.db.run(
// `DELETE FROM kv_${realmName} WHERE will_sid = ?`,
// [realmName, sessionId]
// )
await db.run(
`DELETE FROM set_value_${realmName} WHERE will_sid = ?`,
[sessionId]
)
}
// @return promise
setKeyValue (realmName: string, suri: string, origin: string, data: any, opt: any, sid: string, pubOutEvent: (kind: string, outEvent: any) => void) {
return this.pkq.enQueue(realmName + '|' + suri, async () => {
const db = await this.getDb(realmName)
const willSid = ('will' in opt) ? sid : 0
try {
const oldRow = await db.get(
`SELECT value, opt FROM kv_${realmName} WHERE key = ?`,
[suri]
)
const oldData = oldRow && oldRow.value ? unSerializeData(JSON.parse(oldRow.value)) : null
const newData = deepDataMerge(oldData, data)
const updateHistoryId = this.makeId.generateIdStr()
if (isDataEmpty(newData)) {
await db.run(`DELETE FROM kv_${realmName} WHERE key = ?`, [suri])
} else {
await db.run(
`INSERT OR REPLACE INTO kv_${realmName} (key, value, will_sid, opt, stamp) VALUES (?, ?, ?, ?, ?)`,
[suri, JSON.stringify(makeDataSerializable(newData)), willSid, JSON.stringify(opt), updateHistoryId]
)
}
saveUpdateHistory(db, realmName, origin, updateHistoryId, suri, makeDataSerializable(oldData))
pubOutEvent('event', { sid, oldData, newData })
} catch (e: any) {
console.error('SetKeyValue ERROR:', e.message, e.stack)
}
})
}
async applySegment(segment: any[], pubOutEvent: (kind: string, outEvent: any) => void) {
for (let inEvent of segment) {
console.log("MOD-KV", inEvent)
if (inEvent.opt.trace) {
await this.setKeyValue(
inEvent.realm,
restoreUri(inEvent.uri),
inEvent.qid,
inEvent.data,
inEvent.opt,
inEvent.sid,
pubOutEvent
)
}
}
}
// @uri is array of strings
getKey (realmName: string, uri: string[], cbRow: (key: string[], data: any, stamp: string) => void) {
const strUri = restoreUri(uri)
return this.pkq.enQueue(realmName + '|' + strUri, async () => {
const db = await this.getDb(realmName)
// TODO: optimize search
await db.each(
`SELECT key, value, opt, stamp FROM kv_${realmName}`,
[],
(err, row) => {
const aKey: string[] = defaultParse(row.key)
if (match(aKey, uri)) {
const rowData = JSON.parse(row.value)
cbRow(aKey, unSerializeData(rowData), row.stamp)
}
}
)
})
}
}
export class SqliteKv extends KeyValueStorageAbstract {
private mod: SqliteKvFabric
private realmName: string
constructor (mod: SqliteKvFabric, realmName: string) {
super()
this.mod = mod
this.realmName = realmName
}
eraseSessionData (sessionId: string) {
return this.mod.eraseSessionData (this.realmName, sessionId, this.runInboundEvent.bind(this))
}
// @result promise
setKeyActor (actor: ActorPush) {
const suri = this.getStrUri(actor)
return this.mod.setKeyValue(
this.realmName,
suri,
actor.getEventId(),
actor.getData(),
actor.getOpt(),
actor.getSid(),
(kind:any, outEvent:any) => actor.confirm(actor.msg)
)
}
// @return promise
getKey (uri: [string], cbRow:any) {
return this.mod.getKey(this.realmName, uri, cbRow)
}
}