WIP moving to novy
This commit is contained in:
parent
e0957dc25d
commit
c6fff70bae
6
Makefile
6
Makefile
|
@ -21,3 +21,9 @@ all:
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
inotifytest make -j$$(nproc) all
|
inotifytest make -j$$(nproc) all
|
||||||
|
|
||||||
|
pull-protocols:
|
||||||
|
git subtree pull -P packages/core/protocols \
|
||||||
|
-m 'Merge latest changes from the syndicate-protocols repository' \
|
||||||
|
git@git.syndicate-lang.org:syndicate-lang/syndicate-protocols \
|
||||||
|
main
|
||||||
|
|
|
@ -0,0 +1,309 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
||||||
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { IdentitySet, Value } from '@preserves/core';
|
||||||
|
import { Attenuation, runRewrites } from './rewrite.js';
|
||||||
|
import { queueTask } from './task.js';
|
||||||
|
|
||||||
|
export type AnyValue = Value<Ref>;
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if ('stackTraceLimit' in Error) {
|
||||||
|
Error.stackTraceLimit = Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Assertion = Value<Ref>;
|
||||||
|
export type Handle = number;
|
||||||
|
export type ExitReason = null | { ok: true } | { ok: false, err: Error };
|
||||||
|
export type LocalAction = (t: Turn) => void;
|
||||||
|
|
||||||
|
export interface Entity {
|
||||||
|
assert(turn: Turn, assertion: Assertion, handle: Handle): void;
|
||||||
|
retract(turn: Turn, handle: Handle): void;
|
||||||
|
message(turn: Turn, body: Assertion): void;
|
||||||
|
sync(turn: Turn, peer: Ref): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Cap = Ref;
|
||||||
|
|
||||||
|
export interface Ref {
|
||||||
|
readonly relay: Facet;
|
||||||
|
readonly target: Partial<Entity>;
|
||||||
|
readonly attenuation?: Attenuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function isRef(v: any): v is Ref {
|
||||||
|
return 'relay' in v && v.relay instanceof Facet && 'target' in v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRef(_v: any): Ref | undefined {
|
||||||
|
return isRef(_v) ? _v : void 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundAssertion = { handle: Handle, peer: Ref, established: boolean };
|
||||||
|
type OutboundMap = Map<Handle, OutboundAssertion>;
|
||||||
|
|
||||||
|
let nextActorId = 0;
|
||||||
|
export const __setNextActorId = (v: number) => nextActorId = v;
|
||||||
|
|
||||||
|
export class Actor {
|
||||||
|
readonly id = nextActorId++;
|
||||||
|
readonly root: Facet;
|
||||||
|
exitReason: ExitReason = null;
|
||||||
|
readonly exitHooks: Array<LocalAction> = [];
|
||||||
|
|
||||||
|
constructor(bootProc: LocalAction, initialAssertions: OutboundMap = new Map()) {
|
||||||
|
this.root = new Facet(this, null, initialAssertions);
|
||||||
|
Turn.for(new Facet(this, this.root), stopIfInertAfter(bootProc));
|
||||||
|
}
|
||||||
|
|
||||||
|
atExit(a: LocalAction): void {
|
||||||
|
this.exitHooks.push(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminateWith(t: Turn, reason: Exclude<ExitReason, null>) {
|
||||||
|
if (this.exitReason !== null) return;
|
||||||
|
this.exitReason = reason;
|
||||||
|
if (!reason.ok) {
|
||||||
|
console.error(`Actor ${this.id} crashed:`, reason.err);
|
||||||
|
}
|
||||||
|
this.exitHooks.forEach(hook => hook(t));
|
||||||
|
queueTask(() => Turn.for(this.root, t => this.root._terminate(t, reason.ok), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Facet {
|
||||||
|
readonly id = nextActorId++;
|
||||||
|
readonly actor: Actor;
|
||||||
|
readonly parent: Facet | null;
|
||||||
|
readonly children = new Set<Facet>();
|
||||||
|
readonly outbound: OutboundMap;
|
||||||
|
readonly shutdownActions: Array<LocalAction> = [];
|
||||||
|
// ^ shutdownActions are not exitHooks - those run even on error. These are for clean shutdown
|
||||||
|
isLive = true;
|
||||||
|
inertCheckPreventers = 0;
|
||||||
|
|
||||||
|
constructor(actor: Actor, parent: Facet | null, initialAssertions: OutboundMap = new Map()) {
|
||||||
|
this.actor = actor;
|
||||||
|
this.parent = parent;
|
||||||
|
if (parent) parent.children.add(this);
|
||||||
|
this.outbound = initialAssertions;
|
||||||
|
}
|
||||||
|
|
||||||
|
onStop(a: LocalAction): void {
|
||||||
|
this.shutdownActions.push(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
isInert(): boolean {
|
||||||
|
return this.children.size === 0 && this.outbound.size === 0 && this.inertCheckPreventers === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
preventInertCheck(): () => void {
|
||||||
|
let armed = true;
|
||||||
|
this.inertCheckPreventers++;
|
||||||
|
return () => {
|
||||||
|
if (!armed) return;
|
||||||
|
armed = false;
|
||||||
|
this.inertCheckPreventers--;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_terminate(t: Turn, orderly: boolean): void {
|
||||||
|
if (!this.isLive) return;
|
||||||
|
this.isLive = false;
|
||||||
|
|
||||||
|
const parent = this.parent;
|
||||||
|
if (parent) parent.children.delete(this);
|
||||||
|
|
||||||
|
t._inFacet(this, t => {
|
||||||
|
this.children.forEach(child => child._terminate(t, orderly));
|
||||||
|
if (orderly) this.shutdownActions.forEach(a => a(t));
|
||||||
|
this.outbound.forEach(e => t._retract(e));
|
||||||
|
|
||||||
|
if (orderly) {
|
||||||
|
queueTask(() => {
|
||||||
|
if (parent) {
|
||||||
|
if (parent.isInert()) {
|
||||||
|
Turn.for(parent, t => parent._terminate(t, true));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Turn.for(this.actor.root, t => this.actor.terminateWith(t, { ok: true }), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _sync_impl(turn: Turn, e: Partial<Entity>, peer: Ref): void {
|
||||||
|
e.sync ? e.sync!(turn, peer) : turn.message(peer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextHandle = 0;
|
||||||
|
let nextTurnId = 0;
|
||||||
|
|
||||||
|
export class Turn {
|
||||||
|
readonly id = nextTurnId++;
|
||||||
|
readonly activeFacet: Facet;
|
||||||
|
queues: Map<Facet, LocalAction[]> | null;
|
||||||
|
|
||||||
|
static for(facet: Facet, f: LocalAction, zombieTurn = false): void {
|
||||||
|
if (!zombieTurn) {
|
||||||
|
if (facet.actor.exitReason !== null) return;
|
||||||
|
if (!facet.isLive) return;
|
||||||
|
}
|
||||||
|
const t = new Turn(facet);
|
||||||
|
try {
|
||||||
|
f(t);
|
||||||
|
t.queues!.forEach((q, facet) => queueTask(() => Turn.for(facet, t=> q.forEach(f => f(t)))));
|
||||||
|
t.queues = null;
|
||||||
|
} catch (err) {
|
||||||
|
Turn.for(facet.actor.root, t => facet.actor.terminateWith(t, { ok: false, err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(facet: Facet, queues = new Map<Facet, LocalAction[]>()) {
|
||||||
|
this.activeFacet = facet;
|
||||||
|
this.queues = queues;
|
||||||
|
}
|
||||||
|
|
||||||
|
_inFacet(facet: Facet, f: LocalAction): void {
|
||||||
|
const t = new Turn(facet, this.queues!);
|
||||||
|
f(t);
|
||||||
|
t.queues = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref<T extends Partial<Entity>>(e: T): Ref {
|
||||||
|
return { relay: this.activeFacet, target: e };
|
||||||
|
}
|
||||||
|
|
||||||
|
facet(bootProc: LocalAction): Facet {
|
||||||
|
const newFacet = new Facet(this.activeFacet.actor, this.activeFacet);
|
||||||
|
this._inFacet(newFacet, stopIfInertAfter(bootProc));
|
||||||
|
return newFacet;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(facet: Facet = this.activeFacet, continuation?: LocalAction) {
|
||||||
|
this.enqueue(facet.parent!, t => {
|
||||||
|
facet._terminate(t, true);
|
||||||
|
if (continuation) continuation(t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(bootProc: LocalAction, initialAssertions = new IdentitySet<Handle>()): void {
|
||||||
|
this.enqueue(this.activeFacet, () => {
|
||||||
|
const newOutbound: OutboundMap = new Map();
|
||||||
|
initialAssertions.forEach(key => {
|
||||||
|
newOutbound.set(key, this.activeFacet.outbound.get(key)!); // we trust initialAssertions
|
||||||
|
this.activeFacet.outbound.delete(key);
|
||||||
|
});
|
||||||
|
queueTask(() => new Actor(bootProc, newOutbound));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stopActor(): void {
|
||||||
|
this.enqueue(this.activeFacet.actor.root, t => this.activeFacet.actor.terminateWith(t, { ok: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
crash(err: Error): void {
|
||||||
|
this.enqueue(this.activeFacet.actor.root, t => this.activeFacet.actor.terminateWith(t, { ok: false, err }));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(ref: Ref, assertion: Assertion): Handle {
|
||||||
|
const h = nextHandle++;
|
||||||
|
this._assert(ref, assertion, h);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
_assert(ref: Ref, assertion: Assertion, h: Handle) {
|
||||||
|
const a = runRewrites(ref.attenuation, assertion);
|
||||||
|
if (a !== null) {
|
||||||
|
const e = { handle: h, peer: ref, established: false };
|
||||||
|
this.activeFacet.outbound.set(h, e);
|
||||||
|
this.enqueue(ref.relay, t => {
|
||||||
|
e.established = true;
|
||||||
|
ref.target.assert?.(t, a, h);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retract(h: Handle | undefined): void {
|
||||||
|
if (h !== void 0) {
|
||||||
|
const e = this.activeFacet.outbound.get(h);
|
||||||
|
if (e === void 0) return;
|
||||||
|
this._retract(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replace(ref: Ref, h: Handle | undefined, assertion: Assertion | undefined): Handle | undefined {
|
||||||
|
const newHandle = assertion === void 0 ? void 0 : this.assert(ref, assertion);
|
||||||
|
this.retract(h);
|
||||||
|
return newHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
_retract(e: OutboundAssertion): void {
|
||||||
|
this.activeFacet.outbound.delete(e.handle);
|
||||||
|
this.enqueue(e.peer.relay, t => {
|
||||||
|
if (e.established) {
|
||||||
|
e.established = false;
|
||||||
|
e.peer.target.retract?.(t, e.handle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(ref: Ref): Promise<Turn> {
|
||||||
|
return new Promise(resolve => this._sync(ref, this.ref({ message: resolve })));
|
||||||
|
}
|
||||||
|
|
||||||
|
_sync(ref: Ref, peer: Ref): void {
|
||||||
|
this.enqueue(ref.relay, t => _sync_impl(t, ref.target, peer));
|
||||||
|
}
|
||||||
|
|
||||||
|
message(ref: Ref, assertion: Assertion): void {
|
||||||
|
const a = runRewrites(ref.attenuation, assertion);
|
||||||
|
if (a !== null) this.enqueue(ref.relay, t => ref.target.message?.(t, assertion));
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueue(relay: Facet, a: LocalAction): void {
|
||||||
|
if (this.queues === null) {
|
||||||
|
throw new Error("Attempt to reuse a committed Turn");
|
||||||
|
}
|
||||||
|
this.queues.get(relay)?.push(a) ?? this.queues.set(relay, [a]);
|
||||||
|
}
|
||||||
|
|
||||||
|
freshen(a: LocalAction): void {
|
||||||
|
if (this.queues !== null) {
|
||||||
|
throw new Error("Attempt to freshen a non-stale Turn");
|
||||||
|
}
|
||||||
|
Turn.for(this.activeFacet, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopIfInertAfter(a: LocalAction): LocalAction {
|
||||||
|
return t => {
|
||||||
|
a(t);
|
||||||
|
t.enqueue(t.activeFacet, t => {
|
||||||
|
if ((t.activeFacet.parent && !t.activeFacet.parent.isLive) || t.activeFacet.isInert()) {
|
||||||
|
t.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,62 +0,0 @@
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import { Record, RecordConstructor, AsPreserve, Value } from '@preserves/core';
|
|
||||||
|
|
||||||
export class Seal {
|
|
||||||
readonly contents: any;
|
|
||||||
|
|
||||||
constructor(contents: any) {
|
|
||||||
this.contents = contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
[AsPreserve](): any { // should return Value; we are cheating
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Discard extends RecordConstructor<typeof _Discard, {}, []> {
|
|
||||||
_instance: Record<typeof _Discard, []>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _Discard = Symbol.for('discard');
|
|
||||||
export const Discard: Discard = (function () {
|
|
||||||
let Discard: any = Record.makeConstructor<{}>()(_Discard, []);
|
|
||||||
Discard._instance = Discard();
|
|
||||||
return Discard;
|
|
||||||
})();
|
|
||||||
|
|
||||||
export const _Capture = Symbol.for('capture');
|
|
||||||
export const Capture = Record.makeConstructor<{specification: Value}>()(
|
|
||||||
_Capture, ['specification']);
|
|
||||||
|
|
||||||
export const _Observe = Symbol.for('observe');
|
|
||||||
export const Observe = Record.makeConstructor<{specification: Value}>()(
|
|
||||||
_Observe, ['specification']);
|
|
||||||
|
|
||||||
export const _Inbound = Symbol.for('inbound');
|
|
||||||
export const Inbound = Record.makeConstructor<{assertion: Value}>()(
|
|
||||||
_Inbound, ['assertion']);
|
|
||||||
|
|
||||||
export const _Outbound = Symbol.for('outbound');
|
|
||||||
export const Outbound = Record.makeConstructor<{assertion: Value}>()(
|
|
||||||
_Outbound, ['assertion']);
|
|
||||||
|
|
||||||
export const _Instance = Symbol.for('instance');
|
|
||||||
export const Instance = Record.makeConstructor<{uniqueId: Value}>()(
|
|
||||||
_Instance, ['uniqueId']);
|
|
|
@ -0,0 +1,328 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
||||||
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import type { Assertion, Handle, Ref, Turn } from "./actor.js";
|
||||||
|
import { Bytes, Dictionary, DoubleFloat, embed, IdentityMap, is, isEmbedded, Record, SingleFloat, Tuple } from "@preserves/core";
|
||||||
|
import { SturdyValue } from "../transport/sturdy.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Alts,
|
||||||
|
Attenuation,
|
||||||
|
CArr,
|
||||||
|
CDict,
|
||||||
|
CRec,
|
||||||
|
Caveat,
|
||||||
|
ConstructorSpec,
|
||||||
|
Lit,
|
||||||
|
PAnd,
|
||||||
|
PAtom,
|
||||||
|
PBind,
|
||||||
|
PCompound,
|
||||||
|
PCompoundMembers,
|
||||||
|
PDiscard,
|
||||||
|
PNot,
|
||||||
|
PEmbedded,
|
||||||
|
Pattern,
|
||||||
|
Rewrite,
|
||||||
|
TRef,
|
||||||
|
Template,
|
||||||
|
_embedded,
|
||||||
|
} from '../gen/sturdy.js';
|
||||||
|
export * from '../gen/sturdy.js';
|
||||||
|
|
||||||
|
export type Bindings = Array<Assertion>;
|
||||||
|
|
||||||
|
export function match(p: Pattern, v: Assertion): Bindings | null {
|
||||||
|
let bindings: Bindings = [];
|
||||||
|
|
||||||
|
function walk(p: Pattern, v: Assertion): boolean {
|
||||||
|
switch (p._variant) {
|
||||||
|
case 'PDiscard':
|
||||||
|
return true;
|
||||||
|
case 'PAtom':
|
||||||
|
switch (p.value._variant) {
|
||||||
|
case 'Boolean': return typeof v === 'boolean';
|
||||||
|
case 'ByteString': return Bytes.isBytes(v);
|
||||||
|
case 'Double': return DoubleFloat.isDouble(v);
|
||||||
|
case 'Float': return SingleFloat.isSingle(v);
|
||||||
|
case 'SignedInteger': return typeof v === 'number';
|
||||||
|
case 'String': return typeof v === 'string';
|
||||||
|
case 'Symbol': return typeof v === 'symbol';
|
||||||
|
}
|
||||||
|
case 'PEmbedded':
|
||||||
|
return isEmbedded(v);
|
||||||
|
case 'PBind':
|
||||||
|
if (walk(p.value.pattern, v)) {
|
||||||
|
bindings.push(v);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
case 'PAnd':
|
||||||
|
for (const pp of p.value.patterns) {
|
||||||
|
if (!walk(pp, v)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case 'PNot': {
|
||||||
|
const savedBindings = bindings;
|
||||||
|
bindings = [];
|
||||||
|
const result = !walk(p.value.pattern, v)
|
||||||
|
bindings = savedBindings;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
case 'Lit':
|
||||||
|
return is(p.value.value, v);
|
||||||
|
case 'PCompound': {
|
||||||
|
const ctor = p.value.ctor;
|
||||||
|
const members = p.value.members;
|
||||||
|
switch (ctor._variant) {
|
||||||
|
case 'CRec':
|
||||||
|
if (!Record.isRecord<Assertion, Tuple<Assertion>, Ref>(v)) return false;
|
||||||
|
if (!is(ctor.value.label, v.label)) return false;
|
||||||
|
if (ctor.value.arity !== v.length) return false;
|
||||||
|
for (const [key, pp] of members) {
|
||||||
|
if (typeof key !== 'number') return false;
|
||||||
|
if (!walk(pp, v[key])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case 'CArr':
|
||||||
|
if (!Array.isArray(v)) return false;
|
||||||
|
if ('label' in v) return false;
|
||||||
|
if (ctor.value.arity !== v.length) return false;
|
||||||
|
for (const [key, pp] of members) {
|
||||||
|
if (typeof key !== 'number') return false;
|
||||||
|
if (!walk(pp, v[key])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case 'CDict':
|
||||||
|
if (!Dictionary.isDictionary<Ref, Assertion>(v)) return false;
|
||||||
|
for (const [key, pp] of members) {
|
||||||
|
const vv = v.get(key);
|
||||||
|
if (vv === void 0) return false;
|
||||||
|
if (!walk(pp, vv)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
((_p : never) => {})(p);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return walk(p, v) ? bindings : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function instantiate(t: Template, b: Bindings): Assertion {
|
||||||
|
function walk(t: Template): Assertion {
|
||||||
|
switch (t._variant) {
|
||||||
|
case 'TAttenuate': {
|
||||||
|
const v = walk(t.value.template);
|
||||||
|
if (!isEmbedded(v)) {
|
||||||
|
throw new Error(`Attempt to attenuate non-capability: ${v.asPreservesText()}`);
|
||||||
|
}
|
||||||
|
const r = v.embeddedValue;
|
||||||
|
return embed(attenuate(r, ... t.value.attenuation));
|
||||||
|
}
|
||||||
|
case 'TRef': {
|
||||||
|
const n = t.value.binding;
|
||||||
|
const v = b[n];
|
||||||
|
if (v === void 0) throw new Error(`Unbound reference: ${n}`);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
case 'Lit':
|
||||||
|
return t.value.value;
|
||||||
|
case 'TCompound': {
|
||||||
|
const ctor = t.value.ctor;
|
||||||
|
const members = t.value.members;
|
||||||
|
switch (ctor._variant) {
|
||||||
|
case 'CRec': {
|
||||||
|
const v = [] as unknown as Record<Assertion, Assertion[], Ref>;
|
||||||
|
v.length = ctor.value.arity;
|
||||||
|
v.label = ctor.value.label;
|
||||||
|
for (const [key, tt] of members) {
|
||||||
|
v[key as number] = walk(tt);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
case 'CArr': {
|
||||||
|
const v = [];
|
||||||
|
v.length = ctor.value.arity;
|
||||||
|
for (const [key, tt] of members) {
|
||||||
|
v[key as number] = walk(tt);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
case 'CDict': {
|
||||||
|
const v = new Dictionary<Ref, Assertion>();
|
||||||
|
for (const [key, tt] of members) {
|
||||||
|
v.set(key, walk(tt));
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return walk(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rewrite(r: Rewrite, v: Assertion): Assertion | null {
|
||||||
|
const bindings = match(r.pattern, v);
|
||||||
|
if (bindings === null) return null;
|
||||||
|
return instantiate(r.template, bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function examineAlternatives(cav: Caveat, v: Assertion): Assertion | null {
|
||||||
|
if (cav._variant === 'Alts') {
|
||||||
|
for (const r of cav.value.alternatives) {
|
||||||
|
const w = rewrite(r, v);
|
||||||
|
if (w !== null) return w;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return rewrite(cav.value, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runRewrites(a: Attenuation | undefined, v: Assertion): Assertion | null {
|
||||||
|
if (a !== void 0) {
|
||||||
|
for (const stage of a) {
|
||||||
|
const w = examineAlternatives(stage, v);
|
||||||
|
if (w === null) return null;
|
||||||
|
v = w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rfilter(... patterns: Pattern[]): Caveat {
|
||||||
|
const ps = patterns.map(p => Rewrite({
|
||||||
|
pattern: Pattern.PBind(PBind(p)),
|
||||||
|
template: Template.TRef(TRef(0))
|
||||||
|
}));
|
||||||
|
return ps.length === 1 ? Caveat.Rewrite(ps[0]) : Caveat.Alts(Alts(ps));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attenuate(ref: Ref, ... a: Attenuation): Ref {
|
||||||
|
if (a.length === 0) return ref;
|
||||||
|
return { ... ref, attenuation: [... a, ... (ref.attenuation ?? [])] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forwarder(t: Turn, ref: Ref): { proxy: Ref, revoker: Ref } {
|
||||||
|
let underlying: Ref | null = ref;
|
||||||
|
let handleMap = new IdentityMap<Handle, Handle>();
|
||||||
|
let proxy = t.ref({
|
||||||
|
assert(turn: Turn, assertion: Assertion, handle: Handle): void {
|
||||||
|
if (underlying === null) return;
|
||||||
|
handleMap.set(handle, turn.assert(underlying, assertion));
|
||||||
|
},
|
||||||
|
retract(turn: Turn, handle: Handle): void {
|
||||||
|
if (underlying === null) return;
|
||||||
|
turn.retract(handleMap.get(handle));
|
||||||
|
handleMap.delete(handle);
|
||||||
|
},
|
||||||
|
message(turn: Turn, body: Assertion): void {
|
||||||
|
if (underlying === null) return;
|
||||||
|
turn.message(underlying, body);
|
||||||
|
},
|
||||||
|
sync(turn: Turn, peer: Ref): void {
|
||||||
|
if (underlying === null) return;
|
||||||
|
turn._sync(underlying, peer);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let revoker = t.ref({
|
||||||
|
message(turn: Turn, _body: Assertion): void {
|
||||||
|
underlying = null;
|
||||||
|
handleMap.forEach(h => turn.retract(h));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { proxy, revoker };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pRec(label: SturdyValue, ... members: Array<Pattern>): Pattern {
|
||||||
|
return Pattern.PCompound(PCompound({
|
||||||
|
ctor: ConstructorSpec.CRec(CRec({ label: label, arity: members.length })),
|
||||||
|
members: PCompoundMembers(new Dictionary<_embedded, Pattern>(
|
||||||
|
members.map((p, i) => [i, p] as const).filter(e => e[1]._variant !== 'PDiscard')))}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pArr(... members: Array<Pattern>): Pattern {
|
||||||
|
return Pattern.PCompound(PCompound({
|
||||||
|
ctor: ConstructorSpec.CArr(CArr(members.length)),
|
||||||
|
members: PCompoundMembers(new Dictionary<_embedded, Pattern>(
|
||||||
|
members.map((p, i) => [i, p] as const).filter(e => e[1]._variant !== 'PDiscard')))}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pDict(... entries: [SturdyValue, Pattern][]): Pattern {
|
||||||
|
return Pattern.PCompound(PCompound({
|
||||||
|
ctor: ConstructorSpec.CDict(CDict()),
|
||||||
|
members: PCompoundMembers(new Dictionary<_embedded, Pattern>(entries))}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pLit(value: SturdyValue): Pattern {
|
||||||
|
return Pattern.Lit(Lit(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pNot(p: Pattern): Pattern {
|
||||||
|
return Pattern.PNot(PNot(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pAnd(... ps: Pattern[]): Pattern {
|
||||||
|
return Pattern.PAnd(PAnd(ps));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pBind(pattern: Pattern): Pattern {
|
||||||
|
return Pattern.PBind(PBind(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pEmbedded(): Pattern {
|
||||||
|
return Pattern.PEmbedded(PEmbedded());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pSymbol(): Pattern {
|
||||||
|
return Pattern.PAtom(PAtom.Symbol());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pByteString(): Pattern {
|
||||||
|
return Pattern.PAtom(PAtom.ByteString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pString(): Pattern {
|
||||||
|
return Pattern.PAtom(PAtom.String());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pSignedInteger(): Pattern {
|
||||||
|
return Pattern.PAtom(PAtom.SignedInteger());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pDouble(): Pattern {
|
||||||
|
return Pattern.PAtom(PAtom.Double());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pFloat(): Pattern {
|
||||||
|
return Pattern.PAtom(PAtom.Float());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pBoolean(): Pattern {
|
||||||
|
return Pattern.PAtom(PAtom.Boolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pDiscard(): Pattern {
|
||||||
|
return Pattern.PDiscard(PDiscard());
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
||||||
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { Bytes, underlying } from '@preserves/core';
|
||||||
|
import * as node_crypto from 'crypto';
|
||||||
|
|
||||||
|
export const KEY_LENGTH = 16; // 128 bits
|
||||||
|
|
||||||
|
export const newKey: () => Promise<Bytes> =
|
||||||
|
(typeof crypto !== 'undefined' && 'getRandomValues' in crypto)
|
||||||
|
? (async () => {
|
||||||
|
const bs = new Bytes(KEY_LENGTH);
|
||||||
|
crypto.getRandomValues(underlying(bs));
|
||||||
|
return bs;
|
||||||
|
})
|
||||||
|
: (async () => Bytes.from(node_crypto.randomBytes(KEY_LENGTH)));
|
||||||
|
|
||||||
|
export const mac: (secretKey: Bytes, data: Bytes) => Promise<Bytes> =
|
||||||
|
(typeof crypto !== 'undefined' && 'subtle' in crypto)
|
||||||
|
? (async (secretKey, data) => {
|
||||||
|
if (secretKey.length !== KEY_LENGTH) throw new Error("Invalid key length");
|
||||||
|
const k = await window.crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
underlying(secretKey),
|
||||||
|
{
|
||||||
|
hash: 'SHA-256',
|
||||||
|
name: 'HMAC',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['sign']);
|
||||||
|
const bs = await window.crypto.subtle.sign({ name: 'HMAC' }, k, underlying(data));
|
||||||
|
return Bytes.from(new Uint8Array(bs, 0, KEY_LENGTH));
|
||||||
|
})
|
||||||
|
: (typeof node_crypto.createHmac !== 'undefined')
|
||||||
|
? (async (secretKey, data) => {
|
||||||
|
const hmac = node_crypto.createHmac('sha256', underlying(secretKey));
|
||||||
|
hmac.update(underlying(data));
|
||||||
|
return Bytes.from(hmac.digest().subarray(0, KEY_LENGTH));
|
||||||
|
})
|
||||||
|
: (async (_secretKey, _data) => {
|
||||||
|
throw new Error('No HMAC SHA-256 available');
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
||||||
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Basically Macaroons [1] in a Dataspace context
|
||||||
|
//
|
||||||
|
// [1]: Birgisson, Arnar, Joe Gibbs Politz, Úlfar Erlingsson, Ankur
|
||||||
|
// Taly, Michael Vrable, and Mark Lentczner. “Macaroons: Cookies with
|
||||||
|
// Contextual Caveats for Decentralized Authorization in the Cloud.”
|
||||||
|
// In Network and Distributed System Security Symposium. San Diego,
|
||||||
|
// California: Internet Society, 2014.
|
||||||
|
|
||||||
|
import { mac } from './cryptography';
|
||||||
|
import { Bytes, decode, encode, is, neverEmbeddedType, Value } from '@preserves/core';
|
||||||
|
import * as S from '../gen/sturdy.js';
|
||||||
|
export * from '../gen/sturdy.js';
|
||||||
|
|
||||||
|
export type SturdyValue = Value<S._embedded>;
|
||||||
|
|
||||||
|
export const KEY_LENGTH = 16; // 128 bits
|
||||||
|
|
||||||
|
export function embeddedNotAllowed(): never {
|
||||||
|
throw new Error("Embedded Ref not permitted in SturdyRef");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sturdyEncode(v: SturdyValue): Bytes {
|
||||||
|
return encode<S._embedded>(v, {
|
||||||
|
canonical: true,
|
||||||
|
includeAnnotations: false,
|
||||||
|
embeddedEncode: neverEmbeddedType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sturdyDecode(bs: Bytes): SturdyValue {
|
||||||
|
return decode<S._embedded>(bs, {
|
||||||
|
includeAnnotations: false,
|
||||||
|
embeddedDecode: neverEmbeddedType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function mint(oid: SturdyValue, secretKey: Bytes): Promise<S.SturdyRef> {
|
||||||
|
return S.SturdyRef({
|
||||||
|
oid,
|
||||||
|
caveatChain: [],
|
||||||
|
sig: await mac(secretKey, sturdyEncode(oid)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function attenuate(r: S.SturdyRef, ... a: S.Attenuation): Promise<S.SturdyRef> {
|
||||||
|
return S.SturdyRef({
|
||||||
|
oid: r.oid,
|
||||||
|
caveatChain: [... r.caveatChain, a],
|
||||||
|
sig: await mac(r.sig, sturdyEncode(S.fromAttenuation(a)))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validate(r: S.SturdyRef, secretKey: Bytes): Promise<boolean> {
|
||||||
|
const sig = await r.caveatChain.reduce(
|
||||||
|
async (sig, a) => mac(await sig, sturdyEncode(S.fromAttenuation(a))),
|
||||||
|
mac(secretKey, sturdyEncode(r.oid)));
|
||||||
|
return is(sig, r.sig);
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ const assert = require('assert');
|
||||||
const Immutable = require('immutable');
|
const Immutable = require('immutable');
|
||||||
|
|
||||||
const Syndicate = require('../src/index.js');
|
const Syndicate = require('../src/index.js');
|
||||||
const { Seal, Skeleton, Capture, Discard, Record, Observe } = Syndicate;
|
const { Skeleton, Capture, Discard, Record, Observe } = Syndicate;
|
||||||
|
|
||||||
const __ = Discard();
|
const __ = Discard();
|
||||||
const _$ = Capture(Discard());
|
const _$ = Capture(Discard());
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
for p in packages/*/; do echo $p/veryclean; done | xargs redo
|
|
||||||
rm -rf node_modules
|
|
Loading…
Reference in New Issue