Skip to content

Commit

Permalink
Implement persistence of starred items
Browse files Browse the repository at this point in the history
  • Loading branch information
krassowski committed Apr 18, 2024
1 parent 4282865 commit 574fd9e
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 56 deletions.
58 changes: 58 additions & 0 deletions src/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { IStateDB } from '@jupyterlab/statedb';
import { ReadonlyPartialJSONValue, PromiseDelegate } from '@lumino/coreutils';
import { ILauncher } from '@jupyterlab/launcher';

abstract class Database<V extends ReadonlyPartialJSONValue, K> {
ready: Promise<void>;
constructor(options: { stateDB: IStateDB; fetchInterval: number }) {
this._stateDB = options.stateDB;
const ready = new PromiseDelegate<void>();
this.ready = ready.promise;
this._updateDB().then(() => ready.resolve());
window.setInterval(this._updateDB, options.fetchInterval);
}
protected _get(item: K) {
if (!this._db) {
console.error('Database is not ready!');
return null;
}
return this._db[this._itemKey(item)];
}
protected async _set(item: K, value: V) {
const db = await this._fetch();
this._db = db;
db[this._itemKey(item)] = value;
await this._stateDB.save(this._stateDBKey, db);
}
private _updateDB = async () => {
const db = await this._fetch();
this._db = db;
};
private async _fetch(): Promise<Record<string, V>> {
let db = (await this._stateDB.fetch(this._stateDBKey)) as
| Record<string, V>
| undefined;
if (typeof db === 'undefined') {
// retry once: sometimes state DB does not load up on first try
db = (await this._stateDB.fetch(this._stateDBKey)) as
| Record<string, V>
| undefined;
}
if (typeof db === 'undefined') {
return {};
}
return db;
}
protected abstract _itemKey(item: K): string;
protected abstract _stateDBKey: string;
private _db: Record<string, V> | null = null;
private _stateDB: IStateDB;
}

export abstract class ItemDatabase<
V extends ReadonlyPartialJSONValue
> extends Database<V, ILauncher.IItemOptions> {
protected _itemKey(item: ILauncher.IItemOptions): string {
return item.command + '_' + JSON.stringify(item.args);
}
}
25 changes: 12 additions & 13 deletions src/favorites.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { ILauncher } from '@jupyterlab/launcher';
import { ItemDatabase } from './database';

export interface IFavoritesDatabase {
get(item: ILauncher.IItemOptions): boolean | null;
set(item: ILauncher.IItemOptions, isFavourite: boolean): void;
set(item: ILauncher.IItemOptions, isFavourite: boolean): Promise<void>;
}

export class FavoritesDatabase implements IFavoritesDatabase {
constructor() {
// TODO: use settings registry, or state db, or server to persist this info
this._db = new Map();
}
export class FavoritesDatabase
extends ItemDatabase<boolean>
implements IFavoritesDatabase
{
protected _stateDBKey = 'new-launcher:favorites';

get(item: ILauncher.IItemOptions) {
return this._db.get(this._itemKey(item)) ?? null;
return super._get(item) ?? null;
}
set(item: ILauncher.IItemOptions, isFavourite: boolean) {
this._db.set(this._itemKey(item), isFavourite);
}
private _itemKey(item: ILauncher.IItemOptions): string {
return item.command + '_' + JSON.stringify(item.args);

async set(item: ILauncher.IItemOptions, isFavourite: boolean) {
this._set(item, isFavourite);
}
private _db: Map<string, boolean>;
}
10 changes: 6 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,17 @@ function activate(
});
}

const lastUsedDatabase = new LastUsedDatabase({
const databaseOptions = {
stateDB,
fetchInterval: 10000
});
const favoritesDatabase = new FavoritesDatabase();
};
const lastUsedDatabase = new LastUsedDatabase(databaseOptions);
const favoritesDatabase = new FavoritesDatabase(databaseOptions);

commands.addCommand(CommandIDs.create, {
label: trans.__('New Launcher'),
icon: args => (args.toolbar ? addIcon : undefined),
execute: (args: ReadonlyPartialJSONObject) => {
execute: async (args: ReadonlyPartialJSONObject) => {
const cwd = (args['cwd'] as string) ?? defaultBrowser?.model.path ?? '';
const id = `launcher-${Private.id++}`;
const callback = (item: Widget) => {
Expand All @@ -95,6 +96,7 @@ function activate(
launcher.dispose();
}
};
await Promise.all([lastUsedDatabase.ready, favoritesDatabase.ready]);
const launcher = new Launcher({
model,
cwd,
Expand Down
47 changes: 9 additions & 38 deletions src/last_used.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,23 @@
import { ILauncher } from '@jupyterlab/launcher';
import { IStateDB } from '@jupyterlab/statedb';
import { ItemDatabase } from './database';

export interface ILastUsedDatabase {
get(item: ILauncher.IItemOptions): Date | null;
recordAsUsedNow(item: ILauncher.IItemOptions): Promise<void>;
}

type DatabaseLayout = Record<string, string>;
export class LastUsedDatabase
extends ItemDatabase<string>
implements ILastUsedDatabase
{
protected _stateDBKey = 'new-launcher:last-used';

export class LastUsedDatabase implements ILastUsedDatabase {
constructor(options: { stateDB: IStateDB; fetchInterval: number }) {
this._stateDB = options.stateDB;
this._updateDB();
window.setInterval(this._updateDB, options.fetchInterval);
}
get(item: ILauncher.IItemOptions) {
if (!this._db) {
return null;
}
const date = this._db[this._itemKey(item)];
const date = super._get(item);
return date ? new Date(date) : null;
}

async recordAsUsedNow(item: ILauncher.IItemOptions) {
const db = await this._fetch();
this._db = db;
db[this._itemKey(item)] = new Date().toUTCString();
await this._stateDB.save(this._stateDBKey, db);
}
private _updateDB = () => {
this._fetch()
.then(db => {
this._db = db;
})
.catch(console.warn);
};
private async _fetch(): Promise<DatabaseLayout> {
const db = (await this._stateDB.fetch(this._stateDBKey)) as
| DatabaseLayout
| undefined;
if (typeof db === 'undefined') {
return {};
}
return db;
}
private _itemKey(item: ILauncher.IItemOptions): string {
return item.command + '_' + JSON.stringify(item.args);
this._set(item, new Date().toUTCString());
}
private _db: DatabaseLayout | null = null;
private _stateDB: IStateDB;
private _stateDBKey = 'new-launcher:last-used';
}
3 changes: 2 additions & 1 deletion src/launcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ class Item implements IItem {
const wasStarred = favoritesDatabase.get(item);
const newState = !wasStarred;
this.starred = newState;
favoritesDatabase.set(item, newState);
return favoritesDatabase.set(item, newState);
}
private _setRefreshClock() {
const value = this._lastUsed;
Expand Down Expand Up @@ -533,6 +533,7 @@ export class NewLauncher extends Launcher {
.filter(item => item.category && item.category === notebookCategory)
.map(this.renderKernelCommand);

// TODO: only create items once or if changed; dispose of them too
const typeItems: IItem[] = typeCommands.map(this.renderCommand);

return (
Expand Down

0 comments on commit 574fd9e

Please sign in to comment.