Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 24 additions & 30 deletions client/bin/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,31 @@ let cursorAt: InoPosRef | null = null;
function reconcileOtherCursors() {
// TODO: replace other user cursors
const cursors = floor.byIno(FIXED_CURSORS);
const selfCursorPos = cursorAt ? floor.resolveRef(cursorAt) : 0;
const selfCursorIndex = (cursorAt ? floor.resolveRef(cursorAt) : 0) - 1;

const cursorsToRender = new Map<string, number>();
const data = cursors.data();

for (let i = 0; i < cursors.length(); ++i) {
if (selfCursorPos - 1 === i) {
continue;
}
// TODO: someone else

const v = cursors.at(i);
if (!(v instanceof InoPosRef)) {
continue;
data.forEach((v, index) => {
if (index === selfCursorIndex || !(v instanceof InoPosRef)) {
return;
}

const pos = floor.resolveRef(v);
cursorsToRender.set(String(i), pos);
}
cursorsToRender.set(String(index), pos);
});

e.cursor = cursorsToRender;
}

e.addListener(
'modify',
(m) => {
floor.perform(function* (byIno) {
const api = byIno(FIXED_STRING);
api.updateString(m.lo, m.hi - m.lo, m.insert);
floor.perform(function* (t) {
const api = t.byIno(FIXED_STRING);
api.stringUpdate(m.lo, m.hi - m.lo, m.insert);
});

e.text = model.asString();
e.text = model.string();
reconcileOtherCursors();

// readjust cursor and collapse selection
Expand All @@ -72,19 +66,19 @@ e.addListener(
e.addListener(
'cursor',
() => {
floor.perform(function* (byIno, resolveRef) {
const c = byIno(FIXED_CURSORS);
const text = byIno(FIXED_STRING);
floor.perform(function* (t) {
const c = t.byIno(FIXED_CURSORS);
const text = t.byIno(FIXED_STRING);

let pos = cursorAt ? resolveRef(cursorAt) : 0;
let pos = cursorAt ? t.resolveRef(cursorAt) : 0;
if (pos === 0) {
// insert self
c.splice(0, 0, null);
c.dataUpdate(0, 0, null);
cursorAt = c.refAt(1); // TODO: this is pos, not index
pos = 1;
}

c.set(pos - 1, text.refAt(e.pos));
c.set(pos, text.refAt(e.pos));
});
},
c.signal,
Expand All @@ -102,17 +96,17 @@ e.addListener(
e.addListener(
'escape',
() => {
floor.perform(function* (byIno) {
const api = byIno(FIXED_NUM);
floor.perform(function* (t) {
const api = t.byIno(FIXED_NUM);

// this is a fairly basic transaction loop; if something changes, re-run
for (;;) {
if (api.length() === 0) {
api.splice(0, 0, 0);
api.dataUpdate(0, 0, 0);
}
const last = api.at(0);
const last = api.at(0); // we just inserted ourself
const update = typeof last === 'number' ? last + 1 : 1;
api.set(0, update);
api.set(1, update);
console.warn('! setting num', update);
// TODO: we're not announcing this in the client anywhere

Expand All @@ -124,7 +118,7 @@ e.addListener(
);

e.prompt = '> ';
e.text = model.asString();
e.text = model.string();

const conn = connectToPulse(c.signal, {
remote: 'http://localhost:8080/demo',
Expand Down Expand Up @@ -170,7 +164,7 @@ for (;;) {
floor.update(pack);

// TODO: we assume FIXED_STRING changes (not always true)
e.text = model.asString();
e.text = model.string();
reconcileOtherCursors();

e.pos = floor.resolveRef(cursor);
Expand Down
154 changes: 95 additions & 59 deletions client/lib/gumnut/floor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
type WirePatch,
} from '../ymodel/types.ts';
import { emptySymbol, InoPosRef, InoPosRefImpl, type GumnutData } from './types.ts';
import { GumnutInternString, internString } from './intern.ts';
import type { PatchApi } from '../ymodel/work-holder.ts';

type WireType = {
Expand All @@ -20,8 +19,11 @@ type WireType = {
};

class GumnutFloorApi extends GumnutLow<GumnutData, WireType> {
private encodeGumnutData(lookupId: (inode: string, id: Id) => number, data: GumnutData): any {
private encodeGumnutData(lookupId: (inode: string, id: Id) => number, data: GumnutData) {
switch (typeof data) {
case 'string':
return { s: data };

case 'bigint':
return { p: `b:${data.toString()}` };

Expand Down Expand Up @@ -56,9 +58,7 @@ class GumnutFloorApi extends GumnutLow<GumnutData, WireType> {
throw new Error(`unexpected data: ${JSON.stringify(data)}`);
}

if (data instanceof GumnutInternString) {
return { s: data.s };
} else if (data instanceof InoPosRefImpl) {
if (data instanceof InoPosRefImpl) {
// we need to deref the ID here - on the way up to the server
const pos = lookupId(data.target, data.id);
return { s: data.target, t: pos };
Expand Down Expand Up @@ -110,7 +110,7 @@ class GumnutFloorApi extends GumnutLow<GumnutData, WireType> {
const id = resolvePos(raw.s, raw.t);
return new InoPosRefImpl(raw.s, id);
}
return internString(raw.s);
return raw.s;
}

throw new Error(`unexpected raw: ${JSON.stringify(raw)}`);
Expand Down Expand Up @@ -197,16 +197,16 @@ export class GumnutFloor {
length() {
return internal.length();
},
at(at: number): FloorData {
const int = internal.readData(at, at + 1)[0];
if (int === emptySymbol) {
return undefined;
} else if (int instanceof GumnutInternString) {
return int.s;
at(index: number) {
if (index < 0) {
index = internal.length() + index;
}
return int;
return internal.readData(index, index + 1)[0];
},
asString(start, end) {
data(start, end) {
return internal.readData(start, end);
},
string(start, end) {
return internal.readString(start, end);
},
refAt(at: number): InoPosRef {
Expand All @@ -216,61 +216,51 @@ export class GumnutFloor {
};
}

resolveRef(r: InoPosRef) {
const impl = r as InoPosRefImpl;
return this.low.byIno(impl.target).posOf(impl.id);
}

/**
* Builds a {@link FloorApi} from the internal {@link PatchApi}.
*
* This just smooths over internal string/internal stuff.
* TODO: can we not?
*/
private toFloorApi(ino: string, internal: PatchApi<GumnutData>): FloorApi {
private writeForPatchApi(ino: string, internal: PatchApi<GumnutData>): FloorApi {
return {
length() {
return internal.length();
},
at(at: number): FloorData {
const int = internal.readData(at, at + 1)[0];
if (int === emptySymbol) {
return undefined;
} else if (int instanceof GumnutInternString) {
return int.s;
at(index: number) {
if (index < 0) {
index = internal.length() + index;
}
return int;
return internal.readData(index, index + 1)[0];
},
data(start, end) {
return internal.readData(start, end);
},
asString(start, end) {
string(start, end) {
return internal.readString(start, end);
},
refAt(at: number): InoPosRef {
const id = internal.posToId(at);
return new InoPosRefImpl(ino, id);
},
set(at: number, ...data: FloorData[]): void {
const before = at + data.length;

const int = data.map((v): GumnutData => {
if (typeof v === 'string') {
return internString(v);
}
return v;
});
if (int.length) {
internal.applySet({ before, body: int });
}
// TODO: above this is the same as byIno

set(before: number, ...data: GumnutData[]): void {
internal.applySet({ before, body: data });
},
splice(at: number, deleteCount: number, ...data: FloorData[]): void {

dataUpdate(at, deleteCount, ...data) {
if (deleteCount > 0) {
internal.applyOp({ r: [at, at + deleteCount] });
}
if (data.length) {
internal.applyOp({ r: [at], length: data.length });
this.set(at, ...data);
internal.applySet({ before: at + data.length, body: data });
}
},
updateString(at, deleteCount, str) {

stringUpdate(at, deleteCount, str) {
if (deleteCount > 0) {
internal.applyOp({ r: [at, at + deleteCount] });
}
Expand All @@ -282,37 +272,83 @@ export class GumnutFloor {
};
}

resolveRef(r: InoPosRef) {
const impl = r as InoPosRefImpl;
return this.low.byIno(impl.target).posOf(impl.id);
}

perform(fn: FloorFn) {
const toFloorApi = this.toFloorApi.bind(this);
const writeForPatchApi = this.writeForPatchApi.bind(this);

this.low.perform(function* (byIno) {
yield* fn(
(ino) => toFloorApi(ino, byIno(ino)),
(r: InoPosRef) => {
const api: FloorInoApi = {
byIno(ino) {
const int = byIno(ino);
return writeForPatchApi(ino, int);
},
resolveRef(r) {
const impl = r as InoPosRefImpl;
return byIno(impl.target).posOf(impl.id);
},
);
};
yield* fn(api);
});
this.announceCh.push(true);
}
}

export type FloorFn = (
byIno: (ino: string) => FloorApi,
resolveRef: (r: InoPosRef) => number,
) => Generator<Iterable<string>, void, void>;
export type FloorInoApi = {
byIno(ino: string): FloorApi;
resolveRef(r: InoPosRef): number;
};

export type FloorFn = (api: FloorInoApi) => Generator<Iterable<string>, void, void>;

export type FloorReadApi = {
at(at: number): FloorData;
asString(start?: number, end?: number): string;
refAt(at: number): InoPosRef;
/**
* Returns the length of this inode.
*/
length(): number;

/**
* Returns the data at a given position, with {@link Array.at}-like semantics.
*/
at(index: number): GumnutData;

/**
* Reads the range of data here as {@link GumnutData}.
*/
data(start?: number, end?: number): GumnutData[];

/**
* Reads the range of data here as a string.
*/
string(start?: number, end?: number): string;

/**
* Converts this index to a {@link InoPosRef}.
*
* This index must be in the range `[0,length]`, and this will throw otherwise.
* If this is to track an _item_, you should use the index after that item, as it will be consistent over time.
*/
refAt(at: number): InoPosRef;
};

export type FloorApi = FloorReadApi & {
set(at: number, ...data: FloorData[]): void;
splice(at: number, deleteCount: number, ...data: FloorData[]): void;
updateString(at: number, deleteCount: number, str: string): void;
};
/**
* Set replaces data _before_ the given position (1-indexed).
*
* To replace index 0, call `.set(1, ...)`.
*/
set(before: number, ...data: GumnutData[]): void;

export type FloorData = null | undefined | boolean | number | bigint | string | InoPosRef;
/**
* Acts like 'splice', merging delete/insert operations for data.
*/
dataUpdate(at: number, deleteCount: number, ...data: GumnutData[]): void;

/**
* Acts like 'splice', merging delete/insert operations for string data.
*/
stringUpdate(at: number, deleteCount: number, str: string): void;
};
8 changes: 0 additions & 8 deletions client/lib/gumnut/intern.test.ts

This file was deleted.

Loading
Loading