Skip to content

Commit

Permalink
WIP blob
Browse files Browse the repository at this point in the history
  • Loading branch information
pulsejet committed Feb 21, 2025
1 parent 178fdbb commit 3b93704
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 29 deletions.
46 changes: 46 additions & 0 deletions ndn/app/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

enc "github.com/named-data/ndnd/std/encoding"
"github.com/named-data/ndnd/std/log"
"github.com/named-data/ndnd/std/ndn"
"github.com/named-data/ndnd/std/ndn/svs_ps"
"github.com/named-data/ndnd/std/object"
"github.com/named-data/ndnd/std/security"
Expand Down Expand Up @@ -93,6 +94,51 @@ func (a *App) MakeWorkspace(groupStr string) (api js.Value, err error) {
return nil, nil
}),

// produce(name: string, data: Uint8Array): Promise<void>;
"produce": utils.AsyncFunc(func(this js.Value, p []js.Value) (any, error) {
name, err := enc.NameFromStr(p[0].String())
if err != nil {
return nil, err
}

_, err = client.Produce(ndn.ProduceArgs{
Name: name,
Content: enc.Wire{utils.JsArrayToSlice(p[1])},
})

return nil, err
}),

// consume(name: string): Promise<{ data: Uint8Array; name: string; }>;
"consume": utils.AsyncFunc(func(this js.Value, p []js.Value) (any, error) {
name, err := enc.NameFromStr(p[0].String())
if err != nil {
return nil, err
}

// Attempt to get the content from the local store
local, err := client.GetLocal(name)
if err == nil {
return js.ValueOf(map[string]any{
"data": utils.SliceToJsArray(local.Join()),
"name": js.ValueOf(name.String()),
}), nil
}

// Fetch the content from the network
ch := make(chan ndn.ConsumeState)
client.Consume(name, func(state ndn.ConsumeState) { ch <- state })
state := <-ch
if err := state.Error(); err != nil {
return nil, err
}

return js.ValueOf(map[string]any{
"data": utils.SliceToJsArray(state.Content().Join()),
"name": js.ValueOf(state.Name().String()),
}), nil
}),

