Skip to content

Commit

Permalink
Add awareness
Browse files Browse the repository at this point in the history
  • Loading branch information
zjkmxy committed Feb 1, 2024
1 parent c987c32 commit 2db853d
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 30 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ucla-irl/ndnts-aux",
"version": "1.0.10",
"version": "1.0.11-rc1",
"description": "NDNts Auxiliary Package for Web and Deno",
"scripts": {
"test": "deno test --no-check",
Expand All @@ -22,6 +22,7 @@
"eventemitter3": "^5.0.1",
"jose": "^5.2.0",
"tslib": "^2.6.2",
"y-protocols": "^1.0.6",
"yjs": "^13.6.11"
},
"devDependencies": {
Expand Down
37 changes: 25 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 80 additions & 1 deletion src/adaptors/yjs-ndn-adaptor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SyncAgent } from '../sync-agent/mod.ts';
import * as Y from 'yjs';
import { Awareness } from 'y-protocols/awareness.js';

/**
* NDN SVS Provider for Yjs. Wraps update into `SyncAgent`'s `update` channel.
Expand All @@ -13,6 +14,14 @@ import * as Y from 'yjs';
*/
export class NdnSvsAdaptor {
private readonly callback = this.docUpdateHandler.bind(this);
private readonly awarenessCallback = this.awarenessUpdateHandler.bind(this);

#awarenessDocId: string | undefined;
#awareness: Awareness | undefined;
// Required as a Yjs provider
public get awareness() {
return this.#awareness;
}

constructor(
public syncAgent: SyncAgent,
Expand All @@ -23,9 +32,24 @@ export class NdnSvsAdaptor {
doc.on('update', this.callback);
}

public bindAwareness(subDoc: Y.Doc, docId: string) {
this.#awarenessDocId = docId;
this.#awareness = new Awareness(subDoc); // TODO: Should we do so?
this.#awareness.on('update', this.awarenessCallback);
this.syncAgent.register('status', this.topic, (content) => this.handleAwarenessUpdate(content));
}

public cancelAwareness() {
this.#awarenessDocId = undefined;
this.syncAgent.unregister('status', this.topic);
this.#awareness?.off('update', this.awarenessCallback);
this.#awareness?.destroy();
}

public destroy() {
this.syncAgent.unregister('update', this.topic);
this.cancelAwareness();
this.doc.off('update', this.callback);
this.syncAgent.unregister('update', this.topic);
}

private docUpdateHandler(update: Uint8Array, origin: undefined) {
Expand All @@ -34,6 +58,25 @@ export class NdnSvsAdaptor {
}
}

private awarenessUpdateHandler(_updatedClients: {
added: number[];
updated: number[];
removed: number[];
}, origin: unknown | 'local') {
if (origin !== 'local') {
// Only capture local updates
return;
}
// Capture and publish local state
const localState = this.#awareness!.getLocalState();
const encodedState = JSON.stringify({
clientId: this.#awareness!.clientID,
docId: this.#awarenessDocId,
state: localState,
});
this.syncAgent.publishStatus(this.topic, new TextEncoder().encode(encodedState));
}

private async produce(content: Uint8Array) {
await this.syncAgent.publishUpdate(this.topic, content);
}
Expand All @@ -49,4 +92,40 @@ export class NdnSvsAdaptor {
// https://github.com/yjs/yjs/blob/fe36ffd122a6f2384293098afd52d2c0025fce2a/src/utils/Transaction.js#L415-L426
Y.applyUpdate(this.doc, content, this);
}

public handleAwarenessUpdate(content: Uint8Array) {
const encodedState = JSON.parse(new TextDecoder().decode(content));
const { clientId, docId, state } = encodedState as {
clientId: number;
docId: string;
state: Record<string, unknown> | null;
};
const awareness = this.#awareness;
if (!awareness) {
return;
}

const prevMeta = awareness.meta.get(clientId);
let op: 'added' | 'updated' | 'removed';
if (prevMeta === undefined && docId === this.#awarenessDocId && state !== null) {
op = 'added';
} else if (prevMeta !== undefined && (docId !== this.#awarenessDocId || state === null)) {
op = 'removed';
} else if (docId === this.#awarenessDocId) {
op = 'updated';
} else {
return;
}

if (op === 'removed' || !state) {
awareness.states.delete(clientId);
} else {
awareness.states.set(clientId, state);
}
awareness.meta.set(clientId, {
clock: 0,
lastUpdated: Date.now(),
});
awareness.emit('change', [{ added: [], updated: [], removed: [], [op]: [clientId] }, this]);
}
}
22 changes: 12 additions & 10 deletions src/namespace/name-pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type PatternComponent = {

export type Pattern = Array<PatternComponent | Component>;

export type Mapping = Record<string, MatchValue>;

export const patternComponentToString = (comp: PatternComponent) => `<${comp.type}=${comp.tag}:${comp.kind}>`;

export const componentToString = (comp: PatternComponent | Component) =>
Expand All @@ -30,7 +32,7 @@ export const toString = (pat: Pattern) => '/' + pat.map(componentToString).join(
export const matchStep = (
pattern: PatternComponent | Component,
subject: Component,
mapping: Record<string, MatchValue>,
mapping: Mapping,
) => {
if (pattern.type !== subject.type) {
return false;
Expand All @@ -41,7 +43,7 @@ export const matchStep = (
mapping[pattern.tag] = subject.value;
return true;
} else if (pattern.kind === 'string') {
mapping[pattern.tag] = new TextDecoder().decode(subject.value);
mapping[pattern.tag] = subject.text;
return true;
} else {
try {
Expand All @@ -66,7 +68,7 @@ export const matchStep = (
export const match = (
pattern: Pattern,
subject: Name,
mapping: Record<string, MatchValue>,
mapping: Mapping,
) => {
// Remove automatically added component
// ImplicitSha256DigestComponent(0x01) and ParametersSha256DigestComponent(0x02)
Expand All @@ -87,7 +89,7 @@ export const match = (

export const makeStep = (
pattern: PatternComponent | Component,
mapping: Record<string, MatchValue>,
mapping: Mapping,
) => {
if (pattern instanceof Component) {
return pattern;
Expand All @@ -112,10 +114,10 @@ export const makeStep = (
*/
export const make = (
pattern: Pattern,
mapping: Record<string, MatchValue>,
mapping: Mapping,
) => new Name(pattern.map((p) => makeStep(p, mapping)));

export const componentFromString = (value: string) => {
export const componentFromString = (value: string): Component | PatternComponent => {
if (value.length === 0) {
return new Component();
}
Expand All @@ -128,17 +130,17 @@ export const componentFromString = (value: string) => {
}
return {
type: parseInt(matching.groups.type),
kind: matching.groups.kind as PatternKind,
kind: matching.groups.kind as PatternKind, // Assume correct, no check
tag: matching.groups.tag,
} as PatternComponent;
};
}
};

export const fromString = (value: string) => {
export const fromString = (value: string): Pattern => {
if (value[0] === '/') {
value = value.substring(1);
}
return value.split('/').map(componentFromString) as Pattern;
return value.split('/').map(componentFromString);
};

export const pattern = ([value]: TemplateStringsArray) => {
Expand Down
Loading

0 comments on commit 2db853d

Please sign in to comment.