-
Notifications
You must be signed in to change notification settings - Fork 46
Expand file tree
/
Copy pathsync.ts
More file actions
100 lines (91 loc) · 2.87 KB
/
Copy pathsync.ts
File metadata and controls
100 lines (91 loc) · 2.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { zero, zeroClean, type ZeroCleanAction } from '@logux/actions'
import { parseId } from '@logux/core'
import type { ServerClient } from '@logux/server'
import { dbReset, RETENTION } from '@slowreader/api'
import { inArray, sql } from 'drizzle-orm'
import { db, users } from '../db/index.ts'
import { debounce } from '../lib/debounce.ts'
import type { AppServer, ClientData } from '../lib/types.ts'
function ids(action: ZeroCleanAction): string[] {
return 'id' in action ? [action.id] : action.ids
}
async function wasOfflineTooLong(
client: ServerClient<ClientData>
): Promise<boolean> {
let previous = client.data.usedAt
if (!previous) return false
if (Date.now() - previous.getTime() < RETENTION) return false
let user = await db.query.users.findFirst({
columns: { lastActionAt: true },
where: { id: client.userId }
})
// Nothing was written while the device was away, so it missed nothing
return !!user?.lastActionAt && user.lastActionAt > previous
}
export default (server: AppServer): void => {
// A single sync message can bring many actions, but we need only one write
let acted = new Set<string>()
let writeLastActions = debounce(() => {
let list = [...acted]
acted.clear()
void db
.update(users)
.set({ lastActionAt: sql`now()` })
.where(inArray(users.id, list))
/* node:coverage ignore next 3 */
.catch((error: unknown) => {
server.logger.error(error)
})
}, 50)
server.on('report', event => {
if (event === 'destroy') writeLastActions.flush()
})
// The log keeps the action for the other devices of the user until
// `0/clean` will say that nobody needs it anymore
server.log.on('preadd', (action, meta) => {
if (zero.match(action)) {
let { userId } = parseId(meta.id)
if (userId) {
meta.indexes = [`users/${userId}`]
meta.reasons.push('store')
}
}
})
// The device was offline longer than the retention, so the actions,
// which it missed, could be already cleaned from the log
server.on('authenticated', client => {
wasOfflineTooLong(client)
.then(tooLong => {
if (tooLong && client.clientId) {
return server.process(dbReset({}), { clients: [client.clientId] })
}
})
/* node:coverage ignore next 3 */
.catch((error: unknown) => {
server.logger.error(error)
})
})
server.type(zero, {
access() {
return true
},
resend(ctx) {
return { users: [ctx.userId] }
},
process(ctx) {
acted.add(ctx.userId)
void writeLastActions()
}
})
server.type(zeroClean, {
access(ctx, action) {
return ids(action).every(id => parseId(id).userId === ctx.userId)
},
resend(ctx) {
return { users: [ctx.userId] }
},
async process(ctx, action) {
await server.log.removeReason('store', { ids: ids(action) })
}
})
}