// svs_alo(group: string): Promise<SvsAloApi>;
"svs_alo": utils.AsyncFunc(func(this js.Value, p []js.Value) (any, error) {
svsAloGroup, err := enc.NameFromStr(p[0].String())
Expand Down
Binary file modified public/main.wasm
Binary file not shown.
11 changes: 11 additions & 0 deletions src/components/ProjectTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ async function importHere() {
try {
const path = `${props.path}${file.name}`;
await proj.importFile(path, file.stream());
toast.success(`Imported ${file.name}`);
} catch (err) {
console.warn(err);
toast.warning(`Could not import ${file.name}: ${err}`);
Expand All @@ -377,6 +378,9 @@ async function importZipHere() {
// Read the ZIP file and import each entry
const reader = new zip.ZipReader(new zip.BlobReader(zipFile));
// TODO: show progress for each file (also for importHere)
let importedCount = 0;
for await (const entry of reader.getEntriesGenerator()) {
try {
// We don't need to make folders
Expand All @@ -388,10 +392,17 @@ async function importZipHere() {
const path = `${props.path}${entry.filename}`;
await proj.importFile(path, content.stream());
importedCount++;
} catch (err) {
console.warn(err);
toast.warning(`Could not import ${entry.filename}: ${err}`);
}
if (importedCount > 0) {
toast.success(`Imported ${importedCount} files from ${zipFile.name}`);
} else {
toast.warning(`No files imported from ${zipFile.name}`);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/latex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function compile(project: WorkspaceProj): Promise<Uint8Array> {
}

// Fail fast if main.tex is not found
const fileList = project.fileList();
const fileList = project.getFileList();
if (!fileList.some((file) => file.path === '/main.tex')) {
throw new Error('main.tex not found at root of project');
}
Expand Down
5 changes: 5 additions & 0 deletions src/services/ndn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export interface WorkspaceAPI {
/** Stop the workspace */
stop(): Promise<void>;

/** Produce an NDN object under a given name */
produce(name: string, data: Uint8Array): Promise<void>;
/** Consume an NDN object with a name */
consume(name: string): Promise<{ data: Uint8Array; name: string }>;

/** SVS ALO instance */
svs_alo(group: string): Promise<SvsAloApi>;
/** Awareness instance */
Expand Down
44 changes: 41 additions & 3 deletions src/services/svs-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export class SvsProvider {

/**
* Create a new SVS provider for a project.
*
* @param wksp Workspace API
* @param project Project name
*/
public static async create(wksp: WorkspaceAPI, project: string): Promise<SvsProvider> {
const svs = await wksp.svs_alo(`${wksp.group}/${project}`);
Expand Down Expand Up @@ -106,7 +109,11 @@ export class SvsProvider {
await this.svs.start();
}

/** Get a Yjs document from the project. */
/**
* Get a Yjs document from the project.
*
* @param uuid UUID of the document
*/
public async getDoc(uuid: string): Promise<Y.Doc> {
let doc = this.docs.get(uuid);
if (doc) return doc;
Expand Down Expand Up @@ -138,7 +145,12 @@ export class SvsProvider {
return doc;
}

/** Load updates from persistence into a document */
/**
* Load updates from persistence into a document.
*
* @param doc Document to load into
* @param uuid UUID of the document
*/
public async readInto(doc: Y.Doc, uuid: string): Promise<void> {
await this.db.updates
.where('uuid')
Expand All @@ -148,7 +160,33 @@ export class SvsProvider {
});
}

/** Get the awareness instance for a document */
/**
* Publish a blob object to the group
*
* @param uuid UUID of the document
* @param blob Blob to publish
*
* @returns Name of the published blob
*/
public async publishBlob(uuid: string, blob: Uint8Array): Promise<string> {
// Seems okay to use ms time as version for now.
const version = Date.now();

// Place all blobs under the data prefix.
const name = `${this.svs.data_prefix}/32=blob/${uuid}/v=${version}`;
await this.wksp.produce(name, blob);

// TODO: publish name to sync group for repo to pick up

return name;
}

/**
* Get the awareness instance for a document.
* If an awareness exists, the same instance will be returned.
*
* @param uuid UUID of the document
*/
public async getAwareness(uuid: string): Promise<awareProto.Awareness> {
const doc = this.docs.get(uuid);
if (!doc) throw new Error('Document not loaded');
Expand Down
9 changes: 9 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,12 @@ export type AwarenessLocalState = {
rgb: [number, number, number];
};
};

export type IBlobVersion = {
/** Name of the NDN object */
name: string;
/** Timestamp of version */
time: number;
/** Size of the blob */
size: number;
};
6 changes: 3 additions & 3 deletions src/services/workspace-chat.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EventEmitter } from 'events';
import * as Y from 'yjs';

import type { IChatChannel, IChatMessage } from './types';
import { GlobalBus } from './event-bus';
import { SvsProvider } from './svs-provider';
import type { IChatChannel, IChatMessage } from '@/services/types';
import { GlobalBus } from '@/services/event-bus';
import { SvsProvider } from '@/services/svs-provider';

import type TypedEmitter from 'typed-emitter';

Expand Down
43 changes: 31 additions & 12 deletions src/services/workspace-proj.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as Y from 'yjs';
import * as awareProto from 'y-protocols/awareness.js';

import { GlobalBus } from './event-bus';
import { SvsProvider } from './svs-provider';
import { GlobalBus } from '@/services/event-bus';
import { SvsProvider } from '@/services/svs-provider';
import * as opfs from '@/services/opfs';
import * as utils from '@/utils';

import type { WorkspaceAPI } from './ndn';
import type { IProject, IProjectFile } from './types';
import type { IBlobVersion, IProject, IProjectFile } from './types';

/**
* Project manager for the workspace.
Expand Down Expand Up @@ -132,26 +132,26 @@ export class WorkspaceProj {
}

/** Get the list of files */
public fileList(): IProjectFile[] {
public getFileList(): IProjectFile[] {
return Array.from(this.fileMap.values());
}

/** Callback when the list of files changes */
private onListChange() {
if (!this.fileMap || this.manager.active?.root.guid !== this.root.guid) return;
GlobalBus.emit('project-files', this.name, this.fileList());
GlobalBus.emit('project-files', this.name, this.getFileList());
}

/** Check if a file or folder exists */
public fileMeta(path: string): IProjectFile | undefined {
public getFileMeta(path: string): IProjectFile | undefined {
path = utils.normalizePath(path);
return this.fileMap.get(path);
}

/** Create a new file or folder in the project */
public async newFile(path: string, is_blob?: boolean) {
if (!path) throw new Error('File path is required');
if (this.fileMeta(path) || this.fileMeta(path + '/'))
if (this.getFileMeta(path) || this.getFileMeta(path + '/'))
throw new Error('File or folder already exists');

// Check for invalid characters
Expand Down Expand Up @@ -197,9 +197,8 @@ export class WorkspaceProj {
* @returns The Y.Doc instance for the file.
*/
public async getFile(path: string): Promise<Y.Doc> {
const meta = this.fileMeta(path);
const meta = this.getFileMeta(path);
if (!meta?.uuid) throw new Error(`File not found: ${path}`);
if (meta.is_blob) throw new Error('Binary files not implemented'); // TODO
return await this.provider.getDoc(meta.uuid);
}

Expand Down Expand Up @@ -230,7 +229,7 @@ export class WorkspaceProj {

// Get all matching files
const oldIsFolder = oldPath.endsWith('/');
const oldMetas: IProjectFile[] = this.fileList().filter((f) => {
const oldMetas: IProjectFile[] = this.getFileList().filter((f) => {
if (f.path === oldPath) return true;
if (oldIsFolder && f.path.startsWith(oldPath)) return true;
return false;
Expand Down Expand Up @@ -313,7 +312,6 @@ export class WorkspaceProj {
const isText = utils.isExtensionType(path, 'code');
const isMilkdown = utils.isExtensionType(path, 'milkdown');
const isBlob = !isText && !isMilkdown;
if (isBlob) throw new Error('Binary files not implemented'); // TODO

// This requires us to parse the XML document. A better way might be
// to export to markdown and then import it back.
Expand All @@ -334,7 +332,28 @@ export class WorkspaceProj {

// Import binary content
if (isBlob) {
throw new Error('Not implemented'); // TODO
// Read the full buffer for now. We can use a stream directly in the
// future if we want to support big files for some reason.
const buffer = await new Response(content).arrayBuffer();
const name = await this.provider.publishBlob(meta.uuid, new Uint8Array(buffer));

// Update the file version history
const doc = await this.getFile(path);
try {
const history = doc.getArray<IBlobVersion>('blobs');
history.push([
{
name: name,
time: Date.now(),
size: buffer.byteLength,
},
]);
} finally {
// TODO: see comment on isText block below
doc.destroy();
}

return;
}

// Import text content
Expand Down
4 changes: 2 additions & 2 deletions src/services/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { useToast } from 'vue-toast-notification';
import { WorkspaceChat } from './workspace-chat';
import { WorkspaceProjManager } from './workspace-proj';

import { SvsProvider } from './svs-provider';
import { SvsProvider } from '@/services/svs-provider';

import storage from '@/services/storage';
import ndn from '@/services/ndn';
import * as utils from '@/utils/index';

import type { WorkspaceAPI } from '@/services/ndn';
import type { Router } from 'vue-router';
import type { IWorkspace } from './types';
import type { IWorkspace } from '@/services/types';

/**
* We keep an active instance of the open workspace.
Expand Down
Loading

0 comments on commit 3b93704

Please sign in to comment.