Skip to content

Commit d743a2a

Browse files
committed
WIP: add PathwaysDataBuffer
1 parent 966ec2a commit d743a2a

File tree

10 files changed

+225
-42
lines changed

10 files changed

+225
-42
lines changed

dbux-code/src/activate.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { initDialogController } from './dialogs/dialogController';
2121
import DialogNodeKind from './dialogs/DialogNodeKind';
2222
import { showInformationMessage } from './codeUtil/codeModals';
2323
import { translate } from './lang';
24+
import { initWorkshopSession } from './workshop';
2425
// import { initPlugins } from './PluginMgr';
2526

2627
// eslint-disable-next-line no-unused-vars
@@ -34,7 +35,9 @@ export default async function activate(context) {
3435
log(`Starting Dbux v${process.env.DBUX_VERSION} (mode=${process.env.NODE_ENV}${dbuxRoot})...`);
3536

3637
// make sure, projectManager is available
37-
createProjectManager(context);
38+
const projectsManager = createProjectManager(context);
39+
40+
await initWorkshopSession(projectsManager);
3841

3942
// install dependencies (and show progress bar) right away
4043
await installDbuxDependencies();

dbux-code/src/dialogs/survey1.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-len */
22
import { env, Uri, window } from 'vscode';
33
import ExerciseStatus from '@dbux/projects/src/dataLib/ExerciseStatus';
4-
import { upload } from '@dbux/projects/src/firestore/upload';
4+
import { uploadSurvey } from '@dbux/projects/src/firestore/upload';
55
import { newLogger } from '@dbux/common/src/log/logger';
66
import { showHelp } from '../help';
77
import DialogNodeKind from './DialogNodeKind';
@@ -458,7 +458,7 @@ ${data.email || ''}`;
458458
log('survey result', data);
459459

460460
// store to backend
461-
await upload('survey1', data);
461+
await uploadSurvey(data);
462462
// const backend = await getProjectManager().getAndInitBackend();
463463
// return backend.containers.survey1.storeSurveyResult(data);
464464
},

dbux-code/src/preActivate.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { initPreActivateView } from './preActivateView/preActivateNodeProvider';
99
import { registerCommand } from './commands/commandUtil';
1010
import initLang from './lang';
1111
import { getCurrentResearch } from './research/Research';
12-
import { activateWorkshopSession, isValidCode } from './workshop/Workshop';
12+
import { setupWorkshopSession, isValidCode } from './workshop';
1313
import { showInformationMessage } from './codeUtil/codeModals';
1414

1515
/** @typedef {import('./dialogs/dialogController').DialogController} DialogController */
@@ -70,8 +70,10 @@ export default async function preActivate(context) {
7070
commands.executeCommand('setContext', 'dbux.context.researchEnabled', !!process.env.RESEARCH_ENABLED);
7171

7272
// the following should ensures `doActivate` will be called at least once
73-
autoStart = (process.env.NODE_ENV === 'development') ||
74-
workspace.getConfiguration('dbux').get('autoStart');
73+
// autoStart = (process.env.NODE_ENV === 'development') ||
74+
// workspace.getConfiguration('dbux').get('autoStart');
75+
// TODO-M: recover this
76+
autoStart = workspace.getConfiguration('dbux').get('autoStart');
7577
if (autoStart) {
7678
await ensureActivate(context);
7779
}
@@ -116,11 +118,11 @@ function initPreActivateCommand(context) {
116118
placeHolder: 'Enter Workshop Code'
117119
});
118120
if (isValidCode(code)) {
119-
activateWorkshopSession(code);
121+
setupWorkshopSession(code);
120122
await ensureActivate(context);
121123
}
122124
else {
123-
await showInformationMessage(`Workshop code ${code} is invalid`);
125+
await showInformationMessage(`Workshop code "${code}" is invalid.`, { modal: true });
124126
}
125127
});
126128
}

dbux-code/src/preActivateView/WorkshopNode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode';
22

33
export default class WorkshopNode extends TreeItem {
44
constructor() {
5-
super('Start Dbux', TreeItemCollapsibleState.None);
5+
super('Start Workshop Session', TreeItemCollapsibleState.None);
66

77
this.command = {
88
command: 'dbux.doWorkshopActivate'
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
import { newLogger } from '@dbux/common/src/log/logger';
3+
import NestedError from '@dbux/common/src/NestedError';
4+
import { uploadPathways } from '@dbux/projects/src/firestore/upload';
5+
import SafetyStorage from './SafetyStorage';
6+
7+
// eslint-disable-next-line no-unused-vars
8+
const { log, debug, warn, error: logError } = newLogger('PathwaysDataBuffer');
9+
10+
const DefaultBufferSize = 50;
11+
const DefaultBufferFlushTime = 2 * 60 * 1000; // 2 minutes
12+
13+
export default class PathwaysDataBuffer {
14+
constructor(sessionId, collectionName) {
15+
const storageKeyName = `dbux.pathwaysDataBuffer.${collectionName}`;
16+
this.buffer = new SafetyStorage(storageKeyName);
17+
this.sessionId = sessionId;
18+
this.collectionName = collectionName;
19+
this._previousFlushTime = Date.now();
20+
}
21+
22+
/**
23+
* @return {Array}
24+
*/
25+
safeGetBuffer() {
26+
return this.buffer.get() || [];
27+
}
28+
29+
async add(entries) {
30+
await this.buffer.acquireLock();
31+
32+
try {
33+
let buffer = this.safeGetBuffer();
34+
buffer.push(...entries);
35+
await this.buffer.set(buffer);
36+
}
37+
finally {
38+
this.buffer.releaseLock();
39+
}
40+
}
41+
42+
async maybeFlush() {
43+
if (!this._flushing && (this.safeGetBuffer().length >= DefaultBufferSize || Date.now() - this._previousFlushTime >= DefaultBufferFlushTime)) {
44+
await this.forceFlush();
45+
}
46+
}
47+
48+
async forceFlush() {
49+
this._flushing = true;
50+
await this._flush();
51+
}
52+
53+
async _flush() {
54+
this._flushing = true;
55+
await this.buffer.acquireLock();
56+
57+
let buffer;
58+
try {
59+
buffer = this.safeGetBuffer();
60+
await this.buffer.set([]);
61+
}
62+
finally {
63+
this.buffer.releaseLock();
64+
}
65+
66+
try {
67+
await this.addDoc(buffer);
68+
}
69+
catch (err) {
70+
this._flushing = false;
71+
throw new NestedError(`Failed when flushing`, err);
72+
}
73+
74+
this._previousFlushTime = Date.now();
75+
}
76+
77+
async addDoc(entries) {
78+
return await uploadPathways(this.sessionId, this.collectionName, entries);
79+
}
80+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os from 'os';
2+
import path from 'path';
3+
import lockfile from 'lockfile';
4+
import { newLogger } from '@dbux/common/src/log/logger';
5+
import { get, set } from '../memento';
6+
7+
// eslint-disable-next-line no-unused-vars
8+
const { log, debug, warn, error: logError } = newLogger('SavetyStorage');
9+
10+
function getLockfileName(name) {
11+
const lockfilePath = path.join(os.tmpdir(), `dbux-lockfile.${name}`);
12+
return lockfilePath;
13+
}
14+
15+
async function acquireLock(name) {
16+
return new Promise((resolve, reject) => {
17+
lockfile.lock(getLockfileName(name), { wait: 10 ** 9 }, (err) => {
18+
if (err) {
19+
reject(err);
20+
}
21+
else {
22+
resolve();
23+
}
24+
});
25+
});
26+
}
27+
28+
export default class SafetyStorage {
29+
constructor(name) {
30+
this.name = name;
31+
}
32+
33+
async acquireLock() {
34+
return acquireLock(this.name);
35+
}
36+
37+
releaseLock() {
38+
lockfile.unlockSync(getLockfileName(this.name));
39+
}
40+
41+
get() {
42+
return get(this.name);
43+
}
44+
45+
async set(value) {
46+
await set(this.name, value);
47+
}
48+
}

dbux-code/src/workshop/Workshop.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

dbux-code/src/workshop/index.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/** @typedef {import('@dbux/projects/src/ProjectsManager').default} ProjectsManager */
2+
3+
const ValidCodes = new Set([]);
4+
let currentCode = '';
5+
6+
if (process.env.NODE_ENV === 'development') {
7+
ValidCodes.add('1234');
8+
}
9+
10+
/**
11+
* @param {string} code
12+
* @returns {boolean}
13+
*/
14+
export function isValidCode(code) {
15+
return ValidCodes.has(code);
16+
}
17+
18+
export function isWorkshopSessionActive() {
19+
return !!currentCode;
20+
}
21+
22+
export function setupWorkshopSession(code) {
23+
if (isValidCode(code)) {
24+
currentCode = code;
25+
}
26+
else {
27+
throw new Error('Invalid workshop code');
28+
}
29+
}
30+
31+
/** ###########################################################################
32+
* pdp listener and log writing
33+
* #########################################################################*/
34+
35+
/**
36+
* @param {ProjectsManager} projectsManager
37+
* @returns
38+
*/
39+
export async function initWorkshopSession(projectsManager) {
40+
if (isWorkshopSessionActive()) {
41+
addHook();
42+
projectsManager.onPracticeSessionStateChanged(addHook);
43+
}
44+
}
45+
46+
let sessionId, prevListener;
47+
48+
function addHook(session) {
49+
if (sessionId !== session?.sessionId) {
50+
// stop listening on previous events
51+
prevListener?.();
52+
53+
prevListener = session?.pdp.onAnyData(addData);
54+
}
55+
}
56+
57+
function addData(allData) {
58+
for (const collectionName of Object.keys(allData)) {
59+
// TODO-M: push data into buffer
60+
}
61+
}

dbux-projects/src/ProjectsManager.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,8 +518,8 @@ export default class ProjectsManager {
518518

519519
await this.practiceSession.init();
520520

521-
isNew && emitPracticeSessionEvent('started', this.practiceSession);
522521
this._notifyPracticeSessionStateChanged();
522+
isNew && emitPracticeSessionEvent('started', this.practiceSession);
523523
await this.saveSession();
524524
}
525525

@@ -601,7 +601,7 @@ export default class ProjectsManager {
601601
}
602602

603603
_notifyPracticeSessionStateChanged() {
604-
this._emitter.emit('practiceSessionStateChanged');
604+
this._emitter.emit('practiceSessionStateChanged', this.practiceSession);
605605
}
606606

607607
// ########################################

dbux-projects/src/firestore/upload.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,28 @@ import NestedError from '@dbux/common/src/NestedError';
44

55
const API_KEY = 'AIzaSyC-d0HDLJ8Gd9UZ175z7dg6J98ZrOIK0Mc';
66

7-
function getUrl(collectionId) {
8-
return `https://firestore.googleapis.com/v1/projects/learn-learn-b8e5a/databases/(default)/documents/${collectionId}?key=${API_KEY}`;
7+
function makeUrl(collectionId, documentId = null) {
8+
const url = new URL(`https://firestore.googleapis.com`);
9+
url.pathname = `/v1/projects/learn-learn-b8e5a/databases/(default)/documents/${collectionId}`;
10+
url.searchParams.set('key', API_KEY);
11+
if (documentId) {
12+
url.searchParams.set('documentId', documentId);
13+
}
14+
return url.toString();
915
}
1016

11-
export async function upload(collectionId, data) {
12-
const url = getUrl(collectionId);
17+
export async function uploadSurvey(data) {
18+
return await upload(makeUrl('survey1'), data);
19+
}
20+
21+
export async function uploadPathways(sessionId, collectionName, entries) {
22+
const collectionId = `sessions/${sessionId}/${collectionName}`;
23+
const documentId = null;
24+
const url = makeUrl(collectionId, documentId);
25+
return await upload(url, entries);
26+
}
27+
28+
export async function upload(url, data) {
1329
const serializedData = serialize(data);
1430
const dataString = JSON.stringify({ fields: serializedData });
1531

0 commit comments

Comments
 (0)