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
13 changes: 11 additions & 2 deletions client/bin/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,18 @@ for (;;) {
const cursor = floor.byIno(FIXED_STRING).refAt(e.pos);
const anchor = floor.byIno(FIXED_STRING).refAt(e.anchor);

floor.update(pack);
const changes = floor.update(pack);
if (!(changes.has(FIXED_STRING) || changes.has(FIXED_CURSORS))) {
continue;
}

// TODO: it's probably faster to just read the string again, but this 'proves' the change works
const stringChange = changes.get(FIXED_STRING);
if (stringChange) {
const patch = model.string(stringChange.lo, stringChange.lo + stringChange.length);
e.text = e.text.substring(0, stringChange.lo) + patch + e.text.substring(stringChange.hi);
}

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

Expand Down
50 changes: 50 additions & 0 deletions client/lib/gumnut/floor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import test from 'node:test';
import * as assert from 'node:assert';
import { GumnutFloor } from './floor.ts';

test('floor', () => {
const f = new GumnutFloor();

const changes1 = f.update({
p: {
def: {
r: [-5, 0],
s: [5, 'hello'],
},
},
});
assert.deepStrictEqual(f.byIno('def').string(), 'hello');
assert.deepStrictEqual(changes1, new Map([['def', { lo: 0, hi: 0, length: 5 }]]));

const changes2 = f.perform(function* (t) {
const node = t.byIno('def');
node.stringUpdate(3, 2, 'ls to the yeah');
});
assert.deepStrictEqual(f.byIno('def').string(), 'hells to the yeah');
assert.deepStrictEqual(changes2, new Map([['def', { lo: 3, hi: 5, length: 14 }]]));

const changes3 = f.update({
p: {
def: {
r: [0, 2, 5], // delete "llo"
s: [2, 'i'],
},
},
});
assert.deepStrictEqual(f.byIno('def').string(), 'hi');
assert.deepStrictEqual(changes3, new Map([['def', { lo: 1, hi: 17, length: 1 }]]));
});

test('floor local changes', () => {
const f = new GumnutFloor();

const changes1 = f.perform(function* (t) {
t.byIno('abc').dataUpdate(0, 0, 1, 2, 3);
});
assert.deepStrictEqual(changes1, new Map([['abc', { lo: 0, hi: 0, length: 3 }]]));

const changes2 = f.perform(function* (t) {
t.byIno('abc').set(2, 200);
});
assert.deepStrictEqual(changes2, new Map([['abc', { lo: 1, hi: 2, length: 1 }]]));
});
73 changes: 60 additions & 13 deletions client/lib/gumnut/floor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '../ymodel/types.ts';
import { emptySymbol, InoPosRef, InoPosRefImpl, type GumnutData } from './types.ts';
import type { PatchApi } from '../ymodel/work-holder.ts';
import type { RangeUpdate } from '../ymodel/internal/range.ts';

type WireType = {
b?: string[];
Expand Down Expand Up @@ -147,25 +148,66 @@ class GumnutFloorApi extends GumnutLow<GumnutData, WireType> {
}
}

export type InoChanges = ReadonlyMap<string, Readonly<RangeUpdate>>;

export class GumnutFloor {
private readonly low = new GumnutFloorApi();
private readonly announceCh = newChannel<true>();

private readonly listeners = new Set<(x: InoChanges) => void>();

/**
* Listen to all changes here.
*/
addListener(signal: AbortSignal, listener: (x: InoChanges) => void) {
if (signal.aborted) {
return;
}

if (this.listeners.has(listener)) {
const orig = listener;
listener = (x) => orig(x);
}

this.listeners.add(listener);
signal.addEventListener('abort', () => this.listeners.delete(listener));
}

private dispatch(changes: InoChanges) {
this.listeners.forEach((l) => {
try {
l(changes);
} catch (e) {
Promise.resolve().then(() => {
throw e;
});
}
});
}

/**
* An update arrived from the server.
*/
update(w: WireType) {
update(w: WireType): InoChanges {
if (w.b) {
throw new Error(`panic; can't have barrier in incoming update`);
}
this.low.applyServer(w);
const updates = this.low.applyServer(w);
this.dispatch(updates);
return updates;
}

/**
* Performs an ack to this sequence number.
*/
ackToSeq(seq: number) {
this.low.ackToSeq(seq);

// by acking, we might have unlocked a barrier op
for (const _ of this.low.pendingSeq()) {
this.announceCh.push(true);
break;
}
}

/**
Expand All @@ -189,6 +231,9 @@ export class GumnutFloor {
return p.size ? p : undefined;
}

/**
* Returns a read-only {@link FloorReadApi} for the given ino.
*/
byIno(ino: string): FloorReadApi {
const internal = this.low.byIno(ino);

Expand Down Expand Up @@ -217,11 +262,8 @@ export class GumnutFloor {

/**
* Builds a {@link FloorApi} from the internal {@link PatchApi}.
*
* This just smooths over internal string/internal stuff.
* TODO: can we not?
*/
private writeForPatchApi(ino: string, internal: PatchApi<GumnutData>): FloorApi {
private writeForPatchApi(ino: string, internal: PatchApi<GumnutData>): FloorPerformApi {
return {
length() {
return internal.length();
Expand Down Expand Up @@ -276,10 +318,10 @@ export class GumnutFloor {
return this.low.byIno(impl.target).posOf(impl.id);
}

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

this.low.perform(function* (byIno) {
const updates = this.low.perform(function* (byIno) {
const api: FloorInoApi = {
byIno(ino) {
const int = byIno(ino);
Expand All @@ -293,17 +335,19 @@ export class GumnutFloor {
yield* fn(api);
});
this.announceCh.push(true);
this.dispatch(updates);
return updates;
}
}

export type FloorInoApi = {
byIno(ino: string): FloorApi;
byIno(ino: string): FloorPerformApi;
resolveRef(r: InoPosRef): number;
};

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

export type FloorReadApi = {
export interface FloorReadApi {
/**
* Returns the length of this inode.
*/
Expand Down Expand Up @@ -331,9 +375,12 @@ export type FloorReadApi = {
* 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 & {
/**
* This is a work/transactional API.
*/
export interface FloorPerformApi extends FloorReadApi {
/**
* Set replaces data _before_ the given position (1-indexed).
*
Expand All @@ -350,4 +397,4 @@ export type FloorApi = FloorReadApi & {
* Acts like 'splice', merging delete/insert operations for string data.
*/
stringUpdate(at: number, deleteCount: number, str: string): void;
};
}
Loading
Loading