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
15 changes: 7 additions & 8 deletions client/lib/gumnut/floor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,13 @@ type WireType = {
class GumnutFloorApi extends GumnutLow<GumnutData, WireType> {
private encodeGumnutData(lookupId: (inode: string, id: Id) => number, data: GumnutData) {
switch (typeof data) {
case 'string':
return { s: data };
case 'string': // intern string
case 'boolean': // true/false
return data;

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

case 'boolean':
return data;

case 'number':
if (!isFinite(data)) {
if (data === +Infinity) {
Expand Down Expand Up @@ -71,7 +69,8 @@ class GumnutFloorApi extends GumnutLow<GumnutData, WireType> {

public decodeGumnutData(resolvePos: (inode: string, pos: number) => Id, raw: any): GumnutData {
switch (typeof raw) {
case 'boolean':
case 'string': // intern string
case 'boolean': // true/false
case 'number':
return raw;

Expand Down Expand Up @@ -247,7 +246,7 @@ export class GumnutFloor {
// TODO: above this is the same as byIno

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

dataUpdate(at, deleteCount, ...data) {
Expand All @@ -256,7 +255,7 @@ export class GumnutFloor {
}
if (data.length) {
internal.applyOp({ r: [at], length: data.length });
internal.applySet({ before: at + data.length, body: data });
internal.applySet({ before: at + data.length, body: [data] });
}
},

Expand Down
14 changes: 7 additions & 7 deletions client/lib/ymodel/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ test('api', () => {
api.applyOp({ r: [0], length: 7 });

const id = api.posToId(4);
const xxSet = api.applySet({ before: api.posOf(id), body: 'XX' });
const xxSet = api.applySet({ before: api.posOf(id), body: ['XX'] });

assert.strictEqual(api.length(), 7);
assert.deepStrictEqual(api.readString(), '\ufffd\ufffdXX\ufffd\ufffd\ufffd');
Expand All @@ -54,7 +54,7 @@ test('api', () => {
yield ['abc'];

xxSet.rollback();
api.applySet({ before: api.posOf(id), body: 'YY' });
api.applySet({ before: api.posOf(id), body: ['YY'] });

++calls;
});
Expand All @@ -68,7 +68,7 @@ test('api', () => {
update: {
abc: {
run: [{ r: [0], length: 1 }],
set: [{ before: 1, body: [999] }],
set: [{ before: 1, body: [[999]] }],
},
},
});
Expand All @@ -84,13 +84,13 @@ test('prepareForServer', () => {
l.perform(function* (byIno) {
const api = byIno('abc');
api.applyOp({ r: [0], length: 2 });
api.applySet({ before: 2, body: 'XX' });
api.applySet({ before: 2, body: ['XX'] });
});

l.perform(function* (byIno) {
const api = byIno('abc');
api.applyOp({ r: [1], length: 1 });
api.applySet({ before: 2, body: 'x' });
api.applySet({ before: 2, body: ['x'] });
});

const outer = l.byIno('abc');
Expand All @@ -106,7 +106,7 @@ test('prepareForServer', () => {
update: {
abc: {
run: [{ r: [0], length: 2 }],
set: [{ before: -1, body: 'XX' }],
set: [{ before: -1, body: ['XX'] }],
},
},
},
Expand All @@ -118,7 +118,7 @@ test('prepareForServer', () => {
update: {
abc: {
run: [{ r: [-2], length: 1 }],
set: [{ before: -1, body: 'x' }],
set: [{ before: -1, body: ['x'] }],
},
},
},
Expand Down
6 changes: 3 additions & 3 deletions client/lib/ymodel/internal/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ test('state', () => {
const r = RopeState.new(0);

r.applyOp(r.coerceOp({ r: [0], length: 5 }));
r.applySet(r.coerceSet({ before: 5, body: [100, 1, 2] }));
r.applySet(r.coerceSet({ before: 5, body: [[100, 1, 2]] }));
assert.deepStrictEqual(r.readData(), [0, 0, 100, 1, 2]);

r.applySet(r.coerceSet({ before: 3, body: 'abc' }));
r.applySet(r.coerceSet({ before: 3, body: ['abc'] }));

assert.strictEqual(r.length(), 5);
assert.deepStrictEqual(r.readString(), 'abc\ufffd\ufffd');
assert.deepStrictEqual(r.readData(), [0, 0, 0, 1, 2]);

r.applySet(r.coerceSet({ before: 4, body: [999] }));
r.applySet(r.coerceSet({ before: 4, body: [[999]] }));
assert.deepStrictEqual(r.readData(), [0, 0, 0, 999, 2]);
});
72 changes: 52 additions & 20 deletions client/lib/ymodel/internal/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,10 @@ export class RopeState<X> {
}

public coerceSet(set: SetPart<X>): InternalSet<X> {
const length = set.body.length;
let length = 0;
for (const part of set.body) {
length += part.length;
}
const start = set.before - length;

// TODO: right now, InteralSet splays out every entry or char with a target ID!
Expand All @@ -303,17 +306,16 @@ export class RopeState<X> {
return out;
}

public applySet(int: InternalSet<X>): boolean {
if (int.ids.length === 0) {
return false;
}

/**
* Applies a single chunk of a {@link InteralSet}, i.e., a string or array chunk.
*/
private internalApplySet(chunk: InternalSetChunk<X>): boolean {
let i = 0;

// fast-path array inserts
if (typeof int.data !== 'string') {
while (i < int.ids.length) {
let id = int.ids[i];
if (typeof chunk.data !== 'string') {
while (i < chunk.ids.length) {
let id = chunk.ids[i];

const hostId = this.tree.equalAfter(id);
if (hostId === undefined) {
Expand All @@ -326,39 +328,39 @@ export class RopeState<X> {
}

const index = e.length - 1 - (hostId - id);
e.data[index] = int.data[i];
e.data[index] = chunk.data[i];
++i;
}

if (i === int.ids.length) {
if (i === chunk.ids.length) {
return true;
}
}

// consume as much contiguous data and set, rinse and repeat
let lastId = this.leftOf(int.ids[0]);
let lastId = this.leftOf(chunk.ids[0]);
this.ensureIdEdge(lastId);

while (i < int.ids.length) {
let id = int.ids[i];
while (i < chunk.ids.length) {
let id = chunk.ids[i];
const lo = i;

while (++i < int.ids.length) {
const nextId = int.ids[i];
while (++i < chunk.ids.length) {
const nextId = chunk.ids[i];
if (this.leftOf(nextId) !== id) {
break;
}
id = nextId;
}

const length = i - lo;
const data = int.data.slice(lo, i);
const localData = chunk.data.slice(lo, i);

this.ensureIdEdge(id);
const info = this.rope.lookup(id);

if (info.length === length) {
this.rope.adjust(id, data, length);
this.rope.adjust(id, localData, length);
} else {
// remove all tree entries >lastId <id
let candId = lastId;
Expand All @@ -370,7 +372,7 @@ export class RopeState<X> {
this.tree.remove(candId);
}
this.rope.deleteTo(lastId, id);
this.rope.insertAfter(lastId, id, length, data);
this.rope.insertAfter(lastId, id, length, localData);
}

lastId = id;
Expand All @@ -379,6 +381,18 @@ export class RopeState<X> {
return true;
}

public applySet(int: InternalSet<X>): boolean {
let any = false;

// consume chunks (typically just one!)
const chunks = chunkInternalSet(int);
for (const chunk of chunks) {
const change = this.internalApplySet(chunk);
any ||= change;
}
return any;
}

/**
* Finds the {@link Id} directly to the left of the one passed.
*
Expand Down Expand Up @@ -409,7 +423,7 @@ export class RopeState<X> {

export type InternalOp = { op: Op; id: Id };

export type InternalSet<X> = { ids: Id[]; data: X[] | string };
export type InternalSet<X> = { ids: Id[]; data: (X[] | string)[] };

export type InternalPatch<X> = {
run: InternalOp[];
Expand All @@ -427,3 +441,21 @@ export function rollbackForOp(int: InternalOp): InternalOp | undefined {
};
}
}

export type InternalSetChunk<X> = { ids: Id[]; data: string | X[] };

export function chunkInternalSet<X>(int: InternalSet<X>): InternalSetChunk<X>[] {
// consume chunks (typically just one!)
let remainingIds = int.ids;
const chunks: InternalSetChunk<X>[] = [];

for (let i = 0; i < int.data.length; ++i) {
const data = int.data[i];
const ids = remainingIds.slice(0, data.length);
remainingIds = remainingIds.slice(data.length);

chunks.push({ ids, data });
}

return chunks;
}
4 changes: 2 additions & 2 deletions client/lib/ymodel/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ test('encodePatch', () => {
const wp = encodePatch(p, (x) => x);
assert.deepStrictEqual(wp, {
r: [-2, 5],
s: [1234, ['hello 🗺️']],
s: [1234, 'hello 🗺️'],
});
});

test('decodePatch', () => {
const wp: WirePatch = {
r: [-2, 5],
s: [1234, ['hello 🗺️']],
s: [1234, 'hello 🗺️'],
};

const p = decodePatch(wp, (x) => x as string);
Expand Down
49 changes: 26 additions & 23 deletions client/lib/ymodel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,16 @@ export function decodeOpRun(run: number[]): Op[] {

export type SetPart<X> = {
before: number;
body: X[] | string;
body: (X[] | string)[];
};

export type NotString<T> = T extends string ? never : T;

/**
* Encodes this set op.
*/
export function encodeSetOp<X, O>(parts: SetPart<X>[], encode: (x: X) => NotString<O>): any[] {
export function encodeSetOp<X>(parts: SetPart<X>[], encode: (x: X) => any): any[] {
const out: any[] = [];
for (const part of parts) {
if (typeof part.body === 'string') {
out.push(part.before, part.body);
} else {
out.push(
part.before,
part.body.map((p) => encode(p)),
);
}
out.push(part.before, ...part.body.map((p) => (typeof p === 'string' ? p : p.map(encode))));
}
return out;
}
Expand All @@ -74,18 +65,30 @@ export function encodeSetOp<X, O>(parts: SetPart<X>[], encode: (x: X) => NotStri
*/
export function decodeSetOp<X>(parts: any[], decode: (raw: any) => X = (x) => x): SetPart<X>[] {
const out: SetPart<X>[] = [];
if (parts.length % 2 !== 0) {
throw new Error('model encoding error');
}
for (let i = 0; i < parts.length; i += 2) {

let i = 0;
while (i < parts.length) {
const before = parts[i] as number;
const raw = parts[i + 1];
if (typeof before !== 'number' || Math.floor(before) !== before) {
throw new Error(`can't decode, bad set: ${JSON.stringify(parts)}`);
}
++i;

if (typeof raw === 'string') {
out.push({ before, body: raw });
} else {
const body = (raw as any[]).map((p) => decode(p));
out.push({ before, body });
const part: SetPart<X> = { before, body: [] };
out.push(part);

// decode any number of array/string parts
while (i < parts.length && typeof parts[i] !== 'number') {
const next = parts[i];
++i;

if (typeof next === 'string') {
part.body.push(next);
} else if (Array.isArray(next)) {
part.body.push(next.map(decode));
} else {
throw new Error(`bad set part`);
}
}
}
return out;
Expand All @@ -106,7 +109,7 @@ export type WirePatch = {
s?: any[];
};

export function encodePatch<X, O>(patch: Patch<X>, encode: (x: X) => NotString<O>): WirePatch {
export function encodePatch<X, O>(patch: Patch<X>, encode: (x: X) => any): WirePatch {
const run = encodeOpRun(patch.run);
const set = encodeSetOp(patch.set, encode);
const out: WirePatch = {};
Expand Down
Loading
Loading