Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/core/dbs/SessionDb.ts
1028 views
1
import * as Database from 'better-sqlite3';
2
import { Database as SqliteDatabase, Transaction } from 'better-sqlite3';
3
import * as Path from 'path';
4
import Log from '@secret-agent/commons/Logger';
5
import SqliteTable from '@secret-agent/commons/SqliteTable';
6
import ResourcesTable from '../models/ResourcesTable';
7
import DomChangesTable from '../models/DomChangesTable';
8
import CommandsTable from '../models/CommandsTable';
9
import WebsocketMessagesTable from '../models/WebsocketMessagesTable';
10
import FrameNavigationsTable from '../models/FrameNavigationsTable';
11
import FramesTable from '../models/FramesTable';
12
import PageLogsTable from '../models/PageLogsTable';
13
import SessionTable from '../models/SessionTable';
14
import MouseEventsTable from '../models/MouseEventsTable';
15
import FocusEventsTable from '../models/FocusEventsTable';
16
import ScrollEventsTable from '../models/ScrollEventsTable';
17
import SessionLogsTable from '../models/SessionLogsTable';
18
import SessionsDb from './SessionsDb';
19
import SessionState from '../lib/SessionState';
20
import DevtoolsMessagesTable from '../models/DevtoolsMessagesTable';
21
import TabsTable from '../models/TabsTable';
22
import ResourceStatesTable from '../models/ResourceStatesTable';
23
import SocketsTable from '../models/SocketsTable';
24
import OutputTable from '../models/OutputTable';
25
26
const { log } = Log(module);
27
28
interface IDbOptions {
29
readonly?: boolean;
30
fileMustExist?: boolean;
31
}
32
33
export default class SessionDb {
34
private static byId = new Map<string, SessionDb>();
35
36
public get readonly() {
37
return this.db?.readonly;
38
}
39
40
public readonly commands: CommandsTable;
41
public readonly frames: FramesTable;
42
public readonly frameNavigations: FrameNavigationsTable;
43
public readonly sockets: SocketsTable;
44
public readonly output: OutputTable;
45
public readonly resources: ResourcesTable;
46
public readonly resourceStates: ResourceStatesTable;
47
public readonly websocketMessages: WebsocketMessagesTable;
48
public readonly domChanges: DomChangesTable;
49
public readonly pageLogs: PageLogsTable;
50
public readonly sessionLogs: SessionLogsTable;
51
public readonly session: SessionTable;
52
public readonly mouseEvents: MouseEventsTable;
53
public readonly focusEvents: FocusEventsTable;
54
public readonly scrollEvents: ScrollEventsTable;
55
public readonly devtoolsMessages: DevtoolsMessagesTable;
56
public readonly tabs: TabsTable;
57
public readonly sessionId: string;
58
59
private readonly batchInsert?: Transaction;
60
private readonly saveInterval: NodeJS.Timeout;
61
62
private db: SqliteDatabase;
63
private readonly tables: SqliteTable<any>[] = [];
64
65
constructor(baseDir: string, id: string, dbOptions: IDbOptions = {}) {
66
const { readonly = false, fileMustExist = false } = dbOptions;
67
this.sessionId = id;
68
this.db = new Database(`${baseDir}/${id}.db`, { readonly, fileMustExist });
69
if (!readonly) {
70
this.saveInterval = setInterval(this.flush.bind(this), 5e3).unref();
71
}
72
73
this.commands = new CommandsTable(this.db);
74
this.tabs = new TabsTable(this.db);
75
this.frames = new FramesTable(this.db);
76
this.frameNavigations = new FrameNavigationsTable(this.db);
77
this.sockets = new SocketsTable(this.db);
78
this.resources = new ResourcesTable(this.db);
79
this.resourceStates = new ResourceStatesTable(this.db);
80
this.websocketMessages = new WebsocketMessagesTable(this.db);
81
this.domChanges = new DomChangesTable(this.db);
82
this.pageLogs = new PageLogsTable(this.db);
83
this.session = new SessionTable(this.db);
84
this.mouseEvents = new MouseEventsTable(this.db);
85
this.focusEvents = new FocusEventsTable(this.db);
86
this.scrollEvents = new ScrollEventsTable(this.db);
87
this.sessionLogs = new SessionLogsTable(this.db);
88
this.devtoolsMessages = new DevtoolsMessagesTable(this.db);
89
this.output = new OutputTable(this.db);
90
91
this.tables.push(
92
this.commands,
93
this.tabs,
94
this.frames,
95
this.frameNavigations,
96
this.sockets,
97
this.resources,
98
this.resourceStates,
99
this.websocketMessages,
100
this.domChanges,
101
this.pageLogs,
102
this.session,
103
this.mouseEvents,
104
this.focusEvents,
105
this.scrollEvents,
106
this.sessionLogs,
107
this.devtoolsMessages,
108
this.output,
109
);
110
111
if (!readonly) {
112
this.batchInsert = this.db.transaction(() => {
113
for (const table of this.tables) {
114
try {
115
table.runPendingInserts();
116
} catch (error) {
117
if (String(error).match('attempt to write a readonly database')) {
118
clearInterval(this.saveInterval);
119
this.db = null;
120
}
121
log.error('SessionDb.flushError', {
122
sessionId: this.sessionId,
123
error,
124
table: table.tableName,
125
});
126
}
127
}
128
});
129
}
130
}
131
132
public close() {
133
clearInterval(this.saveInterval);
134
if (this.db) {
135
this.flush();
136
this.db.close();
137
}
138
this.db = null;
139
}
140
141
public flush() {
142
if (this.batchInsert) {
143
try {
144
this.batchInsert.immediate();
145
} catch (error) {
146
if (String(error).match(/attempt to write a readonly database/)) {
147
clearInterval(this.saveInterval);
148
}
149
throw error;
150
}
151
}
152
}
153
154
public unsubscribeToChanges() {
155
for (const table of this.tables) table.unsubscribe();
156
}
157
158
public static getCached(sessionId: string, basePath: string, fileMustExist = false) {
159
if (!this.byId.get(sessionId)?.db?.open) {
160
this.byId.set(
161
sessionId,
162
new SessionDb(basePath, sessionId, {
163
readonly: true,
164
fileMustExist,
165
}),
166
);
167
}
168
return this.byId.get(sessionId);
169
}
170
171
public static findWithRelated(scriptArgs: ISessionLookupArgs): ISessionLookup {
172
let { dataLocation, sessionId } = scriptArgs;
173
174
const ext = Path.extname(dataLocation);
175
if (ext === '.db') {
176
sessionId = Path.basename(dataLocation, ext);
177
dataLocation = Path.dirname(dataLocation);
178
}
179
180
// NOTE: don't close db - it's from a shared cache
181
const sessionsDb = SessionsDb.find(dataLocation);
182
if (!sessionId) {
183
sessionId = sessionsDb.findLatestSessionId(scriptArgs);
184
if (!sessionId) return null;
185
}
186
187
const activeSession = SessionState.registry.get(sessionId);
188
189
const sessionDb = activeSession?.db ?? this.getCached(sessionId, dataLocation, true);
190
191
const session = sessionDb.session.get();
192
const related = sessionsDb.findRelatedSessions(session);
193
194
return {
195
...related,
196
dataLocation,
197
sessionDb,
198
sessionState: activeSession,
199
};
200
}
201
}
202
203
export interface ISessionLookup {
204
sessionDb: SessionDb;
205
dataLocation: string;
206
sessionState: SessionState;
207
relatedSessions: { id: string; name: string }[];
208
relatedScriptInstances: { id: string; startDate: number; defaultSessionId: string }[];
209
}
210
211
export interface ISessionLookupArgs {
212
scriptInstanceId: string;
213
sessionName: string;
214
scriptEntrypoint: string;
215
dataLocation: string;
216
sessionId: string;
217
}
218
219