Initial port to typescript/es6/babel-free. Far from working or complete
This commit is contained in:
parent
bdc3dcc1d5
commit
58e190e419
|
@ -9,3 +9,5 @@ scratch/
|
||||||
*.did
|
*.did
|
||||||
.do_built
|
.do_built
|
||||||
.do_built.dir/
|
.do_built.dir/
|
||||||
|
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
bootstrap: node_modules/lerna
|
||||||
|
|
||||||
|
node_modules/lerna:
|
||||||
|
npm i .
|
||||||
|
$(MAKE) clean
|
||||||
|
+$(MAKE) -j$$(nproc) all
|
||||||
|
|
||||||
|
PACKAGE_JSONS=$(wildcard packages/*/package.json)
|
||||||
|
PACKAGE_DIRS=$(PACKAGE_JSONS:/package.json=)
|
||||||
|
|
||||||
|
all clean veryclean:
|
||||||
|
+for d in $(PACKAGE_DIRS); do make -C $$d $@ & done; wait
|
||||||
|
|
||||||
|
watch:
|
||||||
|
inotifytest make -j$$(nproc) all
|
1
all.do
1
all.do
|
@ -1 +0,0 @@
|
||||||
(for p in packages/*/; do echo $p/all; done | xargs redo-ifchange) && (echo Done. >&2)
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
[ -d node_modules/lerna ] || npm i .
|
|
||||||
redo clean
|
|
||||||
redo -j$(nproc) all
|
|
368
do
368
do
|
@ -1,368 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# A minimal alternative to djb redo that doesn't support incremental builds.
|
|
||||||
# For the full version, visit http://github.com/apenwarr/redo
|
|
||||||
#
|
|
||||||
# The author disclaims copyright to this source file and hereby places it in
|
|
||||||
# the public domain. (2010 12 14; updated 2018 10 31)
|
|
||||||
#
|
|
||||||
USAGE="
|
|
||||||
usage: $0 [-d] [-x] [-v] [-c] <targets...>
|
|
||||||
-d print extra debug messages (mostly about dependency checks)
|
|
||||||
-v run .do files with 'set -v'
|
|
||||||
-x run .do files with 'set -x'
|
|
||||||
-c clean up all old targets before starting
|
|
||||||
|
|
||||||
Note: $0 is an implementation of redo that does *not* check dependencies.
|
|
||||||
It will never rebuild a target it has already built, unless you use -c.
|
|
||||||
"
|
|
||||||
|
|
||||||
# By default, no output coloring.
|
|
||||||
green=""
|
|
||||||
bold=""
|
|
||||||
plain=""
|
|
||||||
|
|
||||||
if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then
|
|
||||||
green="$(printf '\033[32m')"
|
|
||||||
bold="$(printf '\033[1m')"
|
|
||||||
plain="$(printf '\033[m')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Split $1 into a dir part ($_dirsplit_dir) and base filename ($_dirsplit_base)
|
|
||||||
_dirsplit()
|
|
||||||
{
|
|
||||||
_dirsplit_base=${1##*/}
|
|
||||||
_dirsplit_dir=${1%$_dirsplit_base}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Like /usr/bin/dirname, but avoids a fork and uses _dirsplit semantics.
|
|
||||||
dirname()
|
|
||||||
(
|
|
||||||
_dirsplit "$1"
|
|
||||||
dir=${_dirsplit_dir%/}
|
|
||||||
echo "${dir:-.}"
|
|
||||||
)
|
|
||||||
|
|
||||||
_dirsplit "$0"
|
|
||||||
export REDO=$(cd "${_dirsplit_dir:-.}" && echo "$PWD/$_dirsplit_base")
|
|
||||||
_cmd=$_dirsplit_base
|
|
||||||
|
|
||||||
DO_TOP=
|
|
||||||
if [ -z "$DO_BUILT" ]; then
|
|
||||||
export _do_opt_debug=
|
|
||||||
export _do_opt_exec=
|
|
||||||
export _do_opt_verbose=
|
|
||||||
export _do_opt_clean=
|
|
||||||
fi
|
|
||||||
while getopts 'dxvch?' _opt; do
|
|
||||||
case $_opt in
|
|
||||||
d) _do_opt_debug=1 ;;
|
|
||||||
x) _do_opt_exec=x ;;
|
|
||||||
v) _do_opt_verbose=v ;;
|
|
||||||
c) _do_opt_clean=1 ;;
|
|
||||||
\?|h|*) printf "%s" "$USAGE" >&2
|
|
||||||
exit 99
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
shift "$((OPTIND - 1))"
|
|
||||||
_debug() {
|
|
||||||
[ -z "$_do_opt_debug" ] || echo "$@" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then
|
|
||||||
DO_TOP=1
|
|
||||||
if [ "$#" -eq 0 ] && [ "$_cmd" = "do" -o "$_cmd" = "redo" ]; then
|
|
||||||
set all # only toplevel redo has a default target
|
|
||||||
fi
|
|
||||||
export DO_BUILT=$PWD/.do_built
|
|
||||||
: >>"$DO_BUILT"
|
|
||||||
sort -u "$DO_BUILT" >"$DO_BUILT.new"
|
|
||||||
echo "Cleaning up from previous run..." >&2
|
|
||||||
while read f; do
|
|
||||||
[ -n "$_do_opt_clean" ] && printf "%s\0%s.did\0" "$f" "$f"
|
|
||||||
printf "%s.did.tmp\0" "$f"
|
|
||||||
done <"$DO_BUILT.new" |
|
|
||||||
xargs -0 rm -f 2>/dev/null
|
|
||||||
mv "$DO_BUILT.new" "$DO_BUILT"
|
|
||||||
DO_PATH=$DO_BUILT.dir
|
|
||||||
export PATH=$DO_PATH:$PATH
|
|
||||||
rm -rf "$DO_PATH"
|
|
||||||
mkdir "$DO_PATH"
|
|
||||||
for d in redo redo-ifchange redo-whichdo; do
|
|
||||||
ln -s "$REDO" "$DO_PATH/$d"
|
|
||||||
done
|
|
||||||
[ -e /bin/true ] && TRUE=/bin/true || TRUE=/usr/bin/true
|
|
||||||
for d in redo-ifcreate redo-stamp redo-always redo-ood \
|
|
||||||
redo-targets redo-sources; do
|
|
||||||
ln -s $TRUE "$DO_PATH/$d"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Chop the "file" part off a /path/to/file pathname.
|
|
||||||
# Note that if the filename already ends in a /, we just remove the slash.
|
|
||||||
_updir()
|
|
||||||
{
|
|
||||||
local v="${1%/*}"
|
|
||||||
[ "$v" != "$1" ] && echo "$v"
|
|
||||||
# else "empty" which means we went past the root
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Returns true if $1 starts with $2.
|
|
||||||
_startswith()
|
|
||||||
{
|
|
||||||
[ "${1#"$2"}" != "$1" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Returns true if $1 ends with $2.
|
|
||||||
_endswith()
|
|
||||||
{
|
|
||||||
[ "${1%"$2"}" != "$1" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Prints $1 as a path relative to $PWD (not starting with /).
|
|
||||||
# If it already doesn't start with a /, doesn't change the string.
|
|
||||||
_relpath()
|
|
||||||
{
|
|
||||||
local here="$(command pwd)" there="$1" out= hadslash=
|
|
||||||
#echo "RP start '$there' hs='$hadslash'" >&2
|
|
||||||
_startswith "$there" "/" || { echo "$there" && return; }
|
|
||||||
[ "$there" != "/" ] && _endswith "$there" "/" && hadslash=/
|
|
||||||
here=${here%/}/
|
|
||||||
while [ -n "$here" ]; do
|
|
||||||
#echo "RP out='$out' here='$here' there='$there'" >&2
|
|
||||||
[ "${here%/}" = "${there%/}" ] && there= && break;
|
|
||||||
[ "${there#$here}" != "$there" ] && break
|
|
||||||
out=../$out
|
|
||||||
_dirsplit "${here%/}"
|
|
||||||
here=$_dirsplit_dir
|
|
||||||
done
|
|
||||||
there=${there#$here}
|
|
||||||
if [ -n "$there" ]; then
|
|
||||||
echo "$out${there%/}$hadslash"
|
|
||||||
else
|
|
||||||
echo "${out%/}$hadslash"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Prints a "normalized relative" path, with ".." resolved where possible.
|
|
||||||
# For example, a/b/../c will be reduced to just a/c.
|
|
||||||
_normpath()
|
|
||||||
(
|
|
||||||
local path="$1" out= isabs=
|
|
||||||
#echo "NP start '$path'" >&2
|
|
||||||
if _startswith "$path" "/"; then
|
|
||||||
isabs=1
|
|
||||||
else
|
|
||||||
path="${PWD%/}/$path"
|
|
||||||
fi
|
|
||||||
set -f
|
|
||||||
IFS=/
|
|
||||||
for d in $path; do
|
|
||||||
#echo "NP out='$out' d='$d'" >&2
|
|
||||||
if [ "$d" = ".." ]; then
|
|
||||||
out=$(_updir "${out%/}")/
|
|
||||||
else
|
|
||||||
out=$out$d/
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
#echo "NP out='$out' (done)" >&2
|
|
||||||
out=${out%/}
|
|
||||||
if [ -n "$isabs" ]; then
|
|
||||||
echo "${out:-/}"
|
|
||||||
else
|
|
||||||
_relpath "${out:-/}"
|
|
||||||
fi
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# List the possible names for default*.do files in dir $1 matching the target
|
|
||||||
# pattern in $2. We stop searching when we find the first one that exists.
|
|
||||||
_find_dofiles_pwd()
|
|
||||||
{
|
|
||||||
local dodir="$1" dofile="$2"
|
|
||||||
_startswith "$dofile" "default." || dofile=${dofile#*.}
|
|
||||||
while :; do
|
|
||||||
dofile=default.${dofile#default.*.}
|
|
||||||
echo "$dodir$dofile"
|
|
||||||
[ -e "$dodir$dofile" ] && return 0
|
|
||||||
[ "$dofile" = default.do ] && break
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# List the possible names for default*.do files in $PWD matching the target
|
|
||||||
# pattern in $1. We stop searching when we find the first name that works.
|
|
||||||
# If there are no matches in $PWD, we'll search in .., and so on, to the root.
|
|
||||||
_find_dofiles()
|
|
||||||
{
|
|
||||||
local target="$1" dodir= dofile= newdir=
|
|
||||||
_debug "find_dofile: '$PWD' '$target'"
|
|
||||||
dofile="$target.do"
|
|
||||||
echo "$dofile"
|
|
||||||
[ -e "$dofile" ] && return 0
|
|
||||||
|
|
||||||
# Try default.*.do files, walking up the tree
|
|
||||||
_dirsplit "$dofile"
|
|
||||||
dodir=$_dirsplit_dir
|
|
||||||
dofile=$_dirsplit_base
|
|
||||||
[ -n "$dodir" ] && dodir=${dodir%/}/
|
|
||||||
[ -e "$dodir$dofile" ] && return 0
|
|
||||||
for i in $(seq 100); do
|
|
||||||
[ -n "$dodir" ] && dodir=${dodir%/}/
|
|
||||||
#echo "_find_dofiles: '$dodir' '$dofile'" >&2
|
|
||||||
_find_dofiles_pwd "$dodir" "$dofile" && return 0
|
|
||||||
newdir=$(_normpath "${dodir}..")
|
|
||||||
[ "$newdir" = "$dodir" ] && break
|
|
||||||
dodir=$newdir
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Print the last .do file returned by _find_dofiles.
|
|
||||||
# If that file exists, returns 0, else 1.
|
|
||||||
_find_dofile()
|
|
||||||
{
|
|
||||||
local files="$(_find_dofiles "$1")"
|
|
||||||
rv=$?
|
|
||||||
#echo "files='$files'" >&2
|
|
||||||
[ "$rv" -ne 0 ] && return $rv
|
|
||||||
echo "$files" | {
|
|
||||||
while read -r linex; do line=$linex; done
|
|
||||||
printf "%s\n" "$line"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Actually run the given $dofile with the arguments in $@.
|
|
||||||
# Note: you should always run this in a subshell.
|
|
||||||
_run_dofile()
|
|
||||||
{
|
|
||||||
export DO_DEPTH="$DO_DEPTH "
|
|
||||||
export REDO_TARGET="$PWD/$target"
|
|
||||||
local line1
|
|
||||||
set -e
|
|
||||||
read line1 <"$PWD/$dofile" || true
|
|
||||||
cmd=${line1#"#!/"}
|
|
||||||
if [ "$cmd" != "$line1" ]; then
|
|
||||||
set -$_do_opt_verbose$_do_opt_exec
|
|
||||||
exec /$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2"
|
|
||||||
else
|
|
||||||
set -$_do_opt_verbose$_do_opt_exec
|
|
||||||
:; . "$PWD/$dofile" >"$tmp.tmp2"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Find and run the right .do file, starting in dir $1, for target $2, using
|
|
||||||
# filename $3 as the temporary output file. Renames the temp file to $2 when
|
|
||||||
# done.
|
|
||||||
_do()
|
|
||||||
{
|
|
||||||
local dir="$1" target="$2" tmp="$3" dopath= dodir= dofile= ext=
|
|
||||||
if [ "$_cmd" = "redo" ] ||
|
|
||||||
( [ ! -e "$target" -o -d "$target" ] &&
|
|
||||||
[ ! -e "$target.did" ] ); then
|
|
||||||
printf '%sdo %s%s%s%s\n' \
|
|
||||||
"$green" "$DO_DEPTH" "$bold" "$dir$target" "$plain" >&2
|
|
||||||
dopath=$(_find_dofile "$target")
|
|
||||||
if [ ! -e "$dopath" ]; then
|
|
||||||
echo "do: $target: no .do file ($PWD)" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_dirsplit "$dopath"
|
|
||||||
dodir=$_dirsplit_dir dofile=$_dirsplit_base
|
|
||||||
if _startswith "$dofile" "default."; then
|
|
||||||
ext=${dofile#default}
|
|
||||||
ext=${ext%.do}
|
|
||||||
else
|
|
||||||
ext=
|
|
||||||
fi
|
|
||||||
target=$PWD/$target
|
|
||||||
tmp=$PWD/$tmp
|
|
||||||
cd "$dodir" || return 99
|
|
||||||
target=$(_relpath "$target") || return 98
|
|
||||||
tmp=$(_relpath "$tmp") || return 97
|
|
||||||
base=${target%$ext}
|
|
||||||
[ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] ||
|
|
||||||
: >>"$target.did.tmp"
|
|
||||||
( _run_dofile "$target" "$base" "$tmp.tmp" )
|
|
||||||
rv=$?
|
|
||||||
if [ $rv != 0 ]; then
|
|
||||||
printf "do: %s%s\n" "$DO_DEPTH" \
|
|
||||||
"$dir$target: got exit code $rv" >&2
|
|
||||||
rm -f "$tmp.tmp" "$tmp.tmp2" "$target.did"
|
|
||||||
return $rv
|
|
||||||
fi
|
|
||||||
echo "$PWD/$target" >>"$DO_BUILT"
|
|
||||||
mv "$tmp.tmp" "$target" 2>/dev/null ||
|
|
||||||
! test -s "$tmp.tmp2" ||
|
|
||||||
mv "$tmp.tmp2" "$target" 2>/dev/null
|
|
||||||
[ -e "$target.did.tmp" ] &&
|
|
||||||
mv "$target.did.tmp" "$target.did" ||
|
|
||||||
: >>"$target.did"
|
|
||||||
rm -f "$tmp.tmp2"
|
|
||||||
else
|
|
||||||
_debug "do $DO_DEPTH$target exists." >&2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Make corrections for directories that don't actually exist yet.
|
|
||||||
_dir_shovel()
|
|
||||||
{
|
|
||||||
local dir base
|
|
||||||
xdir=$1 xbase=$2 xbasetmp=$2
|
|
||||||
while [ ! -d "$xdir" -a -n "$xdir" ]; do
|
|
||||||
_dirsplit "${xdir%/}"
|
|
||||||
xbasetmp=${_dirsplit_base}__$xbasetmp
|
|
||||||
xdir=$_dirsplit_dir xbase=$_dirsplit_base/$xbase
|
|
||||||
done
|
|
||||||
_debug "xbasetmp='$xbasetmp'" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Implementation of the "redo" command.
|
|
||||||
_redo()
|
|
||||||
{
|
|
||||||
set +e
|
|
||||||
for i in "$@"; do
|
|
||||||
_dirsplit "$i"
|
|
||||||
_dir_shovel "$_dirsplit_dir" "$_dirsplit_base"
|
|
||||||
dir=$xdir base=$xbase basetmp=$xbasetmp
|
|
||||||
( cd "$dir" && _do "$dir" "$base" "$basetmp" )
|
|
||||||
[ "$?" = 0 ] || return 1
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Implementation of the "redo-whichdo" command.
|
|
||||||
_whichdo()
|
|
||||||
{
|
|
||||||
_find_dofiles "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
case $_cmd in
|
|
||||||
do|redo|redo-ifchange) _redo "$@" ;;
|
|
||||||
redo-whichdo) _whichdo "$1" ;;
|
|
||||||
do.test) ;;
|
|
||||||
*) printf "$0: '%s': unexpected redo command" "$_cmd" >&2; exit 99 ;;
|
|
||||||
esac
|
|
||||||
[ "$?" = 0 ] || exit 1
|
|
||||||
|
|
||||||
if [ -n "$DO_TOP" ]; then
|
|
||||||
if [ -n "$_do_opt_clean" ]; then
|
|
||||||
echo "Removing stamp files..." >&2
|
|
||||||
[ ! -e "$DO_BUILT" ] ||
|
|
||||||
while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" |
|
|
||||||
xargs -0 rm -f 2>/dev/null
|
|
||||||
fi
|
|
||||||
fi
|
|
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
|
@ -2,34 +2,21 @@
|
||||||
"name": "@syndicate-lang/root",
|
"name": "@syndicate-lang/root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.11.6",
|
"@rollup/plugin-node-resolve": "^11.0.1",
|
||||||
"@babel/plugin-syntax-jsx": "^7.10.4",
|
"@types/jest": "^26.0.19",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.10.4",
|
"@types/node": "^14.14.20",
|
||||||
"@babel/preset-env": "^7.11.5",
|
|
||||||
"@rollup/plugin-commonjs": "^14.0.0",
|
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
|
||||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
|
"jest": "^26.6.3",
|
||||||
"lerna": "^3.22.1",
|
"lerna": "^3.22.1",
|
||||||
"mocha": "^7.2.0",
|
|
||||||
"nyc": "^14.1.1",
|
"nyc": "^14.1.1",
|
||||||
"rollup": "^2.23.0"
|
"rollup": "^2.36.1",
|
||||||
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"ts-jest": "^26.4.4",
|
||||||
|
"ts-node": "^9.1.1",
|
||||||
|
"ts-node-dev": "^1.1.1",
|
||||||
|
"typescript": "^4.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@syndicate-lang/core": "file:packages/core",
|
"@syndicate-lang/core": "file:packages/core"
|
||||||
"@syndicate-lang/create": "file:packages/create",
|
|
||||||
"@syndicate-lang/driver-browser-ui": "file:packages/driver-browser-ui",
|
|
||||||
"@syndicate-lang/driver-http-node": "file:packages/driver-http-node",
|
|
||||||
"@syndicate-lang/driver-mdns": "file:packages/driver-mdns",
|
|
||||||
"@syndicate-lang/driver-streams-node": "file:packages/driver-streams-node",
|
|
||||||
"@syndicate-lang/driver-timer": "file:packages/driver-timer",
|
|
||||||
"@syndicate-lang/driver-udp-node": "file:packages/driver-udp-node",
|
|
||||||
"@syndicate-lang/driver-websocket": "file:packages/driver-websocket",
|
|
||||||
"@syndicate-lang/flappy-bird-demo": "file:packages/flappy-bird-demo",
|
|
||||||
"@syndicate-lang/server": "file:packages/server",
|
|
||||||
"@syndicate-lang/socks": "file:packages/socks",
|
|
||||||
"@syndicate-lang/syntax": "file:packages/syntax",
|
|
||||||
"@syndicate-lang/syntax-playground": "file:packages/syntax-playground",
|
|
||||||
"@syndicate-lang/syntax-server": "file:packages/syntax-server"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
all:
|
||||||
|
npm run prepare
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf lib dist .nyc_output coverage tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
veryclean: clean
|
||||||
|
rm -rf node_modules package-lock.json
|
|
@ -0,0 +1,16 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
|
||||||
|
|
||||||
|
for (let f of fs.readdirSync('dist')) {
|
||||||
|
const prefix = `syndicate-${pkg.version}`;
|
||||||
|
if (f.startsWith(prefix)) {
|
||||||
|
const linkname = `dist/syndicate${f.substring(prefix.length)}`;
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(linkname);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== 'ENOENT') throw e;
|
||||||
|
}
|
||||||
|
fs.symlinkSync(f, linkname);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
"use strict";
|
#!/usr/bin/env -S node --es-module-specifier-resolution=node
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
||||||
// Copyright (C) 2016-2018 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,14 +17,9 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
const Immutable = require('immutable');
|
import { Dataspace, Skeleton, Ground, Record, Discard, Capture, Observe } from '../lib/index';
|
||||||
const Syndicate = require('../src/index.js');
|
const __ = Discard._instance;
|
||||||
const Skeleton = Syndicate.Skeleton;
|
const _$ = Capture(__);
|
||||||
const Dataspace = Syndicate.Dataspace;
|
|
||||||
const Ground = Syndicate.Ground;
|
|
||||||
const Record = Syndicate.Record;
|
|
||||||
const __ = Syndicate.Discard._instance;
|
|
||||||
const _$ = Syndicate.Capture(__);
|
|
||||||
|
|
||||||
const BoxState = Record.makeConstructor('BoxState', ['value']);
|
const BoxState = Record.makeConstructor('BoxState', ['value']);
|
||||||
const SetBox = Record.makeConstructor('SetBox', ['newValue']);
|
const SetBox = Record.makeConstructor('SetBox', ['newValue']);
|
||||||
|
@ -32,70 +27,58 @@ const SetBox = Record.makeConstructor('SetBox', ['newValue']);
|
||||||
const N = 100000;
|
const N = 100000;
|
||||||
|
|
||||||
console.time('box-and-client-' + N.toString());
|
console.time('box-and-client-' + N.toString());
|
||||||
let _savedGlobalFacet = Dataspace._currentFacet;
|
|
||||||
Dataspace._currentFacet = new Syndicate._Dataspace.ActionCollector();
|
|
||||||
|
|
||||||
Dataspace.spawn('box', function () {
|
new Ground(() => {
|
||||||
Dataspace.declareField(this, 'value', 0);
|
Dataspace.spawn('box', function () {
|
||||||
Dataspace.currentFacet().addEndpoint(() => {
|
Dataspace.declareField(this, 'value', 0);
|
||||||
return [BoxState(this.value), null];
|
Dataspace.currentFacet.addEndpoint(() => {
|
||||||
});
|
return { assertion: BoxState(this.value), analysis: null };
|
||||||
Dataspace.currentFacet().addDataflow(() => {
|
|
||||||
if (this.value === N) {
|
|
||||||
Dataspace.currentFacet().stop(() => {
|
|
||||||
console.log('terminated box root facet');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Dataspace.currentFacet().addEndpoint(() => {
|
|
||||||
let handler = Skeleton.analyzeAssertion(SetBox(_$));
|
|
||||||
handler.callback = Dataspace.wrap((evt, vs) => {
|
|
||||||
if (evt === Skeleton.EVENT_MESSAGE) {
|
|
||||||
Dataspace.currentFacet().actor.scheduleScript(() => {
|
|
||||||
this.value = vs.get(0);
|
|
||||||
// console.log('box updated value', vs.get(0));
|
|
||||||
});
|
});
|
||||||
}
|
Dataspace.currentFacet.addDataflow(() => {
|
||||||
});
|
console.log('dataflow saw new value', this.value);
|
||||||
return [Syndicate.Observe(SetBox(_$)), handler];
|
if (this.value === N) {
|
||||||
});
|
Dataspace.currentFacet.stop(() => {
|
||||||
});
|
console.log('terminated box root facet');
|
||||||
|
});
|
||||||
Dataspace.spawn('client', () => {
|
}
|
||||||
Dataspace.currentFacet().addEndpoint(() => {
|
|
||||||
let handler = Skeleton.analyzeAssertion(BoxState(_$));
|
|
||||||
handler.callback = Dataspace.wrap((evt, vs) => {
|
|
||||||
if (evt === Skeleton.EVENT_ADDED) {
|
|
||||||
Dataspace.currentFacet().actor.scheduleScript(() => {
|
|
||||||
// console.log('client sending SetBox', vs.get(0) + 1);
|
|
||||||
Dataspace.send(SetBox(vs.get(0) + 1));
|
|
||||||
});
|
});
|
||||||
}
|
Dataspace.currentFacet.addEndpoint(() => {
|
||||||
});
|
let analysis = Skeleton.analyzeAssertion(SetBox(_$));
|
||||||
return [Syndicate.Observe(BoxState(_$)), handler];
|
analysis.callback = Dataspace.wrap((evt, vs) => {
|
||||||
});
|
if (evt === Skeleton.EventType.MESSAGE) {
|
||||||
Dataspace.currentFacet().addEndpoint(() => {
|
Dataspace.currentFacet.actor.scheduleScript(() => {
|
||||||
let handler = Skeleton.analyzeAssertion(BoxState(__));
|
this.value = vs[0];
|
||||||
handler.callback = Dataspace.wrap((evt, vs) => {
|
console.log('box updated value', vs[0]);
|
||||||
if (evt === Skeleton.EVENT_REMOVED) {
|
});
|
||||||
Dataspace.currentFacet().actor.scheduleScript(() => {
|
}
|
||||||
console.log('box gone');
|
});
|
||||||
|
return { assertion: Observe(SetBox(_$)), analysis };
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return [Syndicate.Observe(BoxState(__)), handler];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports[Dataspace.BootSteps] = {
|
Dataspace.spawn('client', function () {
|
||||||
module: module,
|
Dataspace.currentFacet.addEndpoint(() => {
|
||||||
steps: Dataspace._currentFacet.actions
|
let analysis = Skeleton.analyzeAssertion(BoxState(_$));
|
||||||
};
|
analysis.callback = Dataspace.wrap((evt, vs) => {
|
||||||
Dataspace._currentFacet = _savedGlobalFacet;
|
if (evt === Skeleton.EventType.ADDED) {
|
||||||
_savedGlobalFacet = null;
|
Dataspace.currentFacet.actor.scheduleScript(() => {
|
||||||
|
console.log('client sending SetBox', vs[0] + 1);
|
||||||
Ground.bootModule(module, (g) => {
|
Dataspace.send(SetBox(vs[0] + 1));
|
||||||
g.addStopHandler(() => {
|
});
|
||||||
console.timeEnd('box-and-client-' + N.toString());
|
}
|
||||||
});
|
});
|
||||||
});
|
return { assertion: Observe(BoxState(_$)), analysis };
|
||||||
|
});
|
||||||
|
Dataspace.currentFacet.addEndpoint(() => {
|
||||||
|
let analysis = Skeleton.analyzeAssertion(BoxState(__));
|
||||||
|
analysis.callback = Dataspace.wrap((evt, _vs) => {
|
||||||
|
if (evt === Skeleton.EventType.REMOVED) {
|
||||||
|
Dataspace.currentFacet.actor.scheduleScript(() => {
|
||||||
|
console.log('box gone');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { assertion: Observe(BoxState(__)), analysis };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).addStopHandler(() => console.timeEnd('box-and-client-' + N.toString())).start();
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import 'preserves';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
};
|
|
@ -9,14 +9,17 @@
|
||||||
},
|
},
|
||||||
"repository": "github:syndicate-lang/syndicate-js",
|
"repository": "github:syndicate-lang/syndicate-js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha",
|
"prepare": "npm run compile && npm run rollup",
|
||||||
"cover": "nyc --reporter=html mocha"
|
"compile": "../../node_modules/.bin/tsc --incremental",
|
||||||
|
"rollup": "../../node_modules/.bin/rollup -c syndicate.dist.js && node ./dist-link.js",
|
||||||
|
"test": "../../node_modules/.bin/jest",
|
||||||
|
"cover": "../../node_modules/.bin/nyc --reporter=html ../../node_modules/.bin/jest"
|
||||||
},
|
},
|
||||||
"main": "src/index.js",
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",
|
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.1.1",
|
"preserves": "0.4.0"
|
||||||
"immutable": "^4.0.0-rc.12",
|
|
||||||
"preserves": "0.3.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'assertions.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
var { Record } = require('preserves');
|
|
||||||
|
|
||||||
function Seal(contents) {
|
|
||||||
if (!(this instanceof Seal)) return new Seal(contents);
|
|
||||||
this.contents = contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
Seal.prototype.toJSON = function () {
|
|
||||||
// This definition is useless for actual transport, of course, but
|
|
||||||
// useful for debugging, inasmuch as it seals off the contents from
|
|
||||||
// the view of the JSON renderer, which has trouble with e.g. cyclic
|
|
||||||
// data.
|
|
||||||
return { '@seal': 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.Discard = Record.makeConstructor('discard', []);
|
|
||||||
module.exports.Discard._instance = module.exports.Discard();
|
|
||||||
|
|
||||||
module.exports.Capture = Record.makeConstructor('capture', ['specification']);
|
|
||||||
module.exports.Observe = Record.makeConstructor('observe', ['specification']);
|
|
||||||
|
|
||||||
module.exports.Inbound = Record.makeConstructor('inbound', ['assertion']);
|
|
||||||
module.exports.Outbound = Record.makeConstructor('outbound', ['assertion']);
|
|
||||||
module.exports.Instance = Record.makeConstructor('instance', ['uniqueId']);
|
|
||||||
|
|
||||||
module.exports.Seal = Seal;
|
|
|
@ -1,129 +0,0 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Bags and Deltas (which are Bags where item-counts can be negative).
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'bag.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
const Immutable = require("immutable");
|
|
||||||
const { fromJS } = require("preserves");
|
|
||||||
|
|
||||||
const PRESENT_TO_ABSENT = -1;
|
|
||||||
const ABSENT_TO_ABSENT = 0;
|
|
||||||
const ABSENT_TO_PRESENT = 1;
|
|
||||||
const PRESENT_TO_PRESENT = 2;
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function MutableBag(s) {
|
|
||||||
this._items = s ? fromSet(s) : Immutable.Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
MutableBag.prototype.change = function (key, delta, clamp) {
|
|
||||||
var net;
|
|
||||||
({bag: this._items, net: net} = change(this._items, key, delta, clamp));
|
|
||||||
return net;
|
|
||||||
};
|
|
||||||
|
|
||||||
MutableBag.prototype.get = function (key) {
|
|
||||||
return get(this._items, key);
|
|
||||||
};
|
|
||||||
|
|
||||||
MutableBag.prototype.clear = function () {
|
|
||||||
this._items = Immutable.Map();
|
|
||||||
};
|
|
||||||
|
|
||||||
MutableBag.prototype.includes = function (key) {
|
|
||||||
return includes(this._items, key);
|
|
||||||
};
|
|
||||||
|
|
||||||
MutableBag.prototype.isEmpty = function () {
|
|
||||||
return this._items.isEmpty();
|
|
||||||
};
|
|
||||||
|
|
||||||
MutableBag.prototype.count = function () {
|
|
||||||
return this._items.count();
|
|
||||||
};
|
|
||||||
|
|
||||||
MutableBag.prototype.keys = function () {
|
|
||||||
return this._items.keys();
|
|
||||||
};
|
|
||||||
|
|
||||||
MutableBag.prototype.entries = function () {
|
|
||||||
return this._items.entries();
|
|
||||||
};
|
|
||||||
|
|
||||||
MutableBag.prototype.snapshot = function () {
|
|
||||||
return this._items;
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
const Bag = Immutable.Map;
|
|
||||||
|
|
||||||
function fromSet(s) {
|
|
||||||
return Bag().withMutations(function (b) {
|
|
||||||
for (let v of Immutable.Set(s)) {
|
|
||||||
b = b.set(fromJS(v), 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function change(bag, key, delta, clamp) {
|
|
||||||
let oldCount = get(bag, key);
|
|
||||||
let newCount = oldCount + delta;
|
|
||||||
if (clamp) {
|
|
||||||
newCount = Math.max(0, newCount);
|
|
||||||
}
|
|
||||||
if (newCount === 0) {
|
|
||||||
return {
|
|
||||||
bag: bag.remove(key),
|
|
||||||
net: (oldCount === 0) ? ABSENT_TO_ABSENT : PRESENT_TO_ABSENT
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
bag: bag.set(key, newCount),
|
|
||||||
net: (oldCount === 0) ? ABSENT_TO_PRESENT : PRESENT_TO_PRESENT
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get(bag, key) {
|
|
||||||
return bag.get(key, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function includes(bag, key) {
|
|
||||||
return get(bag, key) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
module.exports.PRESENT_TO_ABSENT = PRESENT_TO_ABSENT;
|
|
||||||
module.exports.ABSENT_TO_ABSENT = ABSENT_TO_ABSENT;
|
|
||||||
module.exports.ABSENT_TO_PRESENT = ABSENT_TO_PRESENT;
|
|
||||||
module.exports.PRESENT_TO_PRESENT = PRESENT_TO_PRESENT;
|
|
||||||
module.exports.MutableBag = MutableBag;
|
|
||||||
module.exports.Bag = Bag;
|
|
||||||
module.exports.fromSet = fromSet;
|
|
||||||
module.exports.change = change;
|
|
||||||
module.exports.get = get;
|
|
||||||
module.exports.includes = includes;
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
randomBytes: void 0,
|
|
||||||
};
|
|
|
@ -1,130 +0,0 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Property-based "dataflow"
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'dataflow.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
var Immutable = require("immutable");
|
|
||||||
var MapSet = require("./mapset.js");
|
|
||||||
|
|
||||||
function Graph() {
|
|
||||||
this.edgesForward = Immutable.Map();
|
|
||||||
this.edgesReverse = Immutable.Map();
|
|
||||||
this.damagedNodes = Immutable.Set();
|
|
||||||
this.currentSubjectId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Graph.prototype.withSubject = function (subjectId, f) {
|
|
||||||
var oldSubjectId = this.currentSubjectId;
|
|
||||||
this.currentSubjectId = subjectId;
|
|
||||||
var result;
|
|
||||||
try {
|
|
||||||
result = f();
|
|
||||||
} catch (e) {
|
|
||||||
this.currentSubjectId = oldSubjectId;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
this.currentSubjectId = oldSubjectId;
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
Graph.prototype.recordObservation = function (objectId) {
|
|
||||||
if (this.currentSubjectId) {
|
|
||||||
this.edgesForward = MapSet.add(this.edgesForward, objectId, this.currentSubjectId);
|
|
||||||
this.edgesReverse = MapSet.add(this.edgesReverse, this.currentSubjectId, objectId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Graph.prototype.recordDamage = function (objectId) {
|
|
||||||
this.damagedNodes = this.damagedNodes.add(objectId);
|
|
||||||
};
|
|
||||||
|
|
||||||
Graph.prototype.forgetSubject = function (subjectId) {
|
|
||||||
var self = this;
|
|
||||||
var subjectObjects = self.edgesReverse.get(subjectId) || Immutable.Set();
|
|
||||||
self.edgesReverse = self.edgesReverse.remove(subjectId);
|
|
||||||
subjectObjects.forEach(function (objectId) {
|
|
||||||
self.edgesForward = MapSet.remove(self.edgesForward, objectId, subjectId);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Graph.prototype.repairDamage = function (repairNode) {
|
|
||||||
var self = this;
|
|
||||||
var repairedThisRound = Immutable.Set();
|
|
||||||
while (true) {
|
|
||||||
var workSet = self.damagedNodes;
|
|
||||||
self.damagedNodes = Immutable.Set();
|
|
||||||
|
|
||||||
var alreadyDamaged = workSet.intersect(repairedThisRound);
|
|
||||||
if (!alreadyDamaged.isEmpty()) {
|
|
||||||
console.warn('Cyclic dependencies involving', alreadyDamaged);
|
|
||||||
}
|
|
||||||
|
|
||||||
workSet = workSet.subtract(repairedThisRound);
|
|
||||||
repairedThisRound = repairedThisRound.union(workSet);
|
|
||||||
|
|
||||||
if (workSet.isEmpty()) break;
|
|
||||||
|
|
||||||
workSet.forEach(function (objectId) {
|
|
||||||
var subjects = self.edgesForward.get(objectId) || Immutable.Set();
|
|
||||||
subjects.forEach(function (subjectId) {
|
|
||||||
self.forgetSubject(subjectId);
|
|
||||||
self.withSubject(subjectId, function () {
|
|
||||||
repairNode(subjectId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Graph.prototype.defineObservableProperty = function (obj, prop, value, maybeOptions) {
|
|
||||||
var graph = this;
|
|
||||||
var options = maybeOptions === void 0 ? {} : maybeOptions;
|
|
||||||
var objectId = options.objectId || '__' + prop;
|
|
||||||
Object.defineProperty(obj, prop, {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
get: function () {
|
|
||||||
graph.recordObservation(objectId);
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
set: function (newValue) {
|
|
||||||
if (!options.noopGuard || !options.noopGuard(value, newValue)) {
|
|
||||||
graph.recordDamage(objectId);
|
|
||||||
value = newValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
graph.recordDamage(objectId);
|
|
||||||
return objectId;
|
|
||||||
};
|
|
||||||
|
|
||||||
Graph.newScope = function (o) {
|
|
||||||
function O() {}
|
|
||||||
O.prototype = o;
|
|
||||||
return new O();
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
module.exports.Graph = Graph;
|
|
|
@ -1,873 +0,0 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'dataspace.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
const Immutable = require("immutable");
|
|
||||||
const Preserves = require("preserves");
|
|
||||||
// const debug = require("debug")("syndicate/core:dataspace");
|
|
||||||
|
|
||||||
const Skeleton = require('./skeleton.js');
|
|
||||||
const $Special = require('./special.js');
|
|
||||||
const Bag = require('./bag.js');
|
|
||||||
const Assertions = require('./assertions.js');
|
|
||||||
const Dataflow = require('./dataflow.js');
|
|
||||||
|
|
||||||
const PRIORITY = Object.freeze({
|
|
||||||
QUERY_HIGH: 0,
|
|
||||||
QUERY: 1,
|
|
||||||
QUERY_HANDLER: 2,
|
|
||||||
NORMAL: 3,
|
|
||||||
GC: 4,
|
|
||||||
IDLE: 5,
|
|
||||||
_count: 6
|
|
||||||
});
|
|
||||||
|
|
||||||
function Dataspace(bootProc) {
|
|
||||||
this.nextId = 0;
|
|
||||||
this.index = new Skeleton.Index();
|
|
||||||
this.dataflow = new Dataflow.Graph();
|
|
||||||
this.runnable = Immutable.List();
|
|
||||||
this.pendingActions = Immutable.List([
|
|
||||||
new ActionGroup(null, Immutable.List([new Spawn(null, bootProc, Immutable.Set())]))]);
|
|
||||||
this.activatedModules = Immutable.Set();
|
|
||||||
this.actors = Immutable.Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
Dataspace._currentFacet = null;
|
|
||||||
Dataspace._inScript = true;
|
|
||||||
|
|
||||||
Dataspace.BootSteps = Symbol.for('SyndicateBootSteps');
|
|
||||||
|
|
||||||
Dataspace.currentFacet = function () {
|
|
||||||
return Dataspace._currentFacet;
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.withNonScriptContext = function (thunk) {
|
|
||||||
let savedInScript = Dataspace._inScript;
|
|
||||||
Dataspace._inScript = false;
|
|
||||||
try {
|
|
||||||
return thunk();
|
|
||||||
} finally {
|
|
||||||
Dataspace._inScript = savedInScript;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.withCurrentFacet = function (facet, thunk) {
|
|
||||||
let savedFacet = Dataspace._currentFacet;
|
|
||||||
Dataspace._currentFacet = facet;
|
|
||||||
try {
|
|
||||||
// console.group('Facet', facet && facet.toString());
|
|
||||||
let result = thunk();
|
|
||||||
Dataspace._currentFacet = savedFacet;
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
let a = facet.actor;
|
|
||||||
a.abandonQueuedWork();
|
|
||||||
a._terminate(false);
|
|
||||||
Dataspace._currentFacet = savedFacet;
|
|
||||||
console.error('Actor ' + a.toString() + ' exited with exception:', e);
|
|
||||||
} finally {
|
|
||||||
// console.groupEnd();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.wrap = function (f) {
|
|
||||||
let savedFacet = Dataspace._currentFacet;
|
|
||||||
return function () {
|
|
||||||
let actuals = arguments;
|
|
||||||
Dataspace.withCurrentFacet(savedFacet, function () {
|
|
||||||
f.apply(savedFacet.fields, actuals);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.wrapExternal = function (f) {
|
|
||||||
let savedFacet = Dataspace._currentFacet;
|
|
||||||
let ac = savedFacet.actor;
|
|
||||||
return function () {
|
|
||||||
if (savedFacet.isLive) {
|
|
||||||
let actuals = arguments;
|
|
||||||
ac.dataspace.start();
|
|
||||||
ac.pushScript(function () {
|
|
||||||
Dataspace.withCurrentFacet(savedFacet, function () {
|
|
||||||
f.apply(this, actuals);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.backgroundTask = function (k) {
|
|
||||||
return Dataspace._currentFacet.actor.dataspace.ground().backgroundTask(k);
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.referenceField = function (obj, prop) {
|
|
||||||
if (!(prop in obj)) {
|
|
||||||
Dataspace._currentFacet.actor.dataspace.dataflow.recordObservation(
|
|
||||||
Immutable.List.of(obj, prop));
|
|
||||||
}
|
|
||||||
return obj[prop];
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.declareField = function (obj, prop, init) {
|
|
||||||
if (prop in obj) {
|
|
||||||
obj[prop] = init;
|
|
||||||
} else {
|
|
||||||
Dataspace._currentFacet.actor.dataspace.dataflow.defineObservableProperty(
|
|
||||||
obj,
|
|
||||||
prop,
|
|
||||||
init,
|
|
||||||
{
|
|
||||||
objectId: Immutable.List.of(obj, prop),
|
|
||||||
noopGuard: Preserves.is
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.deleteField = function (obj, prop) {
|
|
||||||
Dataspace._currentFacet.actor.dataspace.dataflow.recordDamage(Immutable.List.of(obj, prop));
|
|
||||||
return delete obj[prop];
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.runScripts = function () { // TODO: rename?
|
|
||||||
this.runPendingScripts();
|
|
||||||
this.performPendingActions();
|
|
||||||
return !this.runnable.isEmpty() || !this.pendingActions.isEmpty();
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.runPendingScripts = function () {
|
|
||||||
let runnable = this.runnable;
|
|
||||||
this.runnable = Immutable.List();
|
|
||||||
runnable.forEach((ac) => { ac.runPendingScripts(); /* TODO: rename? */ });
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.performPendingActions = function () {
|
|
||||||
let groups = this.pendingActions;
|
|
||||||
this.pendingActions = Immutable.List();
|
|
||||||
groups.forEach((group) => {
|
|
||||||
group.actions.forEach((action) => {
|
|
||||||
// console.log('[DATASPACE]', group.actor && group.actor.toString(), action);
|
|
||||||
action.perform(this, group.actor);
|
|
||||||
this.runPendingScripts();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.commitActions = function (ac, pending) {
|
|
||||||
this.pendingActions = this.pendingActions.push(new ActionGroup(ac, pending));
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.refreshAssertions = function () {
|
|
||||||
Dataspace.withNonScriptContext(() => {
|
|
||||||
this.dataflow.repairDamage((subjectId) => {
|
|
||||||
let [facet, eid] = subjectId;
|
|
||||||
if (facet.isLive) { // TODO: necessary test, or tautological?
|
|
||||||
let ac = facet.actor;
|
|
||||||
Dataspace.withCurrentFacet(facet, () => {
|
|
||||||
facet.endpoints.get(eid).refresh(this, ac, facet);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.addActor = function (name, bootProc, initialAssertions, parentActor) {
|
|
||||||
let ac = new Actor(this, name, initialAssertions, parentActor && parentActor.id);
|
|
||||||
// debug('Spawn', ac && ac.toString());
|
|
||||||
this.applyPatch(ac, ac.adhocAssertions.snapshot());
|
|
||||||
ac.addFacet(null, () => {
|
|
||||||
// Root facet is a dummy "system" facet that exists to hold
|
|
||||||
// one-or-more "user" "root" facets.
|
|
||||||
ac.addFacet(Dataspace._currentFacet, bootProc);
|
|
||||||
// ^ The "true root", user-visible facet.
|
|
||||||
initialAssertions.forEach((a) => { ac.adhocRetract(a); });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.applyPatch = function (ac, delta) {
|
|
||||||
// if (!delta.isEmpty()) debug('applyPatch BEGIN', ac && ac.toString());
|
|
||||||
let removals = [];
|
|
||||||
delta.forEach((count, a) => {
|
|
||||||
if (a !== void 0) {
|
|
||||||
if (count > 0) {
|
|
||||||
// debug('applyPatch +', a && a.toString());
|
|
||||||
this.adjustIndex(a, count);
|
|
||||||
} else {
|
|
||||||
removals.push([count, a]);
|
|
||||||
}
|
|
||||||
if (ac) ac.cleanupChanges.change(a, -count);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
removals.forEach(([count, a]) => {
|
|
||||||
// debug('applyPatch -', a && a.toString());
|
|
||||||
this.adjustIndex(a, count);
|
|
||||||
});
|
|
||||||
// if (!delta.isEmpty()) debug('applyPatch END');
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.sendMessage = function (m, sendingActor) {
|
|
||||||
// debug('sendMessage', sendingActor && sendingActor.toString(), m.toString());
|
|
||||||
this.index.sendMessage(m);
|
|
||||||
// this.index.sendMessage(m, (leaf, _m) => {
|
|
||||||
// sendingActor.touchedTopics = sendingActor.touchedTopics.add(leaf);
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.adjustIndex = function (a, count) {
|
|
||||||
return this.index.adjustAssertion(a, count);
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.subscribe = function (handler) {
|
|
||||||
this.index.addHandler(handler, handler.callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.unsubscribe = function (handler) {
|
|
||||||
this.index.removeHandler(handler, handler.callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype.endpointHook = function (facet, endpoint) {
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype._debugString = function (outerIndent) {
|
|
||||||
const pieces = [];
|
|
||||||
pieces.push(this.index.root._debugString(outerIndent));
|
|
||||||
outerIndent = outerIndent || '\n';
|
|
||||||
pieces.push(outerIndent + 'FACET TREE');
|
|
||||||
this.actors.forEach((a) => {
|
|
||||||
pieces.push(outerIndent + ' ' + a.toString());
|
|
||||||
function walkFacet(indent, f) {
|
|
||||||
pieces.push(indent + f.toString());
|
|
||||||
f.endpoints.forEach((ep) => {
|
|
||||||
pieces.push(indent + ' - ' + ep.id + ': ' + (ep.assertion && ep.assertion.toString()));
|
|
||||||
});
|
|
||||||
f.children.forEach((child) => { walkFacet(indent + ' ', child); });
|
|
||||||
}
|
|
||||||
a.rootFacet.children.forEach((child) => { walkFacet(outerIndent + ' ', child); });
|
|
||||||
});
|
|
||||||
pieces.push(outerIndent + 'ACTORS');
|
|
||||||
this.actors.forEach((a) => pieces.push(outerIndent + ' ' + a.toString()));
|
|
||||||
return pieces.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.prototype._dotGraph = function () {
|
|
||||||
let id = 0;
|
|
||||||
const assertionIds = {};
|
|
||||||
|
|
||||||
const nodes = [];
|
|
||||||
const edges = [];
|
|
||||||
const pieces = [];
|
|
||||||
|
|
||||||
function emitNode(type, id, _label, attrs) {
|
|
||||||
const label = _str(_label);
|
|
||||||
pieces.push(`\n ${id} [label=${JSON.stringify(label)}];`);
|
|
||||||
nodes.push(Object.assign({}, attrs || {}, {type, id, label}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitEdge(source, target, maybeDir) {
|
|
||||||
pieces.push(`\n ${source} -- ${target} [dir=${maybeDir || 'none'}];`);
|
|
||||||
edges.push({source, target, dir: maybeDir || 'none'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _aId(aStr) {
|
|
||||||
// if (aStr.startsWith('observe(Request(') || aStr.startsWith('Request(')) return null;
|
|
||||||
// if (aStr.startsWith('observe(Connection(') || aStr.startsWith('Connection(')) return null;
|
|
||||||
if (!(aStr in assertionIds)) assertionIds[aStr] = id++;
|
|
||||||
return assertionIds[aStr];
|
|
||||||
}
|
|
||||||
|
|
||||||
let topics = Immutable.Map();
|
|
||||||
function topicForLeaf(leaf) {
|
|
||||||
if (topics.has(leaf)) {
|
|
||||||
return topics.get(leaf);
|
|
||||||
} else {
|
|
||||||
const topic = {id: id++, hasEmitter: false, senders: {}, inbound: {}, outbound: {}};
|
|
||||||
topics = topics.set(leaf, topic);
|
|
||||||
return topic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _str(a) {
|
|
||||||
return '' + a;
|
|
||||||
}
|
|
||||||
|
|
||||||
pieces.push('graph G {');
|
|
||||||
pieces.push('\n overlap=false;');
|
|
||||||
|
|
||||||
this.actors.forEach((ac) => {
|
|
||||||
const acId = ac.id;
|
|
||||||
emitNode('actor', `ac_${acId}`, ac.toString());
|
|
||||||
if (this.actors.has(ac.parentId)) {
|
|
||||||
emitEdge(`ac_${ac.parentId}`, `ac_${acId}`, 'forward');
|
|
||||||
}
|
|
||||||
// ac.touchedTopics.forEach((leaf) => {
|
|
||||||
// const topic = topicForLeaf(leaf);
|
|
||||||
// topic.senders[acId] = true;
|
|
||||||
// topic.hasEmitter = true;
|
|
||||||
// });
|
|
||||||
// ac.touchedTopics = Immutable.Set();
|
|
||||||
function walkFacet(parent) {
|
|
||||||
return (f) => {
|
|
||||||
const facetId = id++;
|
|
||||||
emitNode('facet', `facet_${facetId}`, `Facet ${f.id}`, {parent});
|
|
||||||
emitEdge(parent, `facet_${facetId}`);
|
|
||||||
f.endpoints.forEach((ep) => {
|
|
||||||
if (ep.assertion !== void 0) {
|
|
||||||
const aId = _aId(_str(ep.assertion));
|
|
||||||
if (aId) {
|
|
||||||
emitNode('endpoint', `ep_${ep.id}`, ep.id);
|
|
||||||
emitEdge(`facet_${facetId}`, `ep_${ep.id}`);
|
|
||||||
emitEdge(`ep_${ep.id}`, `assn_${aId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
f.children.forEach(walkFacet(`facet_${facetId}`));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
ac.rootFacet.children.forEach(walkFacet(`ac_${acId}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
function walkNode(n) {
|
|
||||||
n.edges.forEach((table) => table.forEach(walkNode));
|
|
||||||
n.continuation.leafMap.forEach((cvMap) => cvMap.forEach((leaf) => {
|
|
||||||
const topic = topicForLeaf(leaf);
|
|
||||||
leaf.cachedAssertions.forEach((observed_assertion) => {
|
|
||||||
const observed_assertion_id = _aId(_str(observed_assertion));
|
|
||||||
if (observed_assertion_id) {
|
|
||||||
topic.inbound[observed_assertion_id] = true;
|
|
||||||
topic.hasEmitter = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
leaf.handlerMap.forEach((handler) => {
|
|
||||||
handler.callbacks.forEach((cb) => {
|
|
||||||
const observing_assertion_id = _aId(_str(cb.__endpoint.handler.assertion));
|
|
||||||
if (observing_assertion_id) {
|
|
||||||
topic.outbound[observing_assertion_id] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
walkNode(this.index.root);
|
|
||||||
|
|
||||||
for (const a in assertionIds) {
|
|
||||||
emitNode('assertion', `assn_${assertionIds[a]}`, a);
|
|
||||||
}
|
|
||||||
|
|
||||||
topics.forEach((topic) => {
|
|
||||||
if (topic.hasEmitter) {
|
|
||||||
emitNode('topic', 'topic_' + topic.id, ''); // `Topic ${topic.id}`);
|
|
||||||
for (const acId in topic.senders) {
|
|
||||||
emitEdge(`ac_${acId}`, `topic_${topic.id}`, 'forward');
|
|
||||||
}
|
|
||||||
for (const aId in topic.inbound) {
|
|
||||||
emitEdge(`assn_${aId}`, `topic_${topic.id}`, 'forward');
|
|
||||||
}
|
|
||||||
for (const aId in topic.outbound) {
|
|
||||||
emitEdge(`topic_${topic.id}`, `assn_${aId}`, 'forward');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pieces.push('\n}');
|
|
||||||
|
|
||||||
// require('fs').writeFileSync('d.json', 'var dataspaceContents = ' + JSON.stringify({nodes, edges}, null, 2));
|
|
||||||
|
|
||||||
return pieces.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
function Actor(dataspace, name, initialAssertions, parentActorId) {
|
|
||||||
this.id = dataspace.nextId++;
|
|
||||||
this.dataspace = dataspace;
|
|
||||||
this.name = name;
|
|
||||||
this.rootFacet = null;
|
|
||||||
this.isRunnable = false;
|
|
||||||
this.pendingScripts = [];
|
|
||||||
for (let i = 0; i < PRIORITY._count; i++) { this.pendingScripts.push(Immutable.List()); }
|
|
||||||
this.pendingActions = Immutable.List();
|
|
||||||
this.adhocAssertions = new Bag.MutableBag(initialAssertions); // no negative counts allowed
|
|
||||||
this.cleanupChanges = new Bag.MutableBag(); // negative counts allowed!
|
|
||||||
this.parentId = parentActorId;
|
|
||||||
// this.touchedTopics = Immutable.Set();
|
|
||||||
dataspace.actors = dataspace.actors.set(this.id, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actor.prototype.runPendingScripts = function () {
|
|
||||||
while (true) {
|
|
||||||
let script = this.popNextScript();
|
|
||||||
if (!script) break;
|
|
||||||
script();
|
|
||||||
this.dataspace.refreshAssertions();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isRunnable = false;
|
|
||||||
let pending = this.pendingActions;
|
|
||||||
if (!pending.isEmpty()) {
|
|
||||||
this.pendingActions = Immutable.List();
|
|
||||||
this.dataspace.commitActions(this, pending);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.popNextScript = function () {
|
|
||||||
let scripts = this.pendingScripts;
|
|
||||||
for (let i = 0; i < PRIORITY._count; i++) {
|
|
||||||
let q = scripts[i];
|
|
||||||
if (!q.isEmpty()) {
|
|
||||||
scripts[i] = q.shift();
|
|
||||||
return q.first();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.abandonQueuedWork = function () {
|
|
||||||
this.pendingActions = Immutable.List();
|
|
||||||
for (let i = 0; i < PRIORITY._count; i++) { this.pendingScripts[i] = Immutable.List(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.scheduleScript = function (unwrappedThunk, priority) {
|
|
||||||
this.pushScript(Dataspace.wrap(unwrappedThunk), priority);
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.pushScript = function (wrappedThunk, priority) {
|
|
||||||
// The wrappedThunk must already have code for ensuring
|
|
||||||
// _currentFacet is correct inside it. Compare with scheduleScript.
|
|
||||||
if (priority === void 0) {
|
|
||||||
priority = PRIORITY.NORMAL;
|
|
||||||
}
|
|
||||||
if (!this.isRunnable) {
|
|
||||||
this.isRunnable = true;
|
|
||||||
this.dataspace.runnable = this.dataspace.runnable.push(this);
|
|
||||||
}
|
|
||||||
this.pendingScripts[priority] = this.pendingScripts[priority].push(wrappedThunk);
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.addFacet = function (parentFacet, bootProc, checkInScript) {
|
|
||||||
if (checkInScript === true && !Dataspace._inScript) {
|
|
||||||
throw new Error("Cannot add facet outside script; are you missing a `react { ... }`?");
|
|
||||||
}
|
|
||||||
let f = new Facet(this, parentFacet);
|
|
||||||
Dataspace.withCurrentFacet(f, () => {
|
|
||||||
Dataspace.withNonScriptContext(() => {
|
|
||||||
bootProc.call(f.fields);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.pushScript(() => {
|
|
||||||
if ((parentFacet && !parentFacet.isLive) || f.isInert()) {
|
|
||||||
f._terminate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype._terminate = function (emitPatches) {
|
|
||||||
// Abruptly terminates an entire actor, without running stop-scripts etc.
|
|
||||||
if (emitPatches) {
|
|
||||||
this.pushScript(() => {
|
|
||||||
this.adhocAssertions.snapshot().forEach((_count, a) => { this.retract(a); });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.rootFacet) {
|
|
||||||
this.rootFacet._abort(emitPatches);
|
|
||||||
}
|
|
||||||
this.pushScript(() => { this.enqueueScriptAction(new Quit()); });
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.enqueueScriptAction = function (action) {
|
|
||||||
this.pendingActions = this.pendingActions.push(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.pendingPatch = function () {
|
|
||||||
if (!this.pendingActions.isEmpty()) {
|
|
||||||
let p = this.pendingActions.last();
|
|
||||||
if (p instanceof Patch) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let p = new Patch(Bag.Bag());
|
|
||||||
this.enqueueScriptAction(p);
|
|
||||||
return p;
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.assert = function (a) { this.pendingPatch().adjust(a, +1); };
|
|
||||||
Actor.prototype.retract = function (a) { this.pendingPatch().adjust(a, -1); };
|
|
||||||
|
|
||||||
Actor.prototype.adhocRetract = function (a) {
|
|
||||||
a = Preserves.fromJS(a);
|
|
||||||
if (this.adhocAssertions.change(a, -1, true) === Bag.PRESENT_TO_ABSENT) {
|
|
||||||
this.retract(a);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.adhocAssert = function (a) {
|
|
||||||
a = Preserves.fromJS(a);
|
|
||||||
if (this.adhocAssertions.change(a, +1) === Bag.ABSENT_TO_PRESENT) {
|
|
||||||
this.assert(a);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Actor.prototype.toString = function () {
|
|
||||||
let s = 'Actor(' + this.id;
|
|
||||||
if (this.name !== void 0 && this.name !== null) s = s + ',' + this.name.toString();
|
|
||||||
return s + ')';
|
|
||||||
};
|
|
||||||
|
|
||||||
function Patch(changes) {
|
|
||||||
this.changes = changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
Patch.prototype.perform = function (ds, ac) {
|
|
||||||
ds.applyPatch(ac, this.changes);
|
|
||||||
};
|
|
||||||
|
|
||||||
Patch.prototype.adjust = function (a, count) {
|
|
||||||
if (a !== void 0) {
|
|
||||||
var _net;
|
|
||||||
({bag: this.changes, net: _net} = Bag.change(this.changes, Preserves.fromJS(a), count));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function Message(body) {
|
|
||||||
this.body = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message.prototype.perform = function (ds, ac) {
|
|
||||||
if (this.body !== void 0) {
|
|
||||||
ds.sendMessage(Preserves.fromJS(this.body), ac);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.send = function (body) {
|
|
||||||
if (!Dataspace._inScript) {
|
|
||||||
throw new Error("Cannot `send` during facet setup; are you missing an `on start { ... }`?");
|
|
||||||
}
|
|
||||||
Dataspace._currentFacet.enqueueScriptAction(new Message(body));
|
|
||||||
};
|
|
||||||
|
|
||||||
function Spawn(name, bootProc, initialAssertions) {
|
|
||||||
this.name = name;
|
|
||||||
this.bootProc = bootProc;
|
|
||||||
this.initialAssertions = initialAssertions || Immutable.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
Spawn.prototype.perform = function (ds, ac) {
|
|
||||||
ds.addActor(this.name, this.bootProc, this.initialAssertions, ac);
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.spawn = function (name, bootProc, initialAssertions) {
|
|
||||||
if (!Dataspace._inScript) {
|
|
||||||
throw new Error("Cannot `spawn` during facet setup; are you missing an `on start { ... }`?");
|
|
||||||
}
|
|
||||||
Dataspace._currentFacet.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions));
|
|
||||||
};
|
|
||||||
|
|
||||||
function Quit() { // TODO: rename? Perhaps to Cleanup?
|
|
||||||
// Pseudo-action - not for userland use.
|
|
||||||
}
|
|
||||||
|
|
||||||
Quit.prototype.perform = function (ds, ac) {
|
|
||||||
ds.applyPatch(ac, ac.cleanupChanges.snapshot());
|
|
||||||
ds.actors = ds.actors.remove(ac.id);
|
|
||||||
// debug('Quit', ac && ac.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
function DeferredTurn(continuation) {
|
|
||||||
this.continuation = continuation;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeferredTurn.prototype.perform = function (ds, ac) {
|
|
||||||
// debug('DeferredTurn', ac && ac.toString());
|
|
||||||
ac.pushScript(this.continuation);
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.deferTurn = function (continuation) {
|
|
||||||
if (!Dataspace._inScript) {
|
|
||||||
throw new Error("Cannot defer turn during facet setup; are you missing an `on start { ... }`?");
|
|
||||||
}
|
|
||||||
Dataspace._currentFacet.enqueueScriptAction(new DeferredTurn(Dataspace.wrap(continuation)));
|
|
||||||
};
|
|
||||||
|
|
||||||
function Activation(mod) {
|
|
||||||
this.mod = mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
Activation.prototype.perform = function (ds, ac) {
|
|
||||||
if (!ds.activatedModules.includes(this.mod)) {
|
|
||||||
// debug('Activation', this.mod.id);
|
|
||||||
ds.activatedModules = ds.activatedModules.add(this.mod);
|
|
||||||
this.mod.exports[Dataspace.BootSteps].steps.forEach((a) => {
|
|
||||||
// console.log('[ACTIVATION]', ac && ac.toString(), a);
|
|
||||||
a.perform(ds, ac);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Dataspace.activate = function (modExports) {
|
|
||||||
let { module } = modExports[Dataspace.BootSteps] || {};
|
|
||||||
if (module) {
|
|
||||||
Dataspace._currentFacet.enqueueScriptAction(new Activation(module));
|
|
||||||
}
|
|
||||||
return modExports;
|
|
||||||
};
|
|
||||||
|
|
||||||
function ActionGroup(actor, actions) {
|
|
||||||
this.actor = actor;
|
|
||||||
this.actions = actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Facet(actor, parent) {
|
|
||||||
this.id = actor.dataspace.nextId++;
|
|
||||||
this.isLive = true;
|
|
||||||
this.actor = actor;
|
|
||||||
this.parent = parent;
|
|
||||||
this.endpoints = Immutable.Map();
|
|
||||||
this.stopScripts = Immutable.List();
|
|
||||||
this.children = Immutable.Set();
|
|
||||||
if (parent) {
|
|
||||||
parent.children = parent.children.add(this);
|
|
||||||
this.fields = Dataflow.Graph.newScope(parent.fields);
|
|
||||||
} else {
|
|
||||||
if (actor.rootFacet) {
|
|
||||||
throw new Error("INVARIANT VIOLATED: Attempt to add second root facet");
|
|
||||||
}
|
|
||||||
actor.rootFacet = this;
|
|
||||||
this.fields = Dataflow.Graph.newScope({});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Facet.prototype._abort = function (emitPatches) {
|
|
||||||
this.isLive = false;
|
|
||||||
this.children.forEach((child) => { child._abort(emitPatches); });
|
|
||||||
this.retractAssertionsAndSubscriptions(emitPatches);
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.retractAssertionsAndSubscriptions = function (emitPatches) {
|
|
||||||
let ac = this.actor;
|
|
||||||
let ds = ac.dataspace;
|
|
||||||
ac.pushScript(() => {
|
|
||||||
this.endpoints.forEach((ep) => {
|
|
||||||
ep.destroy(ds, ac, this, emitPatches);
|
|
||||||
});
|
|
||||||
this.endpoints = Immutable.Map();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.isInert = function () {
|
|
||||||
return this.endpoints.isEmpty() && this.children.isEmpty();
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype._terminate = function () {
|
|
||||||
if (this.isLive) {
|
|
||||||
let ac = this.actor;
|
|
||||||
let parent = this.parent;
|
|
||||||
if (parent) {
|
|
||||||
parent.children = parent.children.remove(this);
|
|
||||||
} else {
|
|
||||||
ac.rootFacet = null;
|
|
||||||
}
|
|
||||||
this.isLive = false;
|
|
||||||
|
|
||||||
this.children.forEach((child) => { child._terminate(); });
|
|
||||||
|
|
||||||
// Run stop-scripts after terminating children. This means that
|
|
||||||
// children's stop-scripts run before ours.
|
|
||||||
ac.pushScript(() => {
|
|
||||||
Dataspace.withCurrentFacet(this, () => {
|
|
||||||
this.stopScripts.forEach((s) => { s.call(this.fields); });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.retractAssertionsAndSubscriptions(true);
|
|
||||||
ac.pushScript(() => {
|
|
||||||
if (parent) {
|
|
||||||
if (parent.isInert()) {
|
|
||||||
parent._terminate();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ac._terminate(true);
|
|
||||||
}
|
|
||||||
}, PRIORITY.GC);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.stop = function (continuation) {
|
|
||||||
Dataspace.withCurrentFacet(this.parent, () => {
|
|
||||||
this.actor.scheduleScript(() => {
|
|
||||||
this._terminate();
|
|
||||||
this.actor.scheduleScript(() => {
|
|
||||||
if (continuation) {
|
|
||||||
continuation.call(this.fields); // TODO: is this the correct scope to use??
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.addStartScript = function (s) {
|
|
||||||
if (Dataspace._inScript) {
|
|
||||||
throw new Error("Cannot `on start` outside facet setup");
|
|
||||||
}
|
|
||||||
this.actor.scheduleScript(s);
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.addStopScript = function (s) {
|
|
||||||
if (Dataspace._inScript) {
|
|
||||||
throw new Error("Cannot `on stop` outside facet setup");
|
|
||||||
}
|
|
||||||
this.stopScripts = this.stopScripts.push(s);
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.addEndpoint = function (updateFun, isDynamic) {
|
|
||||||
const ep = new Endpoint(this, isDynamic === void 0 ? true : isDynamic, updateFun);
|
|
||||||
this.actor.dataspace.endpointHook(this, ep);
|
|
||||||
return ep;
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype._addRawObserverEndpoint = function (specThunk, callbacks) {
|
|
||||||
this.addEndpoint(() => {
|
|
||||||
const spec = specThunk();
|
|
||||||
if (spec === void 0) {
|
|
||||||
return [void 0, null];
|
|
||||||
} else {
|
|
||||||
const analysis = Skeleton.analyzeAssertion(spec);
|
|
||||||
analysis.callback = Dataspace.wrap((evt, vs) => {
|
|
||||||
let cb = null;
|
|
||||||
switch (evt) {
|
|
||||||
case Skeleton.EVENT_ADDED: cb = callbacks.add; break;
|
|
||||||
case Skeleton.EVENT_REMOVED: cb = callbacks.del; break;
|
|
||||||
case Skeleton.EVENT_MESSAGE: cb = callbacks.msg; break;
|
|
||||||
}
|
|
||||||
if (cb) cb(vs);
|
|
||||||
});
|
|
||||||
return [Assertions.Observe(spec), analysis];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.addObserverEndpoint = function (specThunk, callbacks) {
|
|
||||||
const self = this;
|
|
||||||
function scriptify(f) {
|
|
||||||
return f && ((vs) => self.actor.scheduleScript(() => f(vs)));
|
|
||||||
}
|
|
||||||
this._addRawObserverEndpoint(specThunk, {
|
|
||||||
add: scriptify(callbacks.add),
|
|
||||||
del: scriptify(callbacks.del),
|
|
||||||
msg: scriptify(callbacks.msg),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.addDataflow = function (subjectFun, priority) {
|
|
||||||
return this.addEndpoint(() => {
|
|
||||||
let subjectId = this.actor.dataspace.dataflow.currentSubjectId;
|
|
||||||
this.actor.scheduleScript(() => {
|
|
||||||
if (this.isLive) {
|
|
||||||
this.actor.dataspace.dataflow.withSubject(subjectId, () => subjectFun.call(this.fields));
|
|
||||||
}
|
|
||||||
}, priority);
|
|
||||||
return [void 0, null];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.enqueueScriptAction = function (action) {
|
|
||||||
this.actor.enqueueScriptAction(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
Facet.prototype.toString = function () {
|
|
||||||
let s = 'Facet(' + this.actor.id;
|
|
||||||
if (this.actor.name !== void 0 && this.actor.name !== null) {
|
|
||||||
s = s + ',' + this.actor.name.toString();
|
|
||||||
}
|
|
||||||
s = s + ',' + this.id;
|
|
||||||
let f = this.parent;
|
|
||||||
while (f != null) {
|
|
||||||
s = s + ':' + f.id;
|
|
||||||
f = f.parent;
|
|
||||||
}
|
|
||||||
return s + ')';
|
|
||||||
};
|
|
||||||
|
|
||||||
function ActionCollector() {
|
|
||||||
this.actions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionCollector.prototype.enqueueScriptAction = function (a) {
|
|
||||||
this.actions.push(a);
|
|
||||||
};
|
|
||||||
|
|
||||||
function Endpoint(facet, isDynamic, updateFun) {
|
|
||||||
if (Dataspace._inScript) {
|
|
||||||
throw new Error("Cannot add endpoint in script; are you missing a `react { ... }`?");
|
|
||||||
}
|
|
||||||
let ac = facet.actor;
|
|
||||||
let ds = ac.dataspace;
|
|
||||||
this.id = ds.nextId++;
|
|
||||||
this.updateFun = updateFun;
|
|
||||||
let [initialAssertion, initialHandler] = ds.dataflow.withSubject(
|
|
||||||
isDynamic ? [facet, this.id] : false,
|
|
||||||
() => updateFun.call(facet.fields));
|
|
||||||
this._install(ds, ac, initialAssertion, initialHandler);
|
|
||||||
facet.endpoints = facet.endpoints.set(this.id, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
Endpoint.prototype._install = function (ds, ac, assertion, handler) {
|
|
||||||
this.assertion = assertion;
|
|
||||||
this.handler = handler;
|
|
||||||
ac.assert(this.assertion);
|
|
||||||
if (this.handler) {
|
|
||||||
this.handler.callback.__endpoint = this; // for reflection/debugging
|
|
||||||
ds.subscribe(this.handler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Endpoint.prototype._uninstall = function (ds, ac, emitPatches) {
|
|
||||||
if (emitPatches) { ac.retract(this.assertion); }
|
|
||||||
if (this.handler) { ds.unsubscribe(this.handler); }
|
|
||||||
};
|
|
||||||
|
|
||||||
Endpoint.prototype.refresh = function (ds, ac, facet) {
|
|
||||||
let [newAssertion, newHandler] = this.updateFun.call(facet.fields);
|
|
||||||
if (newAssertion !== void 0) newAssertion = Preserves.fromJS(newAssertion);
|
|
||||||
if (!Immutable.is(newAssertion, this.assertion)) {
|
|
||||||
this._uninstall(ds, ac, true);
|
|
||||||
this._install(ds, ac, newAssertion, newHandler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Endpoint.prototype.destroy = function (ds, ac, facet, emitPatches) {
|
|
||||||
ds.dataflow.forgetSubject([facet, this.id]);
|
|
||||||
// ^ TODO: this won't work because of object identity problems! Why
|
|
||||||
// does the Racket implementation do this, when the old JS
|
|
||||||
// implementation doesn't?
|
|
||||||
facet.endpoints = facet.endpoints.remove(this.id);
|
|
||||||
this._uninstall(ds, ac, emitPatches);
|
|
||||||
};
|
|
||||||
|
|
||||||
Endpoint.prototype.toString = function () {
|
|
||||||
return 'Endpoint(' + this.id + ')';
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
module.exports.Dataspace = Dataspace;
|
|
||||||
module.exports.ActionCollector = ActionCollector;
|
|
||||||
module.exports.PRIORITY = PRIORITY;
|
|
|
@ -1,135 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'ground.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
const Immutable = require('immutable');
|
|
||||||
const Dataspace = require('./dataspace.js').Dataspace;
|
|
||||||
const Worker = require('./worker');
|
|
||||||
|
|
||||||
function Ground(bootProc) {
|
|
||||||
Dataspace.call(this, bootProc);
|
|
||||||
this.stepperId = null;
|
|
||||||
this.stepping = false;
|
|
||||||
this.startingFuel = 1000;
|
|
||||||
this.stopHandlers = [];
|
|
||||||
this.backgroundTaskCount = 0;
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window._ground = this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ground.prototype = new Dataspace(null);
|
|
||||||
|
|
||||||
Ground._resolved = Promise.resolve();
|
|
||||||
Ground.laterCall = function (thunk) {
|
|
||||||
Ground._resolved.then(() => {
|
|
||||||
Error.stackTraceLimit = 100;
|
|
||||||
try {
|
|
||||||
thunk();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("SYNDICATE/JS INTERNAL ERROR", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Ground.prototype.backgroundTask = function (k) {
|
|
||||||
const ground = this;
|
|
||||||
let active = true;
|
|
||||||
ground.backgroundTaskCount++;
|
|
||||||
function finish() {
|
|
||||||
if (active) {
|
|
||||||
ground.backgroundTaskCount--;
|
|
||||||
active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return k ? k(finish) : finish;
|
|
||||||
};
|
|
||||||
|
|
||||||
Ground.prototype.start = function () {
|
|
||||||
if (!this.stepperId) {
|
|
||||||
this.stepperId = Ground.laterCall(() => {
|
|
||||||
this.stepperId = null;
|
|
||||||
this._step();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this; // allows chaining start() immediately after construction
|
|
||||||
};
|
|
||||||
|
|
||||||
Ground.prototype.ground = function () {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
Ground.prototype._step = function () {
|
|
||||||
this.stepping = true;
|
|
||||||
try {
|
|
||||||
let stillBusy = false;
|
|
||||||
for (var fuel = this.startingFuel; fuel > 0; fuel--) {
|
|
||||||
stillBusy = this.runScripts();
|
|
||||||
if (!stillBusy) break;
|
|
||||||
}
|
|
||||||
if (stillBusy) {
|
|
||||||
this.start();
|
|
||||||
} else {
|
|
||||||
if (!this.backgroundTaskCount) {
|
|
||||||
this.stopHandlers.forEach((h) => h(this));
|
|
||||||
this.stopHandlers = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.stepping = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ground.prototype.stop = function () {
|
|
||||||
if (this.stepperId) {
|
|
||||||
clearTimeout(this.stepperId);
|
|
||||||
this.stepperId = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ground.prototype.addStopHandler = function (h) {
|
|
||||||
this.stopHandlers.push(h);
|
|
||||||
};
|
|
||||||
|
|
||||||
function bootModule(mod, k) {
|
|
||||||
let g = new Ground(() => {
|
|
||||||
Worker.spawnWorkerRelay();
|
|
||||||
if (Dataspace.BootSteps in mod) {
|
|
||||||
// It's really an exports dict, not a module.
|
|
||||||
Dataspace.activate(mod);
|
|
||||||
} else if ('exports' in mod) {
|
|
||||||
// It's probably a module.
|
|
||||||
Dataspace.activate(mod.exports);
|
|
||||||
} else {
|
|
||||||
const e = new Error("Cannot boot Syndicate module");
|
|
||||||
e.irritant = mod;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
document.addEventListener("DOMContentLoaded", (e) => {
|
|
||||||
g.start();
|
|
||||||
if (k) k(g);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
process.on('SIGQUIT', () => {
|
|
||||||
console.log('---------------------------------------------------------------------------');
|
|
||||||
console.log(g._debugString());
|
|
||||||
|
|
||||||
g._dotGraph();
|
|
||||||
// const child_process = require('child_process');
|
|
||||||
// const sp = child_process.spawn('dotpreview.sh 100% neato', {
|
|
||||||
// shell: true,
|
|
||||||
// stdio: ['pipe', 'ignore', 'ignore']
|
|
||||||
// });
|
|
||||||
// sp.stdin.end(g._dotGraph());
|
|
||||||
});
|
|
||||||
g.start();
|
|
||||||
if (k) k(g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.Ground = Ground;
|
|
||||||
module.exports.bootModule = bootModule;
|
|
|
@ -1,68 +0,0 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'index.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
const Skeleton = require('./skeleton.js');
|
|
||||||
const RandomID = require('./randomid.js');
|
|
||||||
const Dataspace = require('./dataspace.js');
|
|
||||||
const Ground = require('./ground.js');
|
|
||||||
const Assertions = require('./assertions.js');
|
|
||||||
const Relay = require('./relay.js');
|
|
||||||
const Bag = require('./bag.js');
|
|
||||||
const Worker = require('./worker.js');
|
|
||||||
|
|
||||||
Object.assign(module.exports, require("preserves"));
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
module.exports.Bag = Bag;
|
|
||||||
module.exports.Skeleton = Skeleton;
|
|
||||||
module.exports.RandomID = RandomID;
|
|
||||||
|
|
||||||
module.exports._Dataspace = Dataspace;
|
|
||||||
module.exports.Dataspace = Dataspace.Dataspace;
|
|
||||||
module.exports.currentFacet = Dataspace.Dataspace.currentFacet;
|
|
||||||
module.exports.Ground = Ground;
|
|
||||||
|
|
||||||
module.exports._Assertions = Assertions;
|
|
||||||
module.exports.Discard = Assertions.Discard;
|
|
||||||
module.exports.Capture = Assertions.Capture;
|
|
||||||
module.exports.Observe = Assertions.Observe;
|
|
||||||
module.exports.Seal = Assertions.Seal;
|
|
||||||
module.exports.Inbound = Assertions.Inbound;
|
|
||||||
module.exports.Outbound = Assertions.Outbound;
|
|
||||||
module.exports.Instance = Assertions.Instance;
|
|
||||||
|
|
||||||
module.exports.$QuitDataspace = Relay.$QuitDataspace;
|
|
||||||
module.exports.NestedDataspace = Relay.NestedDataspace;
|
|
||||||
module.exports.inNestedDataspace = Relay.inNestedDataspace;
|
|
||||||
|
|
||||||
module.exports.bootModule = Ground.bootModule;
|
|
||||||
module.exports.spawnWorker = Worker.spawnWorker;
|
|
||||||
|
|
||||||
// These aren't so much "Universal" as they are "VM-wide-unique".
|
|
||||||
let uuidIndex = 0;
|
|
||||||
let uuidInstance = RandomID.randomId(8);
|
|
||||||
module.exports.genUuid = function (prefix) {
|
|
||||||
return (prefix || '__@syndicate') + '_' + uuidInstance + '_' + uuidIndex++;
|
|
||||||
};
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export * from 'preserves';
|
||||||
|
|
||||||
|
export * from './runtime/randomid.js';
|
||||||
|
export * from './runtime/assertions.js';
|
||||||
|
export * from './runtime/bag.js';
|
||||||
|
export * as Skeleton from './runtime/skeleton.js';
|
||||||
|
export * from './runtime/dataspace.js';
|
||||||
|
export * from './runtime/ground.js';
|
||||||
|
export * from './runtime/relay.js';
|
||||||
|
// export * as Worker from './runtime/worker.js';
|
||||||
|
|
||||||
|
import { randomId } from './runtime/randomid.js';
|
||||||
|
|
||||||
|
// These aren't so much "Universal" as they are "VM-wide-unique".
|
||||||
|
let uuidIndex = 0;
|
||||||
|
let uuidInstance = randomId(8);
|
||||||
|
export function genUuid(prefix: string = '__@syndicate'): string {
|
||||||
|
return `${prefix}_${uuidInstance}_${uuidIndex++}`;
|
||||||
|
}
|
|
@ -1,78 +0,0 @@
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/driver-browser-ui, Browser-based UI for Syndicate
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'randomid.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
let randomId;
|
|
||||||
|
|
||||||
function browserCryptoObject(crypto) {
|
|
||||||
if (typeof crypto.getRandomValues === 'undefined') return false;
|
|
||||||
randomId = function (byteCount, hexOutput) {
|
|
||||||
let buf = new Uint8Array(byteCount);
|
|
||||||
crypto.getRandomValues(buf);
|
|
||||||
if (hexOutput) {
|
|
||||||
let encoded = [];
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
|
||||||
encoded.push("0123456789abcdef"[(buf[i] >> 4) & 15]);
|
|
||||||
encoded.push("0123456789abcdef"[buf[i] & 15]);
|
|
||||||
}
|
|
||||||
return encoded.join('');
|
|
||||||
} else {
|
|
||||||
return btoa(String.fromCharCode.apply(null, buf)).replace(/=/g,'');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((typeof window !== 'undefined') &&
|
|
||||||
(typeof window.crypto !== 'undefined') &&
|
|
||||||
browserCryptoObject(window.crypto)) {
|
|
||||||
// We are in the main page, and window.crypto is available, and
|
|
||||||
// browserCryptoObject has installed a suitable randomId. Do
|
|
||||||
// nothing.
|
|
||||||
} else if ((typeof self !== 'undefined') &&
|
|
||||||
(typeof self.crypto !== 'undefined') &&
|
|
||||||
browserCryptoObject(self.crypto)) {
|
|
||||||
// We are in a web worker, and self.crypto is available, and
|
|
||||||
// browserCryptoObject has installed a suitable randomId. Do
|
|
||||||
// nothing.
|
|
||||||
} else {
|
|
||||||
// See if we're in node.js.
|
|
||||||
|
|
||||||
let crypto;
|
|
||||||
try {
|
|
||||||
crypto = require('crypto');
|
|
||||||
} catch (e) {}
|
|
||||||
if ((typeof crypto !== 'undefined') &&
|
|
||||||
(typeof crypto.randomBytes !== 'undefined')) {
|
|
||||||
randomId = function (byteCount, hexOutput) {
|
|
||||||
if (hexOutput) {
|
|
||||||
return crypto.randomBytes(byteCount).hexSlice().replace(/=/g,'');
|
|
||||||
} else {
|
|
||||||
return crypto.randomBytes(byteCount).base64Slice().replace(/=/g,'');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.warn('No suitable implementation for RandomID.randomId available.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.randomId = randomId;
|
|
|
@ -1,157 +0,0 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'relay.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
const $Special = require('./special.js');
|
|
||||||
|
|
||||||
const _Dataspace = require('./dataspace.js');
|
|
||||||
const Dataspace = _Dataspace.Dataspace;
|
|
||||||
|
|
||||||
const Assertions = require('./assertions.js');
|
|
||||||
const Observe = Assertions.Observe;
|
|
||||||
const Inbound = Assertions.Inbound;
|
|
||||||
const Outbound = Assertions.Outbound;
|
|
||||||
|
|
||||||
const Bag = require('./bag.js');
|
|
||||||
|
|
||||||
const $QuitDataspace = new $Special("quit-dataspace");
|
|
||||||
|
|
||||||
function NestedDataspace(outerFacet, bootProc) {
|
|
||||||
Dataspace.call(this, bootProc);
|
|
||||||
this.outerFacet = outerFacet;
|
|
||||||
}
|
|
||||||
NestedDataspace.prototype = new Dataspace(null);
|
|
||||||
|
|
||||||
NestedDataspace.prototype.sendMessage = function (m) {
|
|
||||||
Dataspace.prototype.sendMessage.call(this, m);
|
|
||||||
if (m === $QuitDataspace) {
|
|
||||||
this.outerFacet.stop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
NestedDataspace.prototype.endpointHook = function (facet, innerEp) {
|
|
||||||
const innerDs = this;
|
|
||||||
Dataspace.prototype.endpointHook.call(this, facet, innerEp);
|
|
||||||
if (Observe.isClassOf(innerEp.assertion) && Inbound.isClassOf(innerEp.assertion.get(0))) {
|
|
||||||
// We know that innerEp.assertion is an Observe(Inbound(...)).
|
|
||||||
// Also, if innerEp.handler exists, it will be consonant with innerEp.assertion.
|
|
||||||
// Beware of completely-constant patterns, which cause skeleton to be null!
|
|
||||||
this.hookEndpointLifecycle(innerEp, this.outerFacet.addEndpoint(() => {
|
|
||||||
const h = innerEp.handler;
|
|
||||||
return [Observe(innerEp.assertion.get(0).get(0)),
|
|
||||||
h && (h.skeleton === null
|
|
||||||
? {
|
|
||||||
skeleton: null,
|
|
||||||
constPaths: h.constPaths,
|
|
||||||
constVals: h.constVals.map((v) => v.get(0)),
|
|
||||||
capturePaths: h.capturePaths.map((p) => p.shift()),
|
|
||||||
callback: function (evt, captures) {
|
|
||||||
h.callback.call(this, evt, captures);
|
|
||||||
innerDs.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
skeleton: h.skeleton[1],
|
|
||||||
constPaths: h.constPaths.map((p) => p.shift()),
|
|
||||||
constVals: h.constVals,
|
|
||||||
capturePaths: h.capturePaths.map((p) => p.shift()),
|
|
||||||
callback: function (evt, captures) {
|
|
||||||
h.callback.call(this, evt, captures);
|
|
||||||
innerDs.start();
|
|
||||||
}
|
|
||||||
})];
|
|
||||||
}, false));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
NestedDataspace.prototype.adjustIndex = function (a, count) {
|
|
||||||
const net = Dataspace.prototype.adjustIndex.call(this, a, count);
|
|
||||||
if (Outbound.isClassOf(a)) {
|
|
||||||
switch (net) {
|
|
||||||
case Bag.ABSENT_TO_PRESENT:
|
|
||||||
this.outerFacet.actor.pushScript(() => {
|
|
||||||
this.outerFacet.actor.adhocAssert(a.get(0));
|
|
||||||
});
|
|
||||||
this.outerFacet.actor.dataspace.start();
|
|
||||||
break;
|
|
||||||
case Bag.PRESENT_TO_ABSENT:
|
|
||||||
this.outerFacet.actor.pushScript(() => {
|
|
||||||
this.outerFacet.actor.adhocRetract(a.get(0));
|
|
||||||
});
|
|
||||||
this.outerFacet.actor.dataspace.start();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return net;
|
|
||||||
};
|
|
||||||
|
|
||||||
NestedDataspace.prototype.hookEndpointLifecycle = function (innerEp, outerEp) {
|
|
||||||
const outerFacet = this.outerFacet;
|
|
||||||
|
|
||||||
const _refresh = innerEp.refresh;
|
|
||||||
innerEp.refresh = function (ds, ac, facet) {
|
|
||||||
_refresh.call(this, ds, ac, facet);
|
|
||||||
outerEp.refresh(outerFacet.actor.dataspace, outerFacet.actor, outerFacet);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _destroy = innerEp.destroy;
|
|
||||||
innerEp.destroy = function (ds, ac, facet, emitPatches) {
|
|
||||||
_destroy.call(this, ds, ac, facet, emitPatches);
|
|
||||||
outerEp.destroy(outerFacet.actor.dataspace, outerFacet.actor, outerFacet, true);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
NestedDataspace.prototype.start = function () {
|
|
||||||
this.outerFacet.actor.dataspace.start();
|
|
||||||
this.outerFacet.actor.pushScript(() => {
|
|
||||||
Dataspace.withCurrentFacet(this.outerFacet, () => {
|
|
||||||
if (this.outerFacet.isLive) {
|
|
||||||
Dataspace.deferTurn(() => {
|
|
||||||
const stillBusy = this.runScripts();
|
|
||||||
if (stillBusy) this.start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
NestedDataspace.prototype.ground = function () {
|
|
||||||
return this.outerFacet.actor.dataspace.ground();
|
|
||||||
};
|
|
||||||
|
|
||||||
function inNestedDataspace(bootProc) {
|
|
||||||
return () => {
|
|
||||||
const outerFacet = Dataspace.currentFacet();
|
|
||||||
outerFacet.addDataflow(function () {});
|
|
||||||
// ^ eww! Dummy endpoint to keep the root facet of the relay alive.
|
|
||||||
const innerDs = new NestedDataspace(outerFacet, function () {
|
|
||||||
Dataspace.currentFacet().addStartScript(() => bootProc.call(this));
|
|
||||||
});
|
|
||||||
innerDs.start();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.$QuitDataspace = $QuitDataspace;
|
|
||||||
module.exports.NestedDataspace = NestedDataspace;
|
|
||||||
module.exports.inNestedDataspace = inNestedDataspace;
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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 } from 'preserves';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
_instance: Record;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Discard: Discard = (function () {
|
||||||
|
let Discard: any = Record.makeConstructor('discard', []);
|
||||||
|
Discard._instance = Discard();
|
||||||
|
return Discard;
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const Capture = Record.makeConstructor('capture', ['specification']);
|
||||||
|
export const Observe = Record.makeConstructor('observe', ['specification']);
|
||||||
|
|
||||||
|
export const Inbound = Record.makeConstructor('inbound', ['assertion']);
|
||||||
|
export const Outbound = Record.makeConstructor('outbound', ['assertion']);
|
||||||
|
export const Instance = Record.makeConstructor('instance', ['uniqueId']);
|
|
@ -0,0 +1,95 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Bags and Deltas (which are Bags where item-counts can be negative).
|
||||||
|
|
||||||
|
import { Value, Set, Dictionary } from 'preserves';
|
||||||
|
|
||||||
|
export enum ChangeDescription {
|
||||||
|
PRESENT_TO_ABSENT = -1,
|
||||||
|
ABSENT_TO_ABSENT = 0,
|
||||||
|
ABSENT_TO_PRESENT = 1,
|
||||||
|
PRESENT_TO_PRESENT = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Bag {
|
||||||
|
_items: Dictionary<number>;
|
||||||
|
|
||||||
|
constructor(s?: Set) {
|
||||||
|
this._items = new Dictionary();
|
||||||
|
if (s) s.forEach((v) => this._items.set(v, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: Value): number {
|
||||||
|
return this._items.get(key, 0) as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key: Value, delta: number, clamp: boolean = false): ChangeDescription {
|
||||||
|
let oldCount = this.get(key);
|
||||||
|
let newCount = oldCount + delta;
|
||||||
|
if (clamp) {
|
||||||
|
newCount = Math.max(0, newCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newCount === 0) {
|
||||||
|
this._items.delete(key);
|
||||||
|
return (oldCount === 0)
|
||||||
|
? ChangeDescription.ABSENT_TO_ABSENT
|
||||||
|
: ChangeDescription.PRESENT_TO_ABSENT;
|
||||||
|
} else {
|
||||||
|
this._items.set(key, newCount);
|
||||||
|
return (oldCount === 0)
|
||||||
|
? ChangeDescription.ABSENT_TO_PRESENT
|
||||||
|
: ChangeDescription.PRESENT_TO_PRESENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._items = new Dictionary();
|
||||||
|
}
|
||||||
|
|
||||||
|
includes(key: Value): boolean {
|
||||||
|
return this._items.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this._items.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): IterableIterator<Value> {
|
||||||
|
return this._items.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): IterableIterator<[Value, number]> {
|
||||||
|
return this._items.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(f: (count: number, value: Value) => void) {
|
||||||
|
this._items.forEach(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot(): Dictionary<number> {
|
||||||
|
return this._items.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
clone(): Bag {
|
||||||
|
const b = new Bag();
|
||||||
|
b._items = this._items.clone();
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Property-based "dataflow"
|
||||||
|
|
||||||
|
import { FlexSet, FlexMap, Canonicalizer } from 'preserves';
|
||||||
|
import * as MapSet from './mapset.js';
|
||||||
|
|
||||||
|
export interface PropertyOptions<ObjectId> {
|
||||||
|
objectId: ObjectId;
|
||||||
|
noopGuard?: (oldValue: any, newValue: any) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Graph<SubjectId, ObjectId> {
|
||||||
|
readonly edgesForward: FlexMap<ObjectId, FlexSet<SubjectId>>;
|
||||||
|
readonly edgesReverse: FlexMap<SubjectId, FlexSet<ObjectId>>;
|
||||||
|
readonly subjectIdCanonicalizer: Canonicalizer<SubjectId>;
|
||||||
|
readonly objectIdCanonicalizer: Canonicalizer<ObjectId>;
|
||||||
|
damagedNodes: FlexSet<ObjectId>;
|
||||||
|
currentSubjectId: SubjectId | undefined;
|
||||||
|
|
||||||
|
constructor(subjectIdCanonicalizer: Canonicalizer<SubjectId>,
|
||||||
|
objectIdCanonicalizer: Canonicalizer<ObjectId>)
|
||||||
|
{
|
||||||
|
this.edgesForward = new FlexMap(objectIdCanonicalizer);
|
||||||
|
this.edgesReverse = new FlexMap(subjectIdCanonicalizer);
|
||||||
|
this.subjectIdCanonicalizer = subjectIdCanonicalizer;
|
||||||
|
this.objectIdCanonicalizer = objectIdCanonicalizer;
|
||||||
|
this.damagedNodes = new FlexSet(objectIdCanonicalizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
withSubject<T>(subjectId: SubjectId | undefined, f: () => T): T {
|
||||||
|
let oldSubjectId = this.currentSubjectId;
|
||||||
|
this.currentSubjectId = subjectId;
|
||||||
|
let result: T;
|
||||||
|
try {
|
||||||
|
result = f();
|
||||||
|
} catch (e) {
|
||||||
|
this.currentSubjectId = oldSubjectId;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
this.currentSubjectId = oldSubjectId;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordObservation(objectId: ObjectId) {
|
||||||
|
if (this.currentSubjectId !== void 0) {
|
||||||
|
MapSet.add(this.edgesForward, objectId, this.currentSubjectId, this.subjectIdCanonicalizer);
|
||||||
|
MapSet.add(this.edgesReverse, this.currentSubjectId, objectId, this.objectIdCanonicalizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDamage(objectId: ObjectId) {
|
||||||
|
this.damagedNodes.add(objectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
forgetSubject(subjectId: SubjectId) {
|
||||||
|
const subjectObjects = this.edgesReverse.get(subjectId) ?? [] as Array<ObjectId>;
|
||||||
|
this.edgesReverse.delete(subjectId);
|
||||||
|
subjectObjects.forEach((oid: ObjectId) => MapSet.del(this.edgesForward, oid, subjectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
repairDamage(repairNode: (subjectId: SubjectId) => void) {
|
||||||
|
let repairedThisRound = new FlexSet(this.objectIdCanonicalizer);
|
||||||
|
while (true) {
|
||||||
|
let workSet = this.damagedNodes;
|
||||||
|
this.damagedNodes = new FlexSet(this.objectIdCanonicalizer);
|
||||||
|
|
||||||
|
const alreadyDamaged = workSet.intersect(repairedThisRound);
|
||||||
|
if (alreadyDamaged.size > 0) {
|
||||||
|
console.warn('Cyclic dependencies involving', alreadyDamaged);
|
||||||
|
}
|
||||||
|
|
||||||
|
workSet = workSet.subtract(repairedThisRound);
|
||||||
|
repairedThisRound = repairedThisRound.union(workSet);
|
||||||
|
|
||||||
|
if (workSet.size === 0) break;
|
||||||
|
|
||||||
|
workSet.forEach(objectId => {
|
||||||
|
const subjects = this.edgesForward.get(objectId) ?? [] as Array<SubjectId>;
|
||||||
|
subjects.forEach((subjectId: SubjectId) => {
|
||||||
|
this.forgetSubject(subjectId);
|
||||||
|
this.withSubject(subjectId, () => repairNode(subjectId));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineObservableProperty(obj: object,
|
||||||
|
prop: string,
|
||||||
|
value: any,
|
||||||
|
options: PropertyOptions<ObjectId>)
|
||||||
|
{
|
||||||
|
const { objectId, noopGuard } = options;
|
||||||
|
Object.defineProperty(obj, prop, {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
get: () => {
|
||||||
|
this.recordObservation(objectId);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set: (newValue) => {
|
||||||
|
if (!noopGuard || !noopGuard(value, newValue)) {
|
||||||
|
this.recordDamage(objectId);
|
||||||
|
value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.recordDamage(objectId);
|
||||||
|
return objectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static newScope(o: object): object {
|
||||||
|
function O() {}
|
||||||
|
O.prototype = o;
|
||||||
|
return new O();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,753 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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 { Value, fromJS, is, Set } from 'preserves';
|
||||||
|
|
||||||
|
import * as Skeleton from './skeleton.js';
|
||||||
|
import { Bag, ChangeDescription } from './bag.js';
|
||||||
|
import { Observe } from './assertions.js';
|
||||||
|
import * as Dataflow from './dataflow.js';
|
||||||
|
import { IdentitySet, IdentityMap } from './idcoll.js';
|
||||||
|
import { Ground } from './ground.js';
|
||||||
|
|
||||||
|
export enum Priority {
|
||||||
|
QUERY_HIGH = 0,
|
||||||
|
QUERY,
|
||||||
|
QUERY_HANDLER,
|
||||||
|
NORMAL,
|
||||||
|
GC,
|
||||||
|
IDLE,
|
||||||
|
_count
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActorId = number;
|
||||||
|
export type FacetId = ActorId;
|
||||||
|
export type EndpointId = ActorId;
|
||||||
|
|
||||||
|
export type Script = () => void;
|
||||||
|
|
||||||
|
export type MaybeValue = Value | undefined;
|
||||||
|
export type EndpointSpec = { assertion: MaybeValue, analysis: Skeleton.Analysis | null };
|
||||||
|
|
||||||
|
export type ObserverCallback = (bindings: Array<Value>) => void;
|
||||||
|
|
||||||
|
export type ObserverCallbacks = {
|
||||||
|
add?: ObserverCallback;
|
||||||
|
del?: ObserverCallback;
|
||||||
|
msg?: ObserverCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataflowObservableObjectId = Symbol.for('DataflowObservableObjectId');
|
||||||
|
export interface DataflowObservableObject {
|
||||||
|
[DataflowObservableObjectId](): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DataflowObservable = [DataflowObservableObject, string];
|
||||||
|
export function _canonicalizeDataflowObservable(i: DataflowObservable): string {
|
||||||
|
return i[0][DataflowObservableObjectId]() + ',' + i[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DataflowDependent = Endpoint;
|
||||||
|
export function _canonicalizeDataflowDependent(i: DataflowDependent): string {
|
||||||
|
return '' + i.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Dataspace {
|
||||||
|
nextId: ActorId = 0;
|
||||||
|
index = new Skeleton.Index();
|
||||||
|
dataflow = new Dataflow.Graph<DataflowDependent, DataflowObservable>(
|
||||||
|
_canonicalizeDataflowDependent,
|
||||||
|
_canonicalizeDataflowObservable);
|
||||||
|
runnable: Array<Actor> = [];
|
||||||
|
pendingTurns: Array<Turn>;
|
||||||
|
actors: IdentityMap<number, Actor> = new IdentityMap();
|
||||||
|
|
||||||
|
constructor(bootProc: Script) {
|
||||||
|
this.pendingTurns = [new Turn(null, [new Spawn(null, bootProc, new Set())])];
|
||||||
|
}
|
||||||
|
|
||||||
|
static _currentFacet: Facet | null = null;
|
||||||
|
static _inScript = true;
|
||||||
|
|
||||||
|
static get currentFacet(): Facet | null {
|
||||||
|
return Dataspace._currentFacet;
|
||||||
|
}
|
||||||
|
|
||||||
|
static withNonScriptContext<T>(thunk: () => T) {
|
||||||
|
let savedInScript = Dataspace._inScript;
|
||||||
|
Dataspace._inScript = false;
|
||||||
|
try {
|
||||||
|
return thunk();
|
||||||
|
} finally {
|
||||||
|
Dataspace._inScript = savedInScript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static withCurrentFacet<T>(facet: Facet, thunk: () => T) {
|
||||||
|
let savedFacet = Dataspace._currentFacet;
|
||||||
|
Dataspace._currentFacet = facet;
|
||||||
|
try {
|
||||||
|
// console.group('Facet', facet && facet.toString());
|
||||||
|
let result = thunk();
|
||||||
|
Dataspace._currentFacet = savedFacet;
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
let a = facet.actor;
|
||||||
|
a.abandonQueuedWork();
|
||||||
|
a._terminate(false);
|
||||||
|
Dataspace._currentFacet = savedFacet;
|
||||||
|
console.error('Actor ' + a.toString() + ' exited with exception:', e);
|
||||||
|
} finally {
|
||||||
|
// console.groupEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static wrap<T extends Array<any>, R>(f: (... args: T) => R): (... args: T) => R {
|
||||||
|
const savedFacet = Dataspace._currentFacet;
|
||||||
|
return (... actuals) =>
|
||||||
|
Dataspace.withCurrentFacet(savedFacet, () => f.apply(savedFacet.fields, actuals));
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract start(): this;
|
||||||
|
abstract ground(): Ground;
|
||||||
|
|
||||||
|
static wrapExternal<T extends Array<any>>(f: (... args: T) => void): (... args: T) => void {
|
||||||
|
const savedFacet = Dataspace._currentFacet;
|
||||||
|
const ac = savedFacet.actor;
|
||||||
|
return (... actuals) => {
|
||||||
|
if (savedFacet.isLive) {
|
||||||
|
ac.dataspace.start();
|
||||||
|
ac.pushScript(() =>
|
||||||
|
Dataspace.withCurrentFacet(savedFacet, () =>
|
||||||
|
f.apply(savedFacet.fields, actuals)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static backgroundTask(): () => void {
|
||||||
|
return Dataspace._currentFacet.actor.dataspace.ground().backgroundTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
static referenceField(obj: DataflowObservableObject, prop: string) {
|
||||||
|
if (!(prop in obj)) {
|
||||||
|
Dataspace._currentFacet.actor.dataspace.dataflow.recordObservation([obj, prop]);
|
||||||
|
}
|
||||||
|
return obj[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
static declareField(obj: DataflowObservableObject, prop: string, init: any) {
|
||||||
|
if (prop in obj) {
|
||||||
|
obj[prop] = init;
|
||||||
|
} else {
|
||||||
|
Dataspace._currentFacet.actor.dataspace.dataflow.defineObservableProperty(
|
||||||
|
obj,
|
||||||
|
prop,
|
||||||
|
init,
|
||||||
|
{
|
||||||
|
objectId: [obj, prop],
|
||||||
|
noopGuard: is
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteField(obj: DataflowObservableObject, prop: string) {
|
||||||
|
Dataspace._currentFacet.actor.dataspace.dataflow.recordDamage([obj, prop]);
|
||||||
|
delete obj[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
runScripts() { // TODO: rename?
|
||||||
|
this.runPendingScripts();
|
||||||
|
this.performPendingActions();
|
||||||
|
return this.runnable.length > 0 || this.pendingTurns.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
runPendingScripts() {
|
||||||
|
let runnable = this.runnable;
|
||||||
|
this.runnable = [];
|
||||||
|
runnable.forEach((ac) => { ac.runPendingScripts(); /* TODO: rename? */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
performPendingActions() {
|
||||||
|
let turns = this.pendingTurns;
|
||||||
|
this.pendingTurns = [];
|
||||||
|
turns.forEach((turn) => {
|
||||||
|
turn.actions.forEach((action) => {
|
||||||
|
// console.log('[DATASPACE]', group.actor && group.actor.toString(), action);
|
||||||
|
action.perform(this, turn.actor);
|
||||||
|
this.runPendingScripts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
commitActions(ac: Actor, pending: Array<Action>) {
|
||||||
|
this.pendingTurns.push(new Turn(ac, pending));
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAssertions() {
|
||||||
|
Dataspace.withNonScriptContext(() => {
|
||||||
|
this.dataflow.repairDamage((ep) => {
|
||||||
|
let facet = ep.facet;
|
||||||
|
if (facet.isLive) { // TODO: necessary test, or tautological?
|
||||||
|
Dataspace.withCurrentFacet(facet, () => ep.refresh());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addActor(name: any, bootProc: Script, initialAssertions: Set, parentActor: Actor | undefined) {
|
||||||
|
let ac = new Actor(this, name, initialAssertions, parentActor?.id);
|
||||||
|
// debug('Spawn', ac && ac.toString());
|
||||||
|
this.applyPatch(ac, ac.adhocAssertions);
|
||||||
|
ac.addFacet(null, () => {
|
||||||
|
// Root facet is a dummy "system" facet that exists to hold
|
||||||
|
// one-or-more "user" "root" facets.
|
||||||
|
ac.addFacet(Dataspace._currentFacet, bootProc);
|
||||||
|
// ^ The "true root", user-visible facet.
|
||||||
|
initialAssertions.forEach((a) => { ac.adhocRetract(a); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPatch(ac: Actor, delta: Bag) {
|
||||||
|
// if (!delta.isEmpty()) debug('applyPatch BEGIN', ac && ac.toString());
|
||||||
|
let removals = [];
|
||||||
|
delta.forEach((count, a) => {
|
||||||
|
if (count > 0) {
|
||||||
|
// debug('applyPatch +', a && a.toString());
|
||||||
|
this.adjustIndex(a, count);
|
||||||
|
} else {
|
||||||
|
removals.push([count, a]);
|
||||||
|
}
|
||||||
|
if (ac) ac.cleanupChanges.change(a, -count);
|
||||||
|
});
|
||||||
|
removals.forEach(([count, a]) => {
|
||||||
|
// debug('applyPatch -', a && a.toString());
|
||||||
|
this.adjustIndex(a, count);
|
||||||
|
});
|
||||||
|
// if (!delta.isEmpty()) debug('applyPatch END');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(m: Value, _sendingActor: Actor) {
|
||||||
|
// debug('sendMessage', sendingActor && sendingActor.toString(), m.toString());
|
||||||
|
this.index.sendMessage(m);
|
||||||
|
// this.index.sendMessage(m, (leaf, _m) => {
|
||||||
|
// sendingActor.touchedTopics = sendingActor.touchedTopics.add(leaf);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustIndex(a: Value, count: number) {
|
||||||
|
return this.index.adjustAssertion(a, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(handler: Skeleton.Analysis) {
|
||||||
|
this.index.addHandler(handler, handler.callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(handler: Skeleton.Analysis) {
|
||||||
|
this.index.removeHandler(handler, handler.callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointHook(_facet: Facet, _endpoint: Endpoint) {
|
||||||
|
// Subclasses may override
|
||||||
|
}
|
||||||
|
|
||||||
|
static send(body: any) {
|
||||||
|
if (!Dataspace._inScript) {
|
||||||
|
throw new Error("Cannot `send` during facet setup; are you missing an `on start { ... }`?");
|
||||||
|
}
|
||||||
|
Dataspace._currentFacet.enqueueScriptAction(new Message(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
static spawn(name: any, bootProc: Script, initialAssertions?: Set) {
|
||||||
|
if (!Dataspace._inScript) {
|
||||||
|
throw new Error("Cannot `spawn` during facet setup; are you missing an `on start { ... }`?");
|
||||||
|
}
|
||||||
|
Dataspace._currentFacet.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions));
|
||||||
|
}
|
||||||
|
|
||||||
|
static deferTurn(continuation: Script) {
|
||||||
|
if (!Dataspace._inScript) {
|
||||||
|
throw new Error("Cannot defer turn during facet setup; are you missing an `on start { ... }`?");
|
||||||
|
}
|
||||||
|
Dataspace._currentFacet.enqueueScriptAction(new DeferredTurn(Dataspace.wrap(continuation)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Actor {
|
||||||
|
readonly id: ActorId;
|
||||||
|
readonly dataspace: Dataspace;
|
||||||
|
readonly name: any;
|
||||||
|
rootFacet: Facet | null = null;
|
||||||
|
isRunnable: boolean = false;
|
||||||
|
readonly pendingScripts: Array<Array<Script>>;
|
||||||
|
pendingActions: Array<Action>;
|
||||||
|
adhocAssertions: Bag;
|
||||||
|
cleanupChanges = new Bag(); // negative counts allowed!
|
||||||
|
parentId: ActorId | undefined;
|
||||||
|
|
||||||
|
constructor(dataspace: Dataspace,
|
||||||
|
name: any,
|
||||||
|
initialAssertions: Set,
|
||||||
|
parentActorId: ActorId | undefined)
|
||||||
|
{
|
||||||
|
this.id = dataspace.nextId++;
|
||||||
|
this.dataspace = dataspace;
|
||||||
|
this.name = name;
|
||||||
|
this.isRunnable = false;
|
||||||
|
this.pendingScripts = [];
|
||||||
|
for (let i = 0; i < Priority._count; i++) { this.pendingScripts.push([]); }
|
||||||
|
this.pendingActions = [];
|
||||||
|
this.adhocAssertions = new Bag(initialAssertions); // no negative counts allowed
|
||||||
|
this.parentId = parentActorId;
|
||||||
|
dataspace.actors.set(this.id, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
runPendingScripts() {
|
||||||
|
while (true) {
|
||||||
|
let script = this.popNextScript();
|
||||||
|
if (!script) break;
|
||||||
|
script();
|
||||||
|
this.dataspace.refreshAssertions();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunnable = false;
|
||||||
|
let pending = this.pendingActions;
|
||||||
|
if (pending.length > 0) {
|
||||||
|
this.pendingActions = [];
|
||||||
|
this.dataspace.commitActions(this, pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popNextScript(): Script | null {
|
||||||
|
let scripts = this.pendingScripts;
|
||||||
|
for (let i = 0; i < Priority._count; i++) {
|
||||||
|
let q = scripts[i];
|
||||||
|
if (q.length > 0) return q.shift();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
abandonQueuedWork() {
|
||||||
|
this.pendingActions = [];
|
||||||
|
for (let i = 0; i < Priority._count; i++) { this.pendingScripts[i] = []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleScript(unwrappedThunk: Script, priority?: Priority) {
|
||||||
|
this.pushScript(Dataspace.wrap(unwrappedThunk), priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
pushScript(wrappedThunk: Script, priority: Priority = Priority.NORMAL) {
|
||||||
|
// The wrappedThunk must already have code for ensuring
|
||||||
|
// _currentFacet is correct inside it. Compare with scheduleScript.
|
||||||
|
if (!this.isRunnable) {
|
||||||
|
this.isRunnable = true;
|
||||||
|
this.dataspace.runnable.push(this);
|
||||||
|
}
|
||||||
|
this.pendingScripts[priority].push(wrappedThunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
addFacet(parentFacet: Facet, bootProc: Script, checkInScript: boolean = false) {
|
||||||
|
if (checkInScript && !Dataspace._inScript) {
|
||||||
|
throw new Error("Cannot add facet outside script; are you missing a `react { ... }`?");
|
||||||
|
}
|
||||||
|
let f = new Facet(this, parentFacet);
|
||||||
|
Dataspace.withCurrentFacet(f, () => {
|
||||||
|
Dataspace.withNonScriptContext(() => {
|
||||||
|
bootProc.call(f.fields);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.pushScript(() => {
|
||||||
|
if ((parentFacet && !parentFacet.isLive) || f.isInert()) {
|
||||||
|
f._terminate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_terminate(emitPatches: boolean) {
|
||||||
|
// Abruptly terminates an entire actor, without running stop-scripts etc.
|
||||||
|
if (emitPatches) {
|
||||||
|
this.pushScript(() => {
|
||||||
|
this.adhocAssertions.snapshot().forEach((_count, a) => { this.retract(a); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.rootFacet) {
|
||||||
|
this.rootFacet._abort(emitPatches);
|
||||||
|
}
|
||||||
|
this.pushScript(() => { this.enqueueScriptAction(new Quit()); });
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueScriptAction(action: Action) {
|
||||||
|
this.pendingActions.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPatch(): Patch {
|
||||||
|
if (this.pendingActions.length > 0) {
|
||||||
|
let p = this.pendingActions[this.pendingActions.length - 1];
|
||||||
|
if (p instanceof Patch) return p;
|
||||||
|
}
|
||||||
|
let p = new Patch(new Bag());
|
||||||
|
this.enqueueScriptAction(p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(a: Value) { this.pendingPatch().adjust(a, +1); }
|
||||||
|
retract(a: Value) { this.pendingPatch().adjust(a, -1); }
|
||||||
|
|
||||||
|
adhocRetract(a: Value) {
|
||||||
|
a = fromJS(a);
|
||||||
|
if (this.adhocAssertions.change(a, -1, true) === ChangeDescription.PRESENT_TO_ABSENT) {
|
||||||
|
this.retract(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adhocAssert(a: Value) {
|
||||||
|
a = fromJS(a);
|
||||||
|
if (this.adhocAssertions.change(a, +1) === ChangeDescription.ABSENT_TO_PRESENT) {
|
||||||
|
this.assert(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
let s = 'Actor(' + this.id;
|
||||||
|
if (this.name !== void 0 && this.name !== null) s = s + ',' + this.name.toString();
|
||||||
|
return s + ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Action {
|
||||||
|
abstract perform(ds: Dataspace, ac: Actor): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Patch extends Action {
|
||||||
|
readonly changes: Bag;
|
||||||
|
|
||||||
|
constructor(changes: Bag) {
|
||||||
|
super();
|
||||||
|
this.changes = changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
perform(ds: Dataspace, ac: Actor): void {
|
||||||
|
ds.applyPatch(ac, this.changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
adjust(a: Value, count: number) {
|
||||||
|
this.changes.change(fromJS(a), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Message extends Action {
|
||||||
|
readonly body: Value;
|
||||||
|
|
||||||
|
constructor(body: any) {
|
||||||
|
super();
|
||||||
|
this.body = fromJS(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
perform(ds: Dataspace, ac: Actor): void {
|
||||||
|
ds.sendMessage(this.body, ac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Spawn extends Action {
|
||||||
|
readonly name: any;
|
||||||
|
readonly bootProc: Script;
|
||||||
|
readonly initialAssertions: Set;
|
||||||
|
|
||||||
|
constructor(name: any, bootProc: Script, initialAssertions: Set = new Set()) {
|
||||||
|
super();
|
||||||
|
this.name = name;
|
||||||
|
this.bootProc = bootProc;
|
||||||
|
this.initialAssertions = initialAssertions;
|
||||||
|
}
|
||||||
|
|
||||||
|
perform(ds: Dataspace, ac: Actor): void {
|
||||||
|
ds.addActor(this.name, this.bootProc, this.initialAssertions, ac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Quit extends Action { // TODO: rename? Perhaps to Cleanup?
|
||||||
|
// Pseudo-action - not for userland use.
|
||||||
|
|
||||||
|
perform(ds: Dataspace, ac: Actor): void {
|
||||||
|
ds.applyPatch(ac, ac.cleanupChanges);
|
||||||
|
ds.actors.delete(ac.id);
|
||||||
|
// debug('Quit', ac && ac.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeferredTurn extends Action {
|
||||||
|
readonly continuation: Script;
|
||||||
|
|
||||||
|
constructor(continuation: Script) {
|
||||||
|
super();
|
||||||
|
this.continuation = continuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
perform(_ds: Dataspace, ac: Actor): void {
|
||||||
|
// debug('DeferredTurn', ac && ac.toString());
|
||||||
|
ac.pushScript(this.continuation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Turn {
|
||||||
|
readonly actor: Actor | null;
|
||||||
|
readonly actions: Array<Action>;
|
||||||
|
|
||||||
|
constructor(actor: Actor | null, actions: Array<Action> = []) {
|
||||||
|
this.actor = actor;
|
||||||
|
this.actions = actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueScriptAction(a: Action) {
|
||||||
|
this.actions.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Facet {
|
||||||
|
readonly id: FacetId;
|
||||||
|
isLive = true;
|
||||||
|
readonly actor: Actor;
|
||||||
|
readonly parent: Facet | null;
|
||||||
|
readonly endpoints = new IdentityMap<EndpointId, Endpoint>();
|
||||||
|
readonly stopScripts: Array<Script> = [];
|
||||||
|
readonly children = new IdentitySet<Facet>();
|
||||||
|
readonly fields: any;
|
||||||
|
|
||||||
|
constructor(actor: Actor, parent: Facet | null) {
|
||||||
|
this.id = actor.dataspace.nextId++;
|
||||||
|
this.actor = actor;
|
||||||
|
this.parent = parent;
|
||||||
|
if (parent) {
|
||||||
|
parent.children.add(this);
|
||||||
|
this.fields = Dataflow.Graph.newScope(parent.fields);
|
||||||
|
} else {
|
||||||
|
if (actor.rootFacet) {
|
||||||
|
throw new Error("INVARIANT VIOLATED: Attempt to add second root facet");
|
||||||
|
}
|
||||||
|
actor.rootFacet = this;
|
||||||
|
this.fields = Dataflow.Graph.newScope({});
|
||||||
|
}
|
||||||
|
this.fields[DataflowObservableObjectId] = () => this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
_abort(emitPatches: boolean) {
|
||||||
|
this.isLive = false;
|
||||||
|
this.children.forEach(child => child._abort(emitPatches));
|
||||||
|
this.retractAssertionsAndSubscriptions(emitPatches);
|
||||||
|
}
|
||||||
|
|
||||||
|
retractAssertionsAndSubscriptions(emitPatches: boolean) {
|
||||||
|
this.actor.pushScript(() => {
|
||||||
|
this.endpoints.forEach((ep) => ep.destroy(emitPatches));
|
||||||
|
this.endpoints.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isInert(): boolean {
|
||||||
|
return this.endpoints.size === 0 && this.children.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_terminate() {
|
||||||
|
if (!this.isLive) return;
|
||||||
|
|
||||||
|
let ac = this.actor;
|
||||||
|
let parent = this.parent;
|
||||||
|
if (parent) {
|
||||||
|
parent.children.delete(this);
|
||||||
|
} else {
|
||||||
|
ac.rootFacet = null;
|
||||||
|
}
|
||||||
|
this.isLive = false;
|
||||||
|
|
||||||
|
this.children.forEach((child) => { child._terminate(); });
|
||||||
|
|
||||||
|
// Run stop-scripts after terminating children. This means
|
||||||
|
// that children's stop-scripts run before ours.
|
||||||
|
ac.pushScript(() => {
|
||||||
|
Dataspace.withCurrentFacet(this, () => {
|
||||||
|
this.stopScripts.forEach((s) => { s.call(this.fields); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.retractAssertionsAndSubscriptions(true);
|
||||||
|
ac.pushScript(() => {
|
||||||
|
if (parent) {
|
||||||
|
if (parent.isInert()) {
|
||||||
|
parent._terminate();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ac._terminate(true);
|
||||||
|
}
|
||||||
|
}, Priority.GC);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(continuation?: Script) {
|
||||||
|
Dataspace.withCurrentFacet(this.parent, () => {
|
||||||
|
this.actor.scheduleScript(() => {
|
||||||
|
this._terminate();
|
||||||
|
if (continuation) {
|
||||||
|
this.actor.scheduleScript(() => continuation.call(this.fields));
|
||||||
|
// ^ TODO: is this the correct scope to use??
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addStartScript(s: Script) {
|
||||||
|
if (Dataspace._inScript) {
|
||||||
|
throw new Error("Cannot `on start` outside facet setup");
|
||||||
|
}
|
||||||
|
this.actor.scheduleScript(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
addStopScript(s: Script) {
|
||||||
|
if (Dataspace._inScript) {
|
||||||
|
throw new Error("Cannot `on stop` outside facet setup");
|
||||||
|
}
|
||||||
|
this.stopScripts.push(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEndpoint(updateFun: () => EndpointSpec, isDynamic: boolean = true): Endpoint {
|
||||||
|
const ep = new Endpoint(this, isDynamic, updateFun);
|
||||||
|
this.actor.dataspace.endpointHook(this, ep);
|
||||||
|
return ep;
|
||||||
|
}
|
||||||
|
|
||||||
|
_addRawObserverEndpoint(specThunk: () => MaybeValue, callbacks: ObserverCallbacks): Endpoint
|
||||||
|
{
|
||||||
|
return this.addEndpoint(() => {
|
||||||
|
const spec = specThunk();
|
||||||
|
if (spec === void 0) {
|
||||||
|
return { assertion: void 0, analysis: null };
|
||||||
|
} else {
|
||||||
|
const analysis = Skeleton.analyzeAssertion(spec);
|
||||||
|
analysis.callback = Dataspace.wrap((evt, vs) => {
|
||||||
|
switch (evt) {
|
||||||
|
case Skeleton.EventType.ADDED: callbacks.add?.(vs); break;
|
||||||
|
case Skeleton.EventType.REMOVED: callbacks.del?.(vs); break;
|
||||||
|
case Skeleton.EventType.MESSAGE: callbacks.msg?.(vs); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { assertion: Observe(spec), analysis };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addObserverEndpoint(specThunk: () => MaybeValue, callbacks: ObserverCallbacks): Endpoint {
|
||||||
|
const scriptify = (f?: ObserverCallback) =>
|
||||||
|
f && ((vs: Array<Value>) => this.actor.scheduleScript(() => f(vs)));
|
||||||
|
return this._addRawObserverEndpoint(specThunk, {
|
||||||
|
add: scriptify(callbacks.add),
|
||||||
|
del: scriptify(callbacks.del),
|
||||||
|
msg: scriptify(callbacks.msg),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addDataflow(subjectFun: Script, priority?: Priority): Endpoint {
|
||||||
|
return this.addEndpoint(() => {
|
||||||
|
let subjectId = this.actor.dataspace.dataflow.currentSubjectId;
|
||||||
|
this.actor.scheduleScript(() => {
|
||||||
|
if (this.isLive) {
|
||||||
|
this.actor.dataspace.dataflow.withSubject(subjectId, () =>
|
||||||
|
subjectFun.call(this.fields));
|
||||||
|
}
|
||||||
|
}, priority);
|
||||||
|
return { assertion: void 0, analysis: null };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueScriptAction(action: Action) {
|
||||||
|
this.actor.enqueueScriptAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
let s = 'Facet(' + this.actor.id;
|
||||||
|
if (this.actor.name !== void 0 && this.actor.name !== null) {
|
||||||
|
s = s + ',' + this.actor.name.toString();
|
||||||
|
}
|
||||||
|
s = s + ',' + this.id;
|
||||||
|
let f = this.parent;
|
||||||
|
while (f != null) {
|
||||||
|
s = s + ':' + f.id;
|
||||||
|
f = f.parent;
|
||||||
|
}
|
||||||
|
return s + ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Endpoint {
|
||||||
|
readonly id: EndpointId;
|
||||||
|
readonly facet: Facet;
|
||||||
|
readonly updateFun: () => EndpointSpec;
|
||||||
|
spec: EndpointSpec;
|
||||||
|
|
||||||
|
constructor(facet: Facet, isDynamic: boolean, updateFun: () => EndpointSpec) {
|
||||||
|
if (Dataspace._inScript) {
|
||||||
|
throw new Error("Cannot add endpoint in script; are you missing a `react { ... }`?");
|
||||||
|
}
|
||||||
|
let ac = facet.actor;
|
||||||
|
let ds = ac.dataspace;
|
||||||
|
this.id = ds.nextId++;
|
||||||
|
this.facet = facet;
|
||||||
|
this.updateFun = updateFun;
|
||||||
|
let initialSpec = ds.dataflow.withSubject(isDynamic ? this : undefined,
|
||||||
|
() => updateFun.call(facet.fields));
|
||||||
|
this._install(initialSpec);
|
||||||
|
facet.endpoints.set(this.id, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_install(spec: EndpointSpec) {
|
||||||
|
this.spec = spec;
|
||||||
|
const ac = this.facet.actor;
|
||||||
|
if (this.spec.assertion !== void 0) {
|
||||||
|
ac.assert(this.spec.assertion);
|
||||||
|
}
|
||||||
|
if (this.spec.analysis) ac.dataspace.subscribe(this.spec.analysis);
|
||||||
|
}
|
||||||
|
|
||||||
|
_uninstall(emitPatches: boolean) {
|
||||||
|
if (emitPatches) {
|
||||||
|
if (this.spec.assertion !== void 0) {
|
||||||
|
this.facet.actor.retract(this.spec.assertion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.spec.analysis) this.facet.actor.dataspace.unsubscribe(this.spec.analysis);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
let newSpec = this.updateFun.call(this.facet.fields);
|
||||||
|
if (newSpec.assertion !== void 0) newSpec.assertion = fromJS(newSpec.assertion);
|
||||||
|
if (is(newSpec.assertion, this.spec.assertion)) {
|
||||||
|
this._uninstall(true);
|
||||||
|
this._install(newSpec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(emitPatches: boolean) {
|
||||||
|
const facet = this.facet;
|
||||||
|
facet.actor.dataspace.dataflow.forgetSubject(this);
|
||||||
|
// ^ TODO: this won't work because of object identity problems! Why
|
||||||
|
// does the Racket implementation do this, when the old JS
|
||||||
|
// implementation doesn't?
|
||||||
|
facet.endpoints.delete(this.id);
|
||||||
|
this._uninstall(emitPatches);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return 'Endpoint(' + this.id + ')';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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 { Dataspace, Script } from './dataspace.js';
|
||||||
|
|
||||||
|
export type StopHandler<D extends Dataspace> = (ds: D) => void;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
_ground: Ground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _resolved = Promise.resolve();
|
||||||
|
|
||||||
|
export class Ground extends Dataspace {
|
||||||
|
stepScheduled = false;
|
||||||
|
stepping = false;
|
||||||
|
startingFuel: number = 1000;
|
||||||
|
stopHandlers: Array<StopHandler<this>> = [];
|
||||||
|
backgroundTaskCount = 0;
|
||||||
|
|
||||||
|
constructor(bootProc: Script) {
|
||||||
|
super(function () { Dataspace.currentFacet.addStartScript(bootProc); });
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window._ground = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async laterCall(thunk: () => void): Promise<void> {
|
||||||
|
await _resolved;
|
||||||
|
if ('stackTraceLimit' in Error) {
|
||||||
|
(Error as any).stackTraceLimit = 100;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
thunk();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("SYNDICATE/JS INTERNAL ERROR", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundTask(): () => void {
|
||||||
|
let active = true;
|
||||||
|
this.backgroundTaskCount++;
|
||||||
|
return () => {
|
||||||
|
if (active) {
|
||||||
|
this.backgroundTaskCount--;
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): this {
|
||||||
|
if (!this.stepScheduled) {
|
||||||
|
Ground.laterCall(() => {
|
||||||
|
this.stepScheduled = false;
|
||||||
|
this._step();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this; // allows chaining start() immediately after construction
|
||||||
|
}
|
||||||
|
|
||||||
|
ground(): Ground {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_step() {
|
||||||
|
this.stepping = true;
|
||||||
|
try {
|
||||||
|
let stillBusy = false;
|
||||||
|
for (var fuel = this.startingFuel; fuel > 0; fuel--) {
|
||||||
|
stillBusy = this.runScripts();
|
||||||
|
if (!stillBusy) break;
|
||||||
|
}
|
||||||
|
if (stillBusy) {
|
||||||
|
this.start();
|
||||||
|
} else {
|
||||||
|
if (!this.backgroundTaskCount) {
|
||||||
|
this.stopHandlers.forEach((h) => h(this));
|
||||||
|
this.stopHandlers = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.stepping = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addStopHandler(h: StopHandler<this>): this {
|
||||||
|
this.stopHandlers.push(h);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let g = new Ground(() => {
|
||||||
|
// Worker.spawnWorkerRelay();
|
||||||
|
// });
|
||||||
|
// if (typeof document !== 'undefined') {
|
||||||
|
// document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
// g.start();
|
||||||
|
// if (k) k(g);
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// g.start();
|
||||||
|
// if (k) k(g);
|
||||||
|
// }
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Convenient alias for the JS-native Set and Map types.
|
||||||
|
|
||||||
|
export type IdentitySet<T> = Set<T>;
|
||||||
|
export const IdentitySet = Set;
|
||||||
|
|
||||||
|
export type IdentityMap<K,V> = Map<K,V>;
|
||||||
|
export const IdentityMap = Map;
|
|
@ -1,7 +1,6 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
||||||
// Copyright (C) 2016-2018 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
@ -19,31 +18,20 @@
|
||||||
|
|
||||||
// Utilities for Maps of Sets
|
// Utilities for Maps of Sets
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
import { FlexSet, FlexMap, Canonicalizer } from 'preserves';
|
||||||
require('../package.json').version,
|
|
||||||
'mapset.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
var Immutable = require('immutable');
|
export function add<K,V>(m: FlexMap<K, FlexSet<V>>, k: K, v: V, c: Canonicalizer<V>) {
|
||||||
|
let s = m.get(k);
|
||||||
function add(ms, key, val) {
|
if (!s) {
|
||||||
return ms.set(key, (ms.get(key) || Immutable.Set()).add(val));
|
s = new FlexSet(c);
|
||||||
}
|
m.set(k, s);
|
||||||
|
|
||||||
function remove(ms, key, val) {
|
|
||||||
var oldSet = ms.get(key);
|
|
||||||
if (oldSet) {
|
|
||||||
var newSet = oldSet.remove(val);
|
|
||||||
if (newSet.isEmpty()) {
|
|
||||||
ms = ms.remove(key);
|
|
||||||
} else {
|
|
||||||
ms = ms.set(key, newSet);
|
|
||||||
}
|
}
|
||||||
}
|
s.add(v);
|
||||||
return ms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
export function del<K,V>(m: FlexMap<K, FlexSet<V>>, k: K, v: V) {
|
||||||
|
const s = m.get(k);
|
||||||
module.exports.add = add;
|
if (!s) return;
|
||||||
module.exports.remove = remove;
|
s.delete(v);
|
||||||
|
if (s.size === 0) m.delete(k);
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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 } from 'preserves';
|
||||||
|
import * as node_crypto from 'crypto';
|
||||||
|
|
||||||
|
export function _btoa(s: string): string {
|
||||||
|
try {
|
||||||
|
return btoa(s);
|
||||||
|
} catch (e) {
|
||||||
|
return Buffer.from(s).toString('base64');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomId(byteCount: number, hexOutput: boolean = false): string {
|
||||||
|
let buf: Uint8Array;
|
||||||
|
if (node_crypto.randomBytes !== void 0) {
|
||||||
|
buf = node_crypto.randomBytes(byteCount);
|
||||||
|
} else {
|
||||||
|
buf = new Uint8Array(byteCount);
|
||||||
|
crypto.getRandomValues(buf);
|
||||||
|
}
|
||||||
|
if (hexOutput) {
|
||||||
|
return Bytes.from(buf).toHex();
|
||||||
|
} else {
|
||||||
|
return _btoa(String.fromCharCode.apply(null, buf)).replace(/=/g,'');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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 { Value } from 'preserves';
|
||||||
|
|
||||||
|
import { $Special } from './special.js';
|
||||||
|
import { Dataspace, Facet, Actor, Endpoint, Script } from './dataspace.js';
|
||||||
|
import { Observe, Inbound, Outbound } from './assertions.js';
|
||||||
|
import { ChangeDescription } from './bag.js';
|
||||||
|
import { EventType, Analysis } from './skeleton.js';
|
||||||
|
import { Ground } from './ground.js';
|
||||||
|
|
||||||
|
export const $QuitDataspace = new $Special("quit-dataspace");
|
||||||
|
|
||||||
|
export class NestedDataspace extends Dataspace {
|
||||||
|
readonly outerFacet: Facet;
|
||||||
|
|
||||||
|
constructor(outerFacet: Facet, bootProc: Script) {
|
||||||
|
super(bootProc);
|
||||||
|
this.outerFacet = outerFacet;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(m: any, _sendingActor: Actor) {
|
||||||
|
super.sendMessage(m, _sendingActor);
|
||||||
|
if (m === $QuitDataspace) {
|
||||||
|
this.outerFacet.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointHook(facet: Facet, innerEp: Endpoint) {
|
||||||
|
const innerDs = this;
|
||||||
|
super.endpointHook(facet, innerEp);
|
||||||
|
if (Observe.isClassOf(innerEp.spec.assertion) &&
|
||||||
|
Inbound.isClassOf(innerEp.spec.assertion[0]))
|
||||||
|
{
|
||||||
|
// We know that innerEp.spec.assertion is an Observe(Inbound(...)). Also, if
|
||||||
|
// innerEp.spec.analysis exists, it will be consonant with innerEp.spec.assertion.
|
||||||
|
// Beware of completely-constant patterns, which cause skeleton to be null!
|
||||||
|
this.hookEndpointLifecycle(innerEp, this.outerFacet.addEndpoint(() => {
|
||||||
|
const assertion = Observe(innerEp.spec.assertion[0][0]);
|
||||||
|
const h = innerEp.spec.analysis;
|
||||||
|
const innerCallback = h.callback;
|
||||||
|
const callback = (innerCallback === void 0) ? void 0 :
|
||||||
|
function (evt: EventType, captures: Array<Value>) {
|
||||||
|
innerCallback.call(this, evt, captures);
|
||||||
|
innerDs.start();
|
||||||
|
};
|
||||||
|
const analysis: Analysis | null = (h === null) ? null :
|
||||||
|
(h.skeleton === void 0
|
||||||
|
? {
|
||||||
|
skeleton: void 0,
|
||||||
|
constPaths: h.constPaths,
|
||||||
|
constVals: h.constVals.map((v) => v[0]),
|
||||||
|
capturePaths: h.capturePaths.map((p) => p.slice(1)),
|
||||||
|
assertion,
|
||||||
|
callback
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
skeleton: h.skeleton[1],
|
||||||
|
constPaths: h.constPaths.map((p) => p.slice(1)),
|
||||||
|
constVals: h.constVals,
|
||||||
|
capturePaths: h.capturePaths.map((p) => p.slice(1)),
|
||||||
|
assertion,
|
||||||
|
callback
|
||||||
|
});
|
||||||
|
return { assertion, analysis };
|
||||||
|
}, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustIndex(a: Value, count: number) {
|
||||||
|
const net = super.adjustIndex(a, count);
|
||||||
|
if (Outbound.isClassOf(a)) {
|
||||||
|
switch (net) {
|
||||||
|
case ChangeDescription.ABSENT_TO_PRESENT:
|
||||||
|
this.outerFacet.actor.pushScript(() => {
|
||||||
|
this.outerFacet.actor.adhocAssert(a.get(0));
|
||||||
|
});
|
||||||
|
this.outerFacet.actor.dataspace.start();
|
||||||
|
break;
|
||||||
|
case ChangeDescription.PRESENT_TO_ABSENT:
|
||||||
|
this.outerFacet.actor.pushScript(() => {
|
||||||
|
this.outerFacet.actor.adhocRetract(a.get(0));
|
||||||
|
});
|
||||||
|
this.outerFacet.actor.dataspace.start();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return net;
|
||||||
|
}
|
||||||
|
|
||||||
|
hookEndpointLifecycle(innerEp: Endpoint, outerEp: Endpoint) {
|
||||||
|
const _refresh = innerEp.refresh;
|
||||||
|
innerEp.refresh = function () {
|
||||||
|
_refresh.call(this);
|
||||||
|
outerEp.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const _destroy = innerEp.destroy;
|
||||||
|
innerEp.destroy = function (emitPatches: boolean) {
|
||||||
|
_destroy.call(this, emitPatches);
|
||||||
|
outerEp.destroy(true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): this {
|
||||||
|
this.outerFacet.actor.dataspace.start();
|
||||||
|
this.outerFacet.actor.pushScript(() => {
|
||||||
|
Dataspace.withCurrentFacet(this.outerFacet, () => {
|
||||||
|
if (this.outerFacet.isLive) {
|
||||||
|
Dataspace.deferTurn(() => {
|
||||||
|
const stillBusy = this.runScripts();
|
||||||
|
if (stillBusy) this.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ground(): Ground {
|
||||||
|
return this.outerFacet.actor.dataspace.ground();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inNestedDataspace(bootProc: Script): Script {
|
||||||
|
return () => {
|
||||||
|
const outerFacet = Dataspace.currentFacet;
|
||||||
|
outerFacet.addDataflow(function () {});
|
||||||
|
// ^ eww! Dummy endpoint to keep the root facet of the relay alive.
|
||||||
|
const innerDs = new NestedDataspace(outerFacet, function () {
|
||||||
|
Dataspace.currentFacet.addStartScript(() => bootProc.call(this));
|
||||||
|
});
|
||||||
|
innerDs.start();
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,402 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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 } from './idcoll.js';
|
||||||
|
import { is, Value, Record, Set, Dictionary, _canonicalString } from 'preserves';
|
||||||
|
|
||||||
|
import { Bag, ChangeDescription } from './bag.js';
|
||||||
|
import { Discard, Capture, Observe } from './assertions.js';
|
||||||
|
|
||||||
|
import * as Stack from './stack.js';
|
||||||
|
|
||||||
|
export enum EventType {
|
||||||
|
ADDED = +1,
|
||||||
|
REMOVED = -1,
|
||||||
|
MESSAGE = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HandlerCallback = (eventType: EventType, bindings: Array<Value>) => void;
|
||||||
|
|
||||||
|
export type Shape = string;
|
||||||
|
export type Skeleton = null | { shape: Shape, members: Skeleton[] };
|
||||||
|
export type Path = Array<number>;
|
||||||
|
export interface Analysis {
|
||||||
|
skeleton: Skeleton;
|
||||||
|
constPaths: Array<Path>;
|
||||||
|
constVals: Array<Value>;
|
||||||
|
capturePaths: Array<Path>;
|
||||||
|
assertion: Value;
|
||||||
|
callback?: HandlerCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _nop = () => {};
|
||||||
|
|
||||||
|
export class Index {
|
||||||
|
readonly allAssertions: Bag = new Bag();
|
||||||
|
readonly root: Node = new Node(new Continuation(new Set()));
|
||||||
|
|
||||||
|
addHandler(analysisResults: Analysis, callback: HandlerCallback) {
|
||||||
|
let {skeleton, constPaths, constVals, capturePaths} = analysisResults;
|
||||||
|
const continuation = this.root.extend(skeleton);
|
||||||
|
let constValMap = continuation.leafMap.get(constPaths);
|
||||||
|
if (!constValMap) {
|
||||||
|
constValMap = new Dictionary();
|
||||||
|
continuation.cachedAssertions.forEach((a) => {
|
||||||
|
const key = projectPaths(a, constPaths);
|
||||||
|
let leaf = constValMap.get(key);
|
||||||
|
if (!leaf) {
|
||||||
|
leaf = new Leaf();
|
||||||
|
constValMap.set(key, leaf);
|
||||||
|
}
|
||||||
|
leaf.cachedAssertions.add(a);
|
||||||
|
});
|
||||||
|
continuation.leafMap.set(constPaths, constValMap);
|
||||||
|
}
|
||||||
|
let leaf = constValMap.get(constVals);
|
||||||
|
if (!leaf) {
|
||||||
|
leaf = new Leaf();
|
||||||
|
constValMap.set(constVals, leaf);
|
||||||
|
}
|
||||||
|
let handler = leaf.handlerMap.get(capturePaths);
|
||||||
|
if (!handler) {
|
||||||
|
const cachedCaptures = new Bag();
|
||||||
|
leaf.cachedAssertions.forEach((a) =>
|
||||||
|
cachedCaptures._items.update(projectPaths(a, capturePaths), n => n + 1, 0));
|
||||||
|
handler = new Handler(cachedCaptures);
|
||||||
|
leaf.handlerMap.set(capturePaths, handler);
|
||||||
|
}
|
||||||
|
handler.callbacks.add(callback);
|
||||||
|
handler.cachedCaptures.forEach((_count, captures) =>
|
||||||
|
callback(EventType.ADDED, captures as Array<Value>));
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHandler(analysisResults: Analysis, callback: HandlerCallback) {
|
||||||
|
let {skeleton, constPaths, constVals, capturePaths} = analysisResults;
|
||||||
|
const continuation = this.root.extend(skeleton);
|
||||||
|
let constValMap = continuation.leafMap.get(constPaths);
|
||||||
|
if (!constValMap) return;
|
||||||
|
let leaf = constValMap.get(constVals);
|
||||||
|
if (!leaf) return;
|
||||||
|
let handler = leaf.handlerMap.get(capturePaths);
|
||||||
|
if (!handler) return;
|
||||||
|
handler.callbacks.delete(callback);
|
||||||
|
if (handler.callbacks.size === 0) {
|
||||||
|
leaf.handlerMap.delete(capturePaths);
|
||||||
|
}
|
||||||
|
if (leaf.isEmpty()) {
|
||||||
|
constValMap.delete(constVals);
|
||||||
|
}
|
||||||
|
if (constValMap.size === 0) {
|
||||||
|
continuation.leafMap.delete(constPaths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustAssertion(outerValue: Value, delta: number): ChangeDescription {
|
||||||
|
let net = this.allAssertions.change(outerValue, delta);
|
||||||
|
switch (net) {
|
||||||
|
case ChangeDescription.ABSENT_TO_PRESENT:
|
||||||
|
this.root.modify(
|
||||||
|
EventType.ADDED,
|
||||||
|
outerValue,
|
||||||
|
(c, v) => c.cachedAssertions.add(v),
|
||||||
|
(l, v) => l.cachedAssertions.add(v),
|
||||||
|
(h, vs) => {
|
||||||
|
if (h.cachedCaptures.change(vs, +1) === ChangeDescription.ABSENT_TO_PRESENT)
|
||||||
|
h.callbacks.forEach(cb => cb(EventType.ADDED, vs));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ChangeDescription.PRESENT_TO_ABSENT:
|
||||||
|
this.root.modify(
|
||||||
|
EventType.REMOVED,
|
||||||
|
outerValue,
|
||||||
|
(c, v) => c.cachedAssertions.delete(v),
|
||||||
|
(l, v) => l.cachedAssertions.delete(v),
|
||||||
|
(h, vs) => {
|
||||||
|
if (h.cachedCaptures.change(vs, -1) === ChangeDescription.PRESENT_TO_ABSENT)
|
||||||
|
h.callbacks.forEach(cb => cb(EventType.REMOVED, vs));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return net;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAssertion(v: Value) {
|
||||||
|
this.adjustAssertion(v, +1);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAssertion(v: Value) {
|
||||||
|
this.adjustAssertion(v, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(v: Value, leafCallback: (l: Leaf, v: Value) => void = _nop) {
|
||||||
|
this.root.modify(EventType.MESSAGE, v, _nop, leafCallback, (h, vs) =>
|
||||||
|
h.callbacks.forEach(cb => cb(EventType.MESSAGE, vs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
readonly continuation: Continuation;
|
||||||
|
readonly edges: { [selector: string]: { [shape: string]: Node } } = {};
|
||||||
|
|
||||||
|
constructor(continuation: Continuation) {
|
||||||
|
this.continuation = continuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(skeleton: Skeleton): Continuation {
|
||||||
|
const path = [];
|
||||||
|
|
||||||
|
function walkNode(node: Node,
|
||||||
|
popCount: number,
|
||||||
|
index: number,
|
||||||
|
skeleton: Skeleton): [number, Node]
|
||||||
|
{
|
||||||
|
if (skeleton === null) {
|
||||||
|
return [popCount, node];
|
||||||
|
} else {
|
||||||
|
const selector = '' + popCount + ',' + index;
|
||||||
|
const cls = skeleton.shape;
|
||||||
|
let table = node.edges[selector];
|
||||||
|
if (!table) {
|
||||||
|
table = {};
|
||||||
|
node.edges[selector] = table;
|
||||||
|
}
|
||||||
|
let nextNode = table[cls];
|
||||||
|
if (!nextNode) {
|
||||||
|
nextNode = new Node(new Continuation(
|
||||||
|
node.continuation.cachedAssertions.filter(
|
||||||
|
(a) => classOf(projectPath(a, path)) === cls)));
|
||||||
|
table[cls] = nextNode;
|
||||||
|
}
|
||||||
|
popCount = 0;
|
||||||
|
index = 0;
|
||||||
|
path.push(index);
|
||||||
|
skeleton.members.forEach((member) => {
|
||||||
|
[popCount, nextNode] = walkNode(nextNode, popCount, index, member);
|
||||||
|
index++;
|
||||||
|
path.pop();
|
||||||
|
path.push(index);
|
||||||
|
});
|
||||||
|
path.pop();
|
||||||
|
return [popCount + 1, nextNode];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return walkNode(this, 0, 0, skeleton)[1].continuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(operation: EventType,
|
||||||
|
outerValue: Value,
|
||||||
|
m_cont: (c: Continuation, v: Value) => void,
|
||||||
|
m_leaf: (l: Leaf, v: Value) => void,
|
||||||
|
m_handler: (h: Handler, vs: Array<Value>) => void)
|
||||||
|
{
|
||||||
|
function walkNode(node: Node, termStack: Stack.NonEmptyStack<Array<Value>>) {
|
||||||
|
walkContinuation(node.continuation);
|
||||||
|
Object.entries(node.edges).forEach(([selectorStr, table]) => {
|
||||||
|
const selector = parseSelector(selectorStr);
|
||||||
|
let nextStack = Stack.dropNonEmpty(termStack, selector.popCount);
|
||||||
|
let nextValue = step(nextStack.item, selector.index);
|
||||||
|
let nextNode = table[classOf(nextValue)];
|
||||||
|
if (nextNode) walkNode(nextNode, Stack.push(nextValue as Array<Value>, nextStack));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkContinuation(continuation: Continuation) {
|
||||||
|
m_cont(continuation, outerValue);
|
||||||
|
continuation.leafMap.forEach((constValMap, constPaths) => {
|
||||||
|
let constVals = projectPaths(outerValue, constPaths as Array<Path>);
|
||||||
|
let leaf = constValMap.get(constVals);
|
||||||
|
if (!leaf && operation === EventType.ADDED) {
|
||||||
|
leaf = new Leaf();
|
||||||
|
constValMap.set(constVals, leaf);
|
||||||
|
}
|
||||||
|
if (leaf) {
|
||||||
|
m_leaf(leaf, outerValue);
|
||||||
|
leaf.handlerMap.forEach((handler, capturePaths) => {
|
||||||
|
m_handler(handler, projectPaths(outerValue, capturePaths as Array<Path>));
|
||||||
|
});
|
||||||
|
if (operation === EventType.REMOVED && leaf.isEmpty()) {
|
||||||
|
constValMap.delete(constVals);
|
||||||
|
if (constValMap.size === 0) {
|
||||||
|
continuation.leafMap.delete(constPaths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
walkNode(this, Stack.push([outerValue], Stack.empty()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSelector(s: string): { popCount: number, index: number } {
|
||||||
|
const pos = s.indexOf(',');
|
||||||
|
return { popCount: parseInt(s.substr(0, pos)),
|
||||||
|
index: parseInt(s.substr(pos + 1)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
class Continuation {
|
||||||
|
readonly cachedAssertions: Set;
|
||||||
|
readonly leafMap: Dictionary<Dictionary<Leaf>> = new Dictionary();
|
||||||
|
|
||||||
|
constructor(cachedAssertions: Set) {
|
||||||
|
this.cachedAssertions = cachedAssertions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Leaf {
|
||||||
|
readonly cachedAssertions: Set = new Set();
|
||||||
|
readonly handlerMap: Dictionary<Handler> = new Dictionary();
|
||||||
|
|
||||||
|
isEmpty(): boolean {
|
||||||
|
return this.cachedAssertions.size === 0 && this.handlerMap.size === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Handler {
|
||||||
|
readonly cachedCaptures: Bag;
|
||||||
|
readonly callbacks: IdentitySet<HandlerCallback> = new IdentitySet();
|
||||||
|
|
||||||
|
constructor(cachedCaptures: Bag) {
|
||||||
|
this.cachedCaptures = cachedCaptures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function classOf(v: any): string | null {
|
||||||
|
if (Record.isRecord(v)) {
|
||||||
|
const ci = v.getConstructorInfo();
|
||||||
|
return _canonicalString(ci.label) + '/' + ci.arity;
|
||||||
|
} else if (Array.isArray(v)) {
|
||||||
|
return '' + v.length;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function step(v: Array<Value> /* includes Record! */, index: number) {
|
||||||
|
return v[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectPath(v: Value, path: Path) {
|
||||||
|
for (let index of path) {
|
||||||
|
v = step(v as Array<Value>, index);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectPaths(v: Value, paths: Array<Path>) {
|
||||||
|
return paths.map((path) => projectPath(v, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analyzeAssertion(a: Value): Analysis {
|
||||||
|
const constPaths = [];
|
||||||
|
const constVals = [];
|
||||||
|
const capturePaths = [];
|
||||||
|
const path = [];
|
||||||
|
|
||||||
|
function walk(a: Value): Skeleton {
|
||||||
|
if (Capture.isClassOf(a)) {
|
||||||
|
// NB. isUnrestricted relies on the specific order that
|
||||||
|
// capturePaths is computed here.
|
||||||
|
capturePaths.push(path.slice());
|
||||||
|
return walk(a[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Discard.isClassOf(a)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cls = classOf(a);
|
||||||
|
if (cls !== null) {
|
||||||
|
let aa = a as Array<Value>;
|
||||||
|
// ^ We know this is safe because it's either Record or Array
|
||||||
|
let arity = aa.length;
|
||||||
|
let result = { shape: cls, members: [] };
|
||||||
|
path.push(0);
|
||||||
|
for (let i = 0; i < arity; i++) {
|
||||||
|
path[path.length - 1] = i;
|
||||||
|
result.members.push(walk(step(aa, i)));
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
constPaths.push(path);
|
||||||
|
constVals.push(a);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let skeleton = walk(a);
|
||||||
|
|
||||||
|
return { skeleton, constPaths, constVals, capturePaths, assertion: Observe(a) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function match(p: Value, v: Value): Array<Value> | false {
|
||||||
|
const captures = [];
|
||||||
|
|
||||||
|
function walk(p: Value, v: Value): boolean {
|
||||||
|
if (Capture.isClassOf(p)) {
|
||||||
|
if (!walk(p[0], v)) return false;
|
||||||
|
captures.push(v);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Discard.isClassOf(p)) return true;
|
||||||
|
|
||||||
|
const pcls = classOf(p);
|
||||||
|
const vcls = classOf(v);
|
||||||
|
if (pcls !== vcls) return false;
|
||||||
|
|
||||||
|
if (pcls === null) return is(p, v);
|
||||||
|
|
||||||
|
const pp = p as Array<Value>;
|
||||||
|
const vv = v as Array<Value>;
|
||||||
|
// ^ These are safe because classOf yielded nonnull for both
|
||||||
|
|
||||||
|
return pp.every((pv, i) => walk(pv, vv[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return walk(p, v) ? captures : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCompletelyConcrete(p: Value) {
|
||||||
|
function walk(p: Value) {
|
||||||
|
if (Capture.isClassOf(p)) return false;
|
||||||
|
if (Discard.isClassOf(p)) return false;
|
||||||
|
|
||||||
|
const cls = classOf(p);
|
||||||
|
if (cls === null) return true;
|
||||||
|
return (p as Array<Value>).every(walk);
|
||||||
|
}
|
||||||
|
return walk(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withoutCaptures(p: Value) {
|
||||||
|
function walk(p: Value) {
|
||||||
|
if (Capture.isClassOf(p)) return walk(p[0]);
|
||||||
|
if (Discard.isClassOf(p)) return p;
|
||||||
|
|
||||||
|
const cls = classOf(p);
|
||||||
|
if (cls === null) return p;
|
||||||
|
if (Record.isRecord(p)) return new Record(p.label, p.map(walk));
|
||||||
|
return (p as Array<Value>).map(walk);
|
||||||
|
}
|
||||||
|
return walk(p);
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
||||||
// Copyright (C) 2016-2018 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
// Copyright (C) 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,15 +16,12 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'special.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
// $Special: Builder of singleton "atoms".
|
// $Special: Builder of singleton "atoms".
|
||||||
|
|
||||||
function $Special(name) {
|
export class $Special {
|
||||||
this.name = name;
|
readonly name: string;
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = $Special;
|
constructor(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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/>.
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type NonEmptyStack<T> = { item: T, rest: Stack<T> };
|
||||||
|
export type Stack<T> = null | NonEmptyStack<T>;
|
||||||
|
|
||||||
|
export function empty<T>(): Stack<T> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function push<T>(item: T, rest: Stack<T>): NonEmptyStack<T> {
|
||||||
|
return { item, rest };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nonEmpty<T>(s: Stack<T>): s is NonEmptyStack<T> {
|
||||||
|
return s !== empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rest<T>(s: Stack<T>): Stack<T> {
|
||||||
|
if (nonEmpty(s)) {
|
||||||
|
return s.rest;
|
||||||
|
} else {
|
||||||
|
throw new Error("pop from empty Stack");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drop<T>(s: Stack<T>, n: number): Stack<T> {
|
||||||
|
while (n--) s = rest(s);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dropNonEmpty<T>(s: NonEmptyStack<T>, n: number): NonEmptyStack<T> {
|
||||||
|
while (n--) {
|
||||||
|
s = s.rest;
|
||||||
|
if (!nonEmpty(s)) throw new Error("dropNonEmpty popped too far");
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
|
@ -0,0 +1,304 @@
|
||||||
|
//---------------------------------------------------------------------------
|
||||||
|
// @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 { Dataspace, Facet } from './dataspace.js';
|
||||||
|
// import { Observe, Outbound, Inbound, Capture, Discard } from './assertions.js';
|
||||||
|
// import * as Skeleton from './skeleton.js';
|
||||||
|
|
||||||
|
// import { preserves, Value, Bytes, Record, Dictionary, encode, decode } from 'preserves';
|
||||||
|
|
||||||
|
// type MessageHandler = (e: Bytes) => void;
|
||||||
|
// type ImplementationType = 'node.js' | 'browser' | 'none';
|
||||||
|
|
||||||
|
// type WorkerConstructor = {
|
||||||
|
// new (stringUrl: string | URL, options?: WorkerOptions): Worker;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// function extractBytes(h: MessageHandler): (e: MessageEvent<Bytes> | Bytes) => void {
|
||||||
|
// return (e) => {
|
||||||
|
// const bs = (e instanceof MessageEvent) ? e.data : e;
|
||||||
|
// return h(bs);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const WorkerEvent = Record.makeConstructor('--worker-event', ['epId', 'event', 'vs']);
|
||||||
|
// const { implementationType, _Worker, postMessage, onMessage, isMainThread }: {
|
||||||
|
// implementationType: ImplementationType,
|
||||||
|
// _Worker?: WorkerConstructor,
|
||||||
|
// postMessage?: (m: any) => void,
|
||||||
|
// onMessage?: (handler: MessageHandler) => void,
|
||||||
|
// isMainThread: boolean,
|
||||||
|
// } = (function () {
|
||||||
|
// try {
|
||||||
|
// // Let's see if we're in node.js with the web worker extension enabled.
|
||||||
|
// const { Worker, parentPort, isMainThread } = require('worker_threads');
|
||||||
|
// return {
|
||||||
|
// implementationType: 'node.js' as ImplementationType,
|
||||||
|
// _Worker: Worker,
|
||||||
|
// postMessage: (m: any) => parentPort.postMessage(m),
|
||||||
|
// onMessage: (handler: MessageHandler) => {
|
||||||
|
// parentPort.removeAllListeners('message');
|
||||||
|
// parentPort.on('message', extractBytes(handler));
|
||||||
|
// },
|
||||||
|
// isMainThread,
|
||||||
|
// };
|
||||||
|
// } catch (_e) {
|
||||||
|
// // Well, something didn't work there. Could we be in the browser?
|
||||||
|
// if (typeof window !== 'undefined' && 'Worker' in window) {
|
||||||
|
// // Yep.
|
||||||
|
// return {
|
||||||
|
// implementationType: 'browser' as ImplementationType,
|
||||||
|
// _Worker: Worker,
|
||||||
|
// postMessage,
|
||||||
|
// onMessage: (handler: MessageHandler) => onmessage = extractBytes(handler),
|
||||||
|
// isMainThread: (typeof self === 'undefined'),
|
||||||
|
// };
|
||||||
|
// } else {
|
||||||
|
// // Nope. No support, then.
|
||||||
|
// return {
|
||||||
|
// implementationType: 'none' as ImplementationType,
|
||||||
|
// isMainThread: true,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })();
|
||||||
|
|
||||||
|
// function encodePacket(p: Value) {
|
||||||
|
// return Bytes.toIO(encode(p));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function decodePacket(m: Bytes) {
|
||||||
|
// return decode(m);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function sendPacket(ch: Worker, p: Value) {
|
||||||
|
// ch.postMessage(encodePacket(p));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function listen(w: Worker, eventType: string, handler: (... args: any[]) => any) {
|
||||||
|
// if ('on' in w) {
|
||||||
|
// (w as any).on(eventType, handler);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const k = 'on' + eventType;
|
||||||
|
// if (k in w) {
|
||||||
|
// w[k] = handler;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export function spawnWorker(workerSourceFilename: string) {
|
||||||
|
// if (implementationType === 'none') {
|
||||||
|
// // In older versions of node.js, try adding --experimental-worker flag to the command line.
|
||||||
|
// throw new Error("Cannot spawnWorker without a web worker implementation available");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Dataspace.spawn(workerSourceFilename, function () {
|
||||||
|
// const outerFacet = Dataspace.currentFacet;
|
||||||
|
// outerFacet.addDataflow(function () {});
|
||||||
|
// // ^ eww! Dummy endpoint to keep the root facet of the relay alive.
|
||||||
|
|
||||||
|
// let endpoints = new Dictionary<Facet>();
|
||||||
|
|
||||||
|
// const w = new _Worker(workerSourceFilename);
|
||||||
|
// listen(w, 'error', Dataspace.wrapExternal((err) => {
|
||||||
|
// throw err;
|
||||||
|
// }));
|
||||||
|
// listen(w, 'exit', Dataspace.wrapExternal(() => {
|
||||||
|
// outerFacet.stop();
|
||||||
|
// }));
|
||||||
|
// listen(w, 'message', Dataspace.wrapExternal(extractBytes((msg: Bytes) => {
|
||||||
|
// const m = decodePacket(msg) as Array<Value>;
|
||||||
|
// switch (m[0]) {
|
||||||
|
// case 'assert': {
|
||||||
|
// const [ep, a] = m.slice(1);
|
||||||
|
// if (!endpoints.has(ep)) {
|
||||||
|
// outerFacet.actor.addFacet(outerFacet, function () {
|
||||||
|
// const epFacet = Dataspace.currentFacet;
|
||||||
|
// endpoints = endpoints.set(ep, epFacet);
|
||||||
|
// epFacet.addStopScript(() => { endpoints.delete(ep); });
|
||||||
|
// Dataspace.declareField(this, 'assertion', a);
|
||||||
|
// epFacet.addEndpoint(() => {
|
||||||
|
// if (Observe.isClassOf(this.assertion)) {
|
||||||
|
// const spec = this.assertion.get(0);
|
||||||
|
// const analysis = Skeleton.analyzeAssertion(spec);
|
||||||
|
// analysis.callback = Dataspace.wrap((evt, vs) => {
|
||||||
|
// epFacet.actor.scheduleScript(() => {
|
||||||
|
// sendPacket(w, ['event', ep, evt, vs]);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// return { assertion: Observe(spec), analysis };
|
||||||
|
// } else {
|
||||||
|
// return { assertion: this.assertion, analysis: null };
|
||||||
|
// }
|
||||||
|
// }, true);
|
||||||
|
// }, true);
|
||||||
|
// } else {
|
||||||
|
// endpoints.get(ep).fields.assertion = a;
|
||||||
|
// }
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// case 'clear': {
|
||||||
|
// const ep = m[1];
|
||||||
|
// const epFacet = endpoints.get(ep);
|
||||||
|
// if (epFacet) epFacet.stop(() => { endpoints.delete(ep); });
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// case 'message': {
|
||||||
|
// const body = m[1];
|
||||||
|
// Dataspace.send(body);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// default: {
|
||||||
|
// throw new Error(
|
||||||
|
// preserves`Invalid Worker protocol message from Worker: ${m}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })));
|
||||||
|
// }, null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export function spawnWorkerRelay() {
|
||||||
|
// if (implementationType === 'none') return;
|
||||||
|
// if (isMainThread) return;
|
||||||
|
|
||||||
|
// Dataspace.currentFacet.actor.dataspace.ground().addStopHandler(() => {
|
||||||
|
// process.exit();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Dataspace.currentFacet.addStartScript(function () {
|
||||||
|
// Dataspace.spawn('WorkerRelay', function () {
|
||||||
|
// const outerFacet = Dataspace.currentFacet;
|
||||||
|
|
||||||
|
// const finish = Dataspace.backgroundTask();
|
||||||
|
// outerFacet.addStopScript(finish);
|
||||||
|
|
||||||
|
// const outboundEndpoints = new Dictionary<number>();
|
||||||
|
// const inboundEndpoints = new Dictionary<{ epId: number, facet: Facet }>();
|
||||||
|
// let nextId = 0;
|
||||||
|
|
||||||
|
// function sendToParent(m: Value) {
|
||||||
|
// postMessage(encodePacket(m));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onMessage(Dataspace.wrapExternal(function (msg: Bytes) {
|
||||||
|
// const m = decodePacket(msg);
|
||||||
|
// if (Array.isArray(m) && m[0] === 'event') {
|
||||||
|
// const [epId, evt, vs] = m.slice(1);
|
||||||
|
// Dataspace.send(WorkerEvent(epId, evt, vs));
|
||||||
|
// } else {
|
||||||
|
// throw new Error(
|
||||||
|
// preserves`Invalid Worker protocol message from parent: ${m}`);
|
||||||
|
// }
|
||||||
|
// }));
|
||||||
|
|
||||||
|
// outerFacet.addEndpoint(function () {
|
||||||
|
// const analysis = Skeleton.analyzeAssertion(Outbound(Capture(Discard._instance)));
|
||||||
|
// analysis.callback = Dataspace.wrap(function (evt, vs) {
|
||||||
|
// outerFacet.actor.scheduleScript(function () {
|
||||||
|
// switch (evt) {
|
||||||
|
// case Skeleton.EventType.ADDED: {
|
||||||
|
// const epId = nextId++;
|
||||||
|
// outboundEndpoints.set(vs[0], epId);
|
||||||
|
// sendToParent(['assert', epId, vs[0]]);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// case Skeleton.EventType.REMOVED: {
|
||||||
|
// const epId = outboundEndpoints.get(vs[0]);
|
||||||
|
// outboundEndpoints.delete(vs[0]);
|
||||||
|
// sendToParent(['clear', epId]);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// case Skeleton.EventType.MESSAGE: {
|
||||||
|
// sendToParent(['message', vs[0]]);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// return { assertion: analysis.assertion, analysis };
|
||||||
|
// }, false);
|
||||||
|
|
||||||
|
// outerFacet.addEndpoint(function () {
|
||||||
|
// const analysis =
|
||||||
|
// Skeleton.analyzeAssertion(Observe(Inbound(Capture(Discard._instance))));
|
||||||
|
// analysis.callback = Dataspace.wrap(function (evt, vs) {
|
||||||
|
// outerFacet.actor.scheduleScript(function () {
|
||||||
|
// const spec = vs[0];
|
||||||
|
// switch (evt) {
|
||||||
|
// case Skeleton.EventType.ADDED: {
|
||||||
|
// const epId = nextId++;
|
||||||
|
// outerFacet.actor.addFacet(outerFacet, function () {
|
||||||
|
// const innerFacet = Dataspace.currentFacet;
|
||||||
|
// inboundEndpoints.set(spec, { epId, facet: innerFacet });
|
||||||
|
// innerFacet.addEndpoint(function () {
|
||||||
|
// const analysis = Skeleton.analyzeAssertion(
|
||||||
|
// WorkerEvent(epId, Capture(Discard._instance), Capture(Discard._instance)));
|
||||||
|
// analysis.callback = Dataspace.wrap(function (evt, vs) {
|
||||||
|
// if (evt === Skeleton.EventType.MESSAGE) {
|
||||||
|
// evt = vs[0] as Skeleton.EventType;
|
||||||
|
// vs = vs[1] as Array<Value>;
|
||||||
|
// const a = Skeleton.instantiateAssertion(Inbound(spec), vs);
|
||||||
|
// innerFacet.actor.scheduleScript(function () {
|
||||||
|
// switch (evt) {
|
||||||
|
// case Skeleton.EventType.ADDED:
|
||||||
|
// innerFacet.actor.addFacet(innerFacet, function () {
|
||||||
|
// const assertionFacet = Dataspace.currentFacet;
|
||||||
|
// assertionFacet.addEndpoint(function () {
|
||||||
|
// return { assertion: a, analysis: null };
|
||||||
|
// }, false);
|
||||||
|
// assertionFacet.addEndpoint(function () {
|
||||||
|
// const analysis = Skeleton.analyzeAssertion(
|
||||||
|
// WorkerEvent(epId, Skeleton.EventType.REMOVED, vs));
|
||||||
|
// analysis.callback = Dataspace.wrap(() => {
|
||||||
|
// assertionFacet.actor.scheduleScript(function () {
|
||||||
|
// assertionFacet.stop();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// return { assertion: analysis.assertion, analysis };
|
||||||
|
// }, false);
|
||||||
|
// }, true);
|
||||||
|
// break;
|
||||||
|
// case Skeleton.EventType.MESSAGE:
|
||||||
|
// Dataspace.send(a);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// return { assertion: analysis.assertion, analysis };
|
||||||
|
// }, false);
|
||||||
|
// }, true);
|
||||||
|
// sendToParent(['assert', epId, Observe(spec)]);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// case Skeleton.EventType.REMOVED: {
|
||||||
|
// const { epId, facet } = inboundEndpoints.get(spec);
|
||||||
|
// inboundEndpoints.delete(spec);
|
||||||
|
// facet.stop();
|
||||||
|
// sendToParent(['clear', epId]);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// return { assertion: analysis.assertion, analysis };
|
||||||
|
// }, false);
|
||||||
|
// }, null);
|
||||||
|
// });
|
||||||
|
// }
|
|
@ -1,594 +0,0 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'skeleton.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
const Immutable = require("immutable");
|
|
||||||
const { Record } = require("preserves");
|
|
||||||
|
|
||||||
const $Special = require('./special.js');
|
|
||||||
const Bag = require('./bag.js');
|
|
||||||
const { Discard, Capture, Observe } = require('./assertions.js');
|
|
||||||
|
|
||||||
const EVENT_ADDED = +1;
|
|
||||||
const EVENT_REMOVED = -1;
|
|
||||||
const EVENT_MESSAGE = 0;
|
|
||||||
|
|
||||||
function Index() {
|
|
||||||
this.allAssertions = Bag.Bag();
|
|
||||||
this.root = new Node(new Continuation(Immutable.Set()));
|
|
||||||
}
|
|
||||||
|
|
||||||
function Node(continuation) {
|
|
||||||
this.continuation = continuation;
|
|
||||||
this.edges = Immutable.Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
function Selector(popCount, index) {
|
|
||||||
this.popCount = popCount;
|
|
||||||
this.index = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
Selector.prototype.equals = function (other) {
|
|
||||||
return (this.popCount === other.popCount) && (this.index === other.index);
|
|
||||||
};
|
|
||||||
|
|
||||||
Selector.prototype.hashCode = function () {
|
|
||||||
return (this.popCount * 5) + this.index;
|
|
||||||
};
|
|
||||||
|
|
||||||
function Continuation(cachedAssertions) {
|
|
||||||
this.cachedAssertions = cachedAssertions;
|
|
||||||
this.leafMap = Immutable.Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
function Leaf() {
|
|
||||||
this.cachedAssertions = Immutable.Set();
|
|
||||||
this.handlerMap = Immutable.Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
Leaf.prototype.isEmpty = function () {
|
|
||||||
return this.cachedAssertions.isEmpty() && this.handlerMap.isEmpty();
|
|
||||||
};
|
|
||||||
|
|
||||||
function Handler(cachedCaptures) {
|
|
||||||
this.cachedCaptures = cachedCaptures;
|
|
||||||
this.callbacks = Immutable.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
function classOf(v) {
|
|
||||||
if (v instanceof Record) {
|
|
||||||
return v.getConstructorInfo();
|
|
||||||
} else if (Immutable.List.isList(v)) {
|
|
||||||
return v.size;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function step(v, index) {
|
|
||||||
return v.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function projectPath(v, path) {
|
|
||||||
path.forEach((index) => { v = step(v, index); return true; });
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
function projectPaths(v, paths) {
|
|
||||||
return paths.map((path) => projectPath(v, path));
|
|
||||||
}
|
|
||||||
|
|
||||||
Node.prototype.extend = function(skeleton) {
|
|
||||||
function walkNode(path, node, popCount, index, skeleton) {
|
|
||||||
if (skeleton === null) {
|
|
||||||
return [popCount, node];
|
|
||||||
} else {
|
|
||||||
let selector = new Selector(popCount, index);
|
|
||||||
let cls = skeleton[0];
|
|
||||||
let table = node.edges.get(selector, false);
|
|
||||||
if (!table) {
|
|
||||||
table = Immutable.Map();
|
|
||||||
node.edges = node.edges.set(selector, table);
|
|
||||||
}
|
|
||||||
let nextNode = table.get(cls, false);
|
|
||||||
if (!nextNode) {
|
|
||||||
nextNode = new Node(new Continuation(
|
|
||||||
node.continuation.cachedAssertions.filter(
|
|
||||||
(a) => Immutable.is(classOf(projectPath(unscope(a), path)), cls))));
|
|
||||||
table = table.set(cls, nextNode);
|
|
||||||
node.edges = node.edges.set(selector, table);
|
|
||||||
}
|
|
||||||
|
|
||||||
popCount = 0;
|
|
||||||
index = 0;
|
|
||||||
path = path.push(index);
|
|
||||||
for (let i = 1; i < skeleton.length; i++) {
|
|
||||||
[popCount, nextNode] = walkNode(path, nextNode, popCount, index, skeleton[i]);
|
|
||||||
index++;
|
|
||||||
path = path.pop().push(index);
|
|
||||||
}
|
|
||||||
return [popCount + 1, nextNode];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let [_popCount, finalNode] = walkNode(Immutable.List(), this, 0, 0, skeleton);
|
|
||||||
return finalNode.continuation;
|
|
||||||
};
|
|
||||||
|
|
||||||
function pathCmp(a, b) {
|
|
||||||
const ai = a.values();
|
|
||||||
let result = 0;
|
|
||||||
b.forEach((bv) => {
|
|
||||||
const e = ai.next();
|
|
||||||
if (e.done || e.value < bv) { result = -1; return false; }
|
|
||||||
else if (e.value > bv) { result = +1; return false; }
|
|
||||||
else { /* keep scanning down */ }
|
|
||||||
});
|
|
||||||
if (result !== 0) return result;
|
|
||||||
return ai.next().done ? 0 : +1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUnrestricted(capturePaths, restrictionPaths) {
|
|
||||||
//------------------------------------------------------------------------------------------
|
|
||||||
// Determining a match between capturePaths and restrictionPaths relies on the particular
|
|
||||||
// *order* that captures are computed in `analyzeAssertion`. If the order is changed, or
|
|
||||||
// becomes non-deterministic, this function will have to be revisited.
|
|
||||||
//------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// We are "unrestricted" if we Set(capturePaths) ⊆ Set(restrictionPaths). Since both
|
|
||||||
// variables really hold lists, we operate with awareness of the order the lists are built
|
|
||||||
// here. We know that the lists are built in fringe order; that is, they are sorted wrt
|
|
||||||
// `pathCmp`.
|
|
||||||
|
|
||||||
if (restrictionPaths === false) return true; // not visibility-restricted in the first place
|
|
||||||
|
|
||||||
const rpi = restrictionPaths.values();
|
|
||||||
let result = true;
|
|
||||||
capturePaths.forEach((c) => {
|
|
||||||
while (true) { // (goto-target for "continue" below)
|
|
||||||
const e = rpi.next();
|
|
||||||
if (e.done) {
|
|
||||||
// there's at least one capturePaths entry (`c`) that does not appear in
|
|
||||||
// restrictionPaths, so we are restricted
|
|
||||||
result = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const r = e.value;
|
|
||||||
switch (pathCmp(c, r)) {
|
|
||||||
case -1:
|
|
||||||
// `c` is less than `r`, but restrictionPaths is sorted, so `c` does not appear in
|
|
||||||
// restrictionPaths, and we are thus restricted.
|
|
||||||
result = false;
|
|
||||||
return false;
|
|
||||||
case 0:
|
|
||||||
// `c` is equal to `r`, so we may yet be unrestricted. Discard both `c` and `r` and
|
|
||||||
// continue.
|
|
||||||
break;
|
|
||||||
case +1:
|
|
||||||
// `c` is greater than `r`, but capturePaths and restrictionPaths are sorted, so while
|
|
||||||
// we might yet come to an `r` that is equal to `c`, we will never find another `c`
|
|
||||||
// that is less than this `c`. Discard this `r` then, keeping the `c`, and compare
|
|
||||||
// against the next `r`.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Either we terminated early because we found some `c` not in restrictionPaths, or we went
|
|
||||||
// all the way through capturePaths without finding any such `c`, in which case `result`
|
|
||||||
// remains true and we don't need to bother looking at the rest of `rpi`.
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Index.prototype.addHandler = function(analysisResults, callback) {
|
|
||||||
let {skeleton, constPaths, constVals, capturePaths} = analysisResults;
|
|
||||||
let continuation = this.root.extend(skeleton);
|
|
||||||
let constValMap = continuation.leafMap.get(constPaths, false);
|
|
||||||
if (!constValMap) {
|
|
||||||
constValMap = Immutable.Map().withMutations((mutableConstValMap) => {
|
|
||||||
continuation.cachedAssertions.forEach((a) => {
|
|
||||||
const key = projectPaths(unscope(a), constPaths);
|
|
||||||
let leaf = mutableConstValMap.get(key, false);
|
|
||||||
if (!leaf) {
|
|
||||||
leaf = new Leaf();
|
|
||||||
mutableConstValMap.set(key, leaf);
|
|
||||||
}
|
|
||||||
leaf.cachedAssertions = leaf.cachedAssertions.add(a);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
continuation.leafMap = continuation.leafMap.set(constPaths, constValMap);
|
|
||||||
}
|
|
||||||
let leaf = constValMap.get(constVals, false);
|
|
||||||
if (!leaf) {
|
|
||||||
leaf = new Leaf();
|
|
||||||
constValMap = constValMap.set(constVals, leaf);
|
|
||||||
continuation.leafMap = continuation.leafMap.set(constPaths, constValMap);
|
|
||||||
}
|
|
||||||
let handler = leaf.handlerMap.get(capturePaths, false);
|
|
||||||
if (!handler) {
|
|
||||||
let cachedCaptures = Bag.Bag().withMutations((mutable) => {
|
|
||||||
leaf.cachedAssertions.forEach((a) => {
|
|
||||||
return unpackScoped(a, (restrictionPaths, term) => {
|
|
||||||
if (isUnrestricted(capturePaths, restrictionPaths)) {
|
|
||||||
let captures = projectPaths(term, capturePaths);
|
|
||||||
mutable.set(captures, mutable.get(captures, 0) + 1);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
|
||||||
handler = new Handler(cachedCaptures);
|
|
||||||
leaf.handlerMap = leaf.handlerMap.set(capturePaths, handler);
|
|
||||||
}
|
|
||||||
handler.callbacks = handler.callbacks.add(callback);
|
|
||||||
handler.cachedCaptures.forEach((_count, captures) => {
|
|
||||||
callback(EVENT_ADDED, captures);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Index.prototype.removeHandler = function(analysisResults, callback) {
|
|
||||||
let {skeleton, constPaths, constVals, capturePaths} = analysisResults;
|
|
||||||
let continuation = this.root.extend(skeleton);
|
|
||||||
let constValMap = continuation.leafMap.get(constPaths, false);
|
|
||||||
if (!constValMap) return;
|
|
||||||
let leaf = constValMap.get(constVals, false);
|
|
||||||
if (!leaf) return;
|
|
||||||
let handler = leaf.handlerMap.get(capturePaths, false);
|
|
||||||
if (!handler) return;
|
|
||||||
handler.callbacks = handler.callbacks.remove(callback);
|
|
||||||
if (handler.callbacks.isEmpty()) {
|
|
||||||
leaf.handlerMap = leaf.handlerMap.remove(capturePaths);
|
|
||||||
}
|
|
||||||
if (leaf.isEmpty()) {
|
|
||||||
constValMap = constValMap.remove(constVals);
|
|
||||||
}
|
|
||||||
if (constValMap.isEmpty()) {
|
|
||||||
continuation.leafMap = continuation.leafMap.remove(constPaths);
|
|
||||||
} else {
|
|
||||||
continuation.leafMap = continuation.leafMap.set(constPaths, constValMap);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Node.prototype.modify = function(operation, outerValue, m_cont, m_leaf, m_handler) {
|
|
||||||
const [restrictionPaths, outerValueTerm] = unpackScoped(outerValue, (p,t) => [p,t]);
|
|
||||||
|
|
||||||
function walkNode(node, termStack) {
|
|
||||||
walkContinuation(node.continuation);
|
|
||||||
node.edges.forEach((table, selector) => {
|
|
||||||
let nextStack = termStack.withMutations((mutable) => {
|
|
||||||
let i = selector.popCount;
|
|
||||||
while (i--) { mutable.pop(); }
|
|
||||||
});
|
|
||||||
let nextValue = step(nextStack.first(), selector.index);
|
|
||||||
let nextNode = table.get(classOf(nextValue), false);
|
|
||||||
if (nextNode) {
|
|
||||||
walkNode(nextNode, nextStack.push(nextValue));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function walkContinuation(continuation) {
|
|
||||||
m_cont(continuation, outerValue);
|
|
||||||
continuation.leafMap.forEach((constValMap, constPaths) => {
|
|
||||||
let constVals = projectPaths(outerValueTerm, constPaths);
|
|
||||||
let leaf = constValMap.get(constVals, false);
|
|
||||||
if (!leaf && operation === EVENT_ADDED) {
|
|
||||||
leaf = new Leaf();
|
|
||||||
constValMap = constValMap.set(constVals, leaf);
|
|
||||||
continuation.leafMap = continuation.leafMap.set(constPaths, constValMap);
|
|
||||||
}
|
|
||||||
if (leaf) {
|
|
||||||
m_leaf(leaf, outerValue);
|
|
||||||
leaf.handlerMap.forEach((handler, capturePaths) => {
|
|
||||||
if (isUnrestricted(capturePaths, restrictionPaths)) {
|
|
||||||
m_handler(handler, projectPaths(outerValueTerm, capturePaths));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
if (operation === EVENT_REMOVED && leaf.isEmpty()) {
|
|
||||||
constValMap = constValMap.remove(constVals);
|
|
||||||
if (constValMap.isEmpty()) {
|
|
||||||
continuation.leafMap = continuation.leafMap.remove(constPaths);
|
|
||||||
} else {
|
|
||||||
continuation.leafMap = continuation.leafMap.set(constPaths, constValMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
walkNode(this, Immutable.Stack().push(Immutable.List([outerValueTerm])));
|
|
||||||
};
|
|
||||||
|
|
||||||
function add_to_cont(c, v) { c.cachedAssertions = c.cachedAssertions.add(v); }
|
|
||||||
function add_to_leaf(l, v) { l.cachedAssertions = l.cachedAssertions.add(v); }
|
|
||||||
function add_to_handler(h, vs) {
|
|
||||||
let net;
|
|
||||||
({bag: h.cachedCaptures, net: net} = Bag.change(h.cachedCaptures, vs, +1));
|
|
||||||
if (net === Bag.ABSENT_TO_PRESENT) {
|
|
||||||
h.callbacks.forEach((cb) => {
|
|
||||||
cb(EVENT_ADDED, vs);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function del_from_cont(c, v) { c.cachedAssertions = c.cachedAssertions.remove(v); }
|
|
||||||
function del_from_leaf(l, v) { l.cachedAssertions = l.cachedAssertions.remove(v); }
|
|
||||||
function del_from_handler(h, vs) {
|
|
||||||
let net;
|
|
||||||
({bag: h.cachedCaptures, net: net} = Bag.change(h.cachedCaptures, vs, -1));
|
|
||||||
if (net === Bag.PRESENT_TO_ABSENT) {
|
|
||||||
h.callbacks.forEach((cb) => {
|
|
||||||
cb(EVENT_REMOVED, vs);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Index.prototype.adjustAssertion = function(outerValue, delta) {
|
|
||||||
let net;
|
|
||||||
({bag: this.allAssertions, net: net} = Bag.change(this.allAssertions, outerValue, delta));
|
|
||||||
switch (net) {
|
|
||||||
case Bag.ABSENT_TO_PRESENT:
|
|
||||||
this.root.modify(EVENT_ADDED, outerValue, add_to_cont, add_to_leaf, add_to_handler);
|
|
||||||
break;
|
|
||||||
case Bag.PRESENT_TO_ABSENT:
|
|
||||||
this.root.modify(EVENT_REMOVED, outerValue, del_from_cont, del_from_leaf, del_from_handler);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return net;
|
|
||||||
};
|
|
||||||
|
|
||||||
Index.prototype.addAssertion = function(v) { this.adjustAssertion(v, +1); };
|
|
||||||
Index.prototype.removeAssertion = function (v) { this.adjustAssertion(v, -1); };
|
|
||||||
|
|
||||||
const _nop = () => {};
|
|
||||||
Index.prototype.sendMessage = function(v, leafCallback) {
|
|
||||||
this.root.modify(EVENT_MESSAGE, v, _nop, leafCallback || _nop, (h, vs) => {
|
|
||||||
h.callbacks.forEach((cb) => {
|
|
||||||
cb(EVENT_MESSAGE, vs);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Node.prototype._debugString = function (outerIndent) {
|
|
||||||
const pieces = [];
|
|
||||||
const inspect = require('util').inspect;
|
|
||||||
function line(indent, content) {
|
|
||||||
pieces.push(indent);
|
|
||||||
pieces.push(content);
|
|
||||||
}
|
|
||||||
function walkNode(indent, n) {
|
|
||||||
line(indent, ' Continuation:');
|
|
||||||
walkContinuation(indent+' ', n.continuation);
|
|
||||||
if (!n.edges.isEmpty()) line(indent, ' Edges:');
|
|
||||||
n.edges.forEach((table, selector) => {
|
|
||||||
line(indent+' ', `pop ${selector.popCount} index ${selector.index}`);
|
|
||||||
table.forEach((nextNode, cls) => {
|
|
||||||
line(indent+' ', inspect(cls));
|
|
||||||
walkNode(indent+' ', nextNode);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function walkCache(indent, cache) {
|
|
||||||
if (!cache.isEmpty()) line(indent, 'Cache:')
|
|
||||||
cache.forEach((v,k) => {
|
|
||||||
line(indent+' ', (k ? k.toString() + ': ' : '') + v && v.toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function walkContinuation(indent, c) {
|
|
||||||
walkCache(indent, c.cachedAssertions);
|
|
||||||
c.leafMap.forEach((constValMap, constPaths) => {
|
|
||||||
line(indent, constPaths.toString() + ' =?= ...');
|
|
||||||
constValMap.forEach((leaf, constVals) => {
|
|
||||||
line(indent+' ', constVals.toString());
|
|
||||||
walkLeaf(indent+' ', leaf);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function walkLeaf(indent, l) {
|
|
||||||
walkCache(indent, l.cachedAssertions);
|
|
||||||
l.handlerMap.forEach((handler, capturePaths) => {
|
|
||||||
line(indent, capturePaths.toString() + ' ==> ...');
|
|
||||||
walkHandler(indent+' ', handler);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function walkHandler(indent, h) {
|
|
||||||
walkCache(indent, h.cachedCaptures);
|
|
||||||
line(indent, '' + h.callbacks.size + ' callback(s)');
|
|
||||||
}
|
|
||||||
line(outerIndent || '', 'INDEX ROOT');
|
|
||||||
walkNode(outerIndent || '\n', this);
|
|
||||||
return pieces.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function analyzeAssertion(a) {
|
|
||||||
let constPaths = Immutable.List();
|
|
||||||
let constVals = Immutable.List();
|
|
||||||
let capturePaths = Immutable.List();
|
|
||||||
|
|
||||||
function walk(path, a) {
|
|
||||||
if (Capture.isClassOf(a)) {
|
|
||||||
// NB. isUnrestricted relies on the specific order that
|
|
||||||
// capturePaths is computed here.
|
|
||||||
capturePaths = capturePaths.push(path);
|
|
||||||
return walk(path, a.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Discard.isClassOf(a)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cls = classOf(a);
|
|
||||||
if (cls !== null) {
|
|
||||||
let arity = (typeof cls === 'number') ? cls : cls.arity;
|
|
||||||
let result = [cls];
|
|
||||||
for (let i = 0; i < arity; i++) {
|
|
||||||
result.push(walk(path.push(i), step(a, i)));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
constPaths = constPaths.push(path);
|
|
||||||
constVals = constVals.push(a);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let skeleton = walk(Immutable.List(), a);
|
|
||||||
|
|
||||||
return { skeleton, constPaths, constVals, capturePaths, assertion: Observe(a) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function OpaquePlaceholder() {}
|
|
||||||
|
|
||||||
function instantiateAssertion(a, vs) {
|
|
||||||
let capturePaths = Immutable.List();
|
|
||||||
let remaining = vs;
|
|
||||||
|
|
||||||
function walk(path, a) {
|
|
||||||
if (Capture.isClassOf(a)) {
|
|
||||||
capturePaths = capturePaths.push(path);
|
|
||||||
const v = remaining.first();
|
|
||||||
remaining = remaining.shift();
|
|
||||||
walk(path, a.get(0));
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Discard.isClassOf(a)) {
|
|
||||||
return new OpaquePlaceholder();
|
|
||||||
// ^ Doesn't match ANYTHING ELSE, even other `OpaquePlaceholder`
|
|
||||||
// instances. This prevents unwanted matching against
|
|
||||||
// "don't-care" positions when `VisibilityRestriction`s are in
|
|
||||||
// play.
|
|
||||||
}
|
|
||||||
|
|
||||||
let cls = classOf(a);
|
|
||||||
if (cls !== null) {
|
|
||||||
if (typeof cls === 'number') {
|
|
||||||
return a.map((v, i) => walk(path.push(i), v));
|
|
||||||
} else {
|
|
||||||
return new Record(a.label, a.fields.map((v, i) => walk(path.push(i), v)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instantiated = walk(Immutable.List(), a);
|
|
||||||
// ^ Compute `instantiated` completely before retrieving the imperatively-updated `capturePaths`.
|
|
||||||
return new VisibilityRestriction(capturePaths, instantiated);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VisibilityRestriction(paths, term) {
|
|
||||||
this.paths = paths;
|
|
||||||
this.term = term;
|
|
||||||
}
|
|
||||||
|
|
||||||
VisibilityRestriction.prototype.toString = function () {
|
|
||||||
return "VisibilityRestriction(" + this.paths.toString() + "," + this.term.toString() + ")";
|
|
||||||
};
|
|
||||||
|
|
||||||
function unscope(a) {
|
|
||||||
return (a instanceof VisibilityRestriction) ? a.term : a;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unpackScoped(a, k) {
|
|
||||||
return (a instanceof VisibilityRestriction) ? k(a.paths, a.term) : k(false, a);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function match(p, v) {
|
|
||||||
let captures = Immutable.List();
|
|
||||||
|
|
||||||
function walk(p, v) {
|
|
||||||
if (Capture.isClassOf(p)) {
|
|
||||||
if (!walk(p.get(0), v)) return false;
|
|
||||||
captures = captures.push(v);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Discard.isClassOf(p)) return true;
|
|
||||||
|
|
||||||
const pcls = classOf(p);
|
|
||||||
const vcls = classOf(v);
|
|
||||||
if (!Immutable.is(pcls, vcls)) return false;
|
|
||||||
|
|
||||||
if (pcls === null) return Immutable.is(p, v);
|
|
||||||
if (typeof pcls === 'number') return p.every((pv, i) => walk(pv, v.get(i)));
|
|
||||||
return p.fields.every((pv, i) => walk(pv, v.fields.get(i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return walk(p, v) ? captures : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCompletelyConcrete(p) {
|
|
||||||
function walk(p) {
|
|
||||||
if (Capture.isClassOf(p)) return false;
|
|
||||||
if (Discard.isClassOf(p)) return false;
|
|
||||||
|
|
||||||
const cls = classOf(p);
|
|
||||||
if (cls === null) return true;
|
|
||||||
if (typeof cls === 'number') return p.every(walk);
|
|
||||||
return p.fields.every(walk);
|
|
||||||
}
|
|
||||||
return walk(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
function withoutCaptures(p) {
|
|
||||||
function walk(p) {
|
|
||||||
if (Capture.isClassOf(p)) return walk(p.get(0));
|
|
||||||
if (Discard.isClassOf(p)) return p;
|
|
||||||
|
|
||||||
const cls = classOf(p);
|
|
||||||
if (cls === null) return p;
|
|
||||||
if (typeof cls === 'number') return p.map(walk);
|
|
||||||
return new Record(p.label, p.fields.map(walk));
|
|
||||||
}
|
|
||||||
return walk(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
module.exports.EVENT_ADDED = EVENT_ADDED;
|
|
||||||
module.exports.EVENT_REMOVED = EVENT_REMOVED;
|
|
||||||
module.exports.EVENT_MESSAGE = EVENT_MESSAGE;
|
|
||||||
module.exports.Index = Index;
|
|
||||||
|
|
||||||
module.exports.analyzeAssertion = analyzeAssertion;
|
|
||||||
module.exports.instantiateAssertion = instantiateAssertion;
|
|
||||||
module.exports.match = match;
|
|
||||||
module.exports.isCompletelyConcrete = isCompletelyConcrete;
|
|
||||||
module.exports.withoutCaptures = withoutCaptures;
|
|
||||||
|
|
||||||
module.exports.__for_testing = {
|
|
||||||
pathCmp,
|
|
||||||
isUnrestricted,
|
|
||||||
};
|
|
|
@ -1,11 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
inspect: (item) => {
|
|
||||||
try {
|
|
||||||
return JSON.stringify(item);
|
|
||||||
} catch (_e) {
|
|
||||||
return '<uninspectable_value>';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.inspect.custom = Symbol('util_stub.inspect.custom');
|
|
|
@ -1,270 +0,0 @@
|
||||||
"use strict";
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS.
|
|
||||||
// Copyright (C) 2016-2018 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/>.
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if (require('preserves/src/singletonmodule.js')('syndicate-lang.org/syndicate-js',
|
|
||||||
require('../package.json').version,
|
|
||||||
'worker.js',
|
|
||||||
module)) return;
|
|
||||||
|
|
||||||
const { Dataspace } = require('./dataspace.js');
|
|
||||||
const { Observe, Outbound, Inbound, Capture, Discard } = require('./assertions.js');
|
|
||||||
const { $QuitDataspace } = require('./relay.js');
|
|
||||||
const Skeleton = require('./skeleton.js');
|
|
||||||
const RandomID = require('./randomid.js');
|
|
||||||
|
|
||||||
const { List, Map } = require('immutable');
|
|
||||||
const { Bytes, Record, Encoder, Decoder } = require("preserves");
|
|
||||||
|
|
||||||
const WorkerEvent = Record.makeConstructor('--worker-event', ['epId', 'event', 'vs']);
|
|
||||||
|
|
||||||
const worker_threads = (function () {
|
|
||||||
try {
|
|
||||||
return require('worker_threads');
|
|
||||||
} catch (_e) {
|
|
||||||
return require('./worker_stub.js');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function encodePacket(p) {
|
|
||||||
return Bytes.toIO(new Encoder().push(p).contents());
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodePacket(m) {
|
|
||||||
return new Decoder(m).next();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendPacket(ch, p) {
|
|
||||||
ch.postMessage(encodePacket(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnWorker(workerSourceFilename, workerData) {
|
|
||||||
if (worker_threads.__isDummyStub) {
|
|
||||||
throw new Error("Cannot spawnWorker without --experimental-worker flag on node.js command line");
|
|
||||||
}
|
|
||||||
Dataspace.spawn([workerSourceFilename, workerData], function () {
|
|
||||||
const outerFacet = Dataspace.currentFacet();
|
|
||||||
outerFacet.addDataflow(function () {});
|
|
||||||
// ^ eww! Dummy endpoint to keep the root facet of the relay alive.
|
|
||||||
|
|
||||||
let endpoints = Map();
|
|
||||||
|
|
||||||
const w = new worker_threads.Worker(workerSourceFilename, {
|
|
||||||
workerData: encodePacket(workerData || false)
|
|
||||||
});
|
|
||||||
w.on('error', Dataspace.wrapExternal((err) => {
|
|
||||||
throw err;
|
|
||||||
}));
|
|
||||||
w.on('exit', Dataspace.wrapExternal(() => {
|
|
||||||
outerFacet.stop();
|
|
||||||
}));
|
|
||||||
w.on('message', Dataspace.wrapExternal((m) => {
|
|
||||||
m = decodePacket(m);
|
|
||||||
switch (m.get(0)) {
|
|
||||||
case 'assert': {
|
|
||||||
const ep = m.get(1);
|
|
||||||
const a = m.get(2);
|
|
||||||
if (!endpoints.includes(ep)) {
|
|
||||||
outerFacet.actor.addFacet(outerFacet, function () {
|
|
||||||
const epFacet = Dataspace.currentFacet();
|
|
||||||
endpoints = endpoints.set(ep, epFacet);
|
|
||||||
epFacet.addStopScript(() => { endpoints = endpoints.remove(ep); });
|
|
||||||
Dataspace.declareField(this, 'assertion', a);
|
|
||||||
epFacet.addEndpoint(() => {
|
|
||||||
if (Observe.isClassOf(this.assertion)) {
|
|
||||||
const spec = this.assertion.get(0);
|
|
||||||
const analysis = Skeleton.analyzeAssertion(spec);
|
|
||||||
analysis.callback = Dataspace.wrap((evt, vs) => {
|
|
||||||
epFacet.actor.scheduleScript(() => {
|
|
||||||
sendPacket(w, ['event', ep, evt, vs]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return [Observe(spec), analysis];
|
|
||||||
} else {
|
|
||||||
return [this.assertion, null];
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}, true);
|
|
||||||
} else {
|
|
||||||
endpoints.get(ep).fields.assertion = a;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'clear': {
|
|
||||||
const ep = m.get(1);
|
|
||||||
const epFacet = endpoints.get(ep, false);
|
|
||||||
if (epFacet) {
|
|
||||||
epFacet.stop(() => {
|
|
||||||
endpoints = endpoints.remove(ep);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'message': {
|
|
||||||
const body = m.get(1);
|
|
||||||
Dataspace.send(body);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
const err = new Error("Invalid Worker protocol message from Worker");
|
|
||||||
err.irritant = m;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnWorkerRelay() {
|
|
||||||
if (worker_threads.__isDummyStub) return;
|
|
||||||
if (worker_threads.isMainThread) return;
|
|
||||||
|
|
||||||
worker_threads.workerData = decodePacket(worker_threads.workerData);
|
|
||||||
|
|
||||||
Dataspace.currentFacet().actor.dataspace.addStopHandler(() => {
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
Dataspace.currentFacet().addStartScript(function () {
|
|
||||||
Dataspace.spawn('WorkerRelay', function () {
|
|
||||||
const outerFacet = Dataspace.currentFacet();
|
|
||||||
|
|
||||||
const finish = Dataspace.backgroundTask();
|
|
||||||
outerFacet.addStopScript(finish);
|
|
||||||
|
|
||||||
let outboundEndpoints = Map();
|
|
||||||
let inboundEndpoints = Map();
|
|
||||||
let nextId = 0;
|
|
||||||
|
|
||||||
const parentPort = worker_threads.parentPort;
|
|
||||||
|
|
||||||
function sendToParent(m) {
|
|
||||||
sendPacket(parentPort, m);
|
|
||||||
}
|
|
||||||
|
|
||||||
parentPort.on('message', Dataspace.wrapExternal(function (m) {
|
|
||||||
m = decodePacket(m);
|
|
||||||
if (List.isList(m) && m.get(0) === 'event') {
|
|
||||||
const epId = m.get(1);
|
|
||||||
const evt = m.get(2);
|
|
||||||
const vs = m.get(3);
|
|
||||||
Dataspace.send(WorkerEvent(epId, evt, vs));
|
|
||||||
} else {
|
|
||||||
const err = new Error("Invalid Worker protocol message from parent");
|
|
||||||
err.irritant = m;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
outerFacet.addEndpoint(function () {
|
|
||||||
const analysis = Skeleton.analyzeAssertion(Outbound(Capture(Discard._instance)));
|
|
||||||
analysis.callback = Dataspace.wrap(function (evt, vs) {
|
|
||||||
outerFacet.actor.scheduleScript(function () {
|
|
||||||
switch (evt) {
|
|
||||||
case Skeleton.EVENT_ADDED: {
|
|
||||||
const epId = nextId++;
|
|
||||||
outboundEndpoints = outboundEndpoints.set(vs.get(0), epId);
|
|
||||||
sendToParent(['assert', epId, vs.get(0)]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Skeleton.EVENT_REMOVED: {
|
|
||||||
const epId = outboundEndpoints.get(vs.get(0));
|
|
||||||
outboundEndpoints = outboundEndpoints.remove(vs.get(0));
|
|
||||||
sendToParent(['clear', epId]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Skeleton.EVENT_MESSAGE: {
|
|
||||||
sendToParent(['message', vs.get(0)]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return [analysis.assertion, analysis];
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
outerFacet.addEndpoint(function () {
|
|
||||||
const analysis = Skeleton.analyzeAssertion(Observe(Inbound(Capture(Discard._instance))));
|
|
||||||
analysis.callback = Dataspace.wrap(function (evt, vs) {
|
|
||||||
outerFacet.actor.scheduleScript(function () {
|
|
||||||
const spec = vs.get(0);
|
|
||||||
switch (evt) {
|
|
||||||
case Skeleton.EVENT_ADDED: {
|
|
||||||
const epId = nextId++;
|
|
||||||
outerFacet.actor.addFacet(outerFacet, function () {
|
|
||||||
const innerFacet = Dataspace.currentFacet();
|
|
||||||
inboundEndpoints = inboundEndpoints.set(spec, { epId, facet: innerFacet });
|
|
||||||
innerFacet.addEndpoint(function () {
|
|
||||||
const analysis = Skeleton.analyzeAssertion(
|
|
||||||
WorkerEvent(epId, Capture(Discard._instance), Capture(Discard._instance)));
|
|
||||||
analysis.callback = Dataspace.wrap(function (evt, vs) {
|
|
||||||
if (evt === Skeleton.EVENT_MESSAGE) {
|
|
||||||
evt = vs.get(0);
|
|
||||||
vs = vs.get(1);
|
|
||||||
const a = Skeleton.instantiateAssertion(Inbound(spec), vs);
|
|
||||||
innerFacet.actor.scheduleScript(function () {
|
|
||||||
switch (evt) {
|
|
||||||
case Skeleton.EVENT_ADDED:
|
|
||||||
innerFacet.actor.addFacet(innerFacet, function () {
|
|
||||||
const assertionFacet = Dataspace.currentFacet();
|
|
||||||
assertionFacet.addEndpoint(function () {
|
|
||||||
return [a, null];
|
|
||||||
}, false);
|
|
||||||
assertionFacet.addEndpoint(function () {
|
|
||||||
const analysis = Skeleton.analyzeAssertion(
|
|
||||||
WorkerEvent(epId, Skeleton.EVENT_REMOVED, vs));
|
|
||||||
analysis.callback = Dataspace.wrap(function (evt, vs) {
|
|
||||||
assertionFacet.actor.scheduleScript(function () {
|
|
||||||
assertionFacet.stop();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return [analysis.assertion, analysis];
|
|
||||||
}, false);
|
|
||||||
}, true);
|
|
||||||
break;
|
|
||||||
case Skeleton.EVENT_MESSAGE:
|
|
||||||
Dataspace.send(a);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [analysis.assertion, analysis];
|
|
||||||
}, false);
|
|
||||||
}, true);
|
|
||||||
sendToParent(['assert', epId, Observe(spec)]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Skeleton.EVENT_REMOVED: {
|
|
||||||
const { epId, facet } = inboundEndpoints.get(spec);
|
|
||||||
inboundEndpoints = inboundEndpoints.remove(spec);
|
|
||||||
facet.stop();
|
|
||||||
sendToParent(['clear', epId]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return [analysis.assertion, analysis];
|
|
||||||
}, false);
|
|
||||||
}, null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.spawnWorker = spawnWorker;
|
|
||||||
module.exports.spawnWorkerRelay = spawnWorkerRelay;
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
__isDummyStub: true,
|
|
||||||
};
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const randomBytes = void 0;
|
|
@ -0,0 +1,41 @@
|
||||||
|
import pkg from './package.json';
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
|
||||||
|
function distfile(insertion) {
|
||||||
|
const f = `syndicate-${pkg.version}${insertion}.js`;
|
||||||
|
return `dist/${f}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function umd(insertion, extra) {
|
||||||
|
return {
|
||||||
|
file: distfile(insertion),
|
||||||
|
format: 'umd',
|
||||||
|
name: 'Syndicate',
|
||||||
|
... (extra || {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function es6(insertion, extra) {
|
||||||
|
return {
|
||||||
|
file: distfile('.es6' + insertion),
|
||||||
|
format: 'es',
|
||||||
|
... (extra || {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'lib/index.js',
|
||||||
|
plugins: [
|
||||||
|
resolve({
|
||||||
|
moduleDirectories: ['stubs', 'node_modules'],
|
||||||
|
preferBuiltins: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
output: [
|
||||||
|
umd(''),
|
||||||
|
umd('.min', { plugins: [terser()] }),
|
||||||
|
es6(''),
|
||||||
|
es6('.min', { plugins: [terser()] }),
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["ES2017", "DOM"],
|
||||||
|
"declaration": true,
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"declarationDir": "./lib",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "es6",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
cd "$(dirname "$1")"
|
|
||||||
case "$1" in
|
|
||||||
*/all)
|
|
||||||
for d in src/*.js; do [ -f "$d" ] && echo lib/$(basename "$d"); done | xargs redo-ifchange
|
|
||||||
for d in *.dist.json
|
|
||||||
do
|
|
||||||
[ -f "$d" ] && echo dist/$(basename "$d" .dist.json).js
|
|
||||||
done | xargs redo-ifchange
|
|
||||||
[ -f _all.do ] && redo-ifchange _all || true
|
|
||||||
;;
|
|
||||||
*/clean)
|
|
||||||
rm -rf lib
|
|
||||||
rm -rf dist
|
|
||||||
rm -rf .nyc_output
|
|
||||||
rm -rf coverage
|
|
||||||
;;
|
|
||||||
*/veryclean)
|
|
||||||
redo clean
|
|
||||||
rm -rf node_modules
|
|
||||||
rm -f package-lock.json
|
|
||||||
;;
|
|
||||||
esac
|
|
|
@ -1,52 +0,0 @@
|
||||||
# To be invoked with PACKAGENAME/lib/FOO.js or PACKAGENAME/dist/FOO.js
|
|
||||||
targettempfile="$(pwd)/$3"
|
|
||||||
mkdir -p "$(dirname "$1")"
|
|
||||||
cd "$(dirname "$1")"/..
|
|
||||||
case "$1" in
|
|
||||||
syntax/lib/babel_parser.js)
|
|
||||||
src=../../node_modules/@babel/parser/lib/index.js
|
|
||||||
[ -f "$src" ] || npm -i .
|
|
||||||
redo-ifchange "$src" babel_parser_suffix.js
|
|
||||||
cat "$src" babel_parser_suffix.js
|
|
||||||
;;
|
|
||||||
syntax/lib/*)
|
|
||||||
file=$(basename "$1")
|
|
||||||
redo-ifchange "src/$file"
|
|
||||||
../../node_modules/.bin/babel "src/$file"
|
|
||||||
;;
|
|
||||||
*/lib/*)
|
|
||||||
redo-ifchange ../syntax/all
|
|
||||||
file=$(basename "$1")
|
|
||||||
redo-ifchange "src/$file"
|
|
||||||
if [ -n "$SYNDICATE_COMPILE_SERVER" ]
|
|
||||||
then
|
|
||||||
if curl -fs --data-binary "@src/$file" ${SYNDICATE_COMPILE_SERVER}/"$file" \
|
|
||||||
> ${targettempfile} 2>/dev/null
|
|
||||||
then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
# I can't figure out a way to get curl to both exit on
|
|
||||||
# error, and print the response body on error. So
|
|
||||||
# instead we try once, discarding the output but
|
|
||||||
# keeping the exit status, and if it fails, we try
|
|
||||||
# again, keeping the output but discarding the exit
|
|
||||||
# status.
|
|
||||||
curl -s --data-binary "@src/$file" ${SYNDICATE_COMPILE_SERVER}/"$file" >&2
|
|
||||||
rm -f ${targettempfile}
|
|
||||||
false
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
../../syndicate-babel "src/$file"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*/dist/*)
|
|
||||||
configfile=$(basename "$1" .js).dist.json
|
|
||||||
redo-ifchange $configfile
|
|
||||||
redo-ifchange ../../rollup-redo
|
|
||||||
for maybedep in $(../../rollup-redo deps "$configfile" "$targettempfile")
|
|
||||||
do
|
|
||||||
[ -f "$maybedep" ] && echo "$maybedep"
|
|
||||||
done | xargs redo-ifchange
|
|
||||||
../../rollup-redo generate "$configfile" "$targettempfile"
|
|
||||||
;;
|
|
||||||
esac
|
|
106
rollup-redo
106
rollup-redo
|
@ -1,106 +0,0 @@
|
||||||
#!/usr/bin/env -S node -r esm
|
|
||||||
|
|
||||||
import * as rollup from 'rollup';
|
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
import json from '@rollup/plugin-json';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
function computeBasename(f) {
|
|
||||||
const m = /^(.*).dist.json$/.exec(f);
|
|
||||||
if (!m) throw new Error(`Config filename ${f} does not match pattern`);
|
|
||||||
return m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [_node, _rollup_redo, mode, configJsonFilename, targetFilename] = process.argv;
|
|
||||||
|
|
||||||
const basename = computeBasename(configJsonFilename);
|
|
||||||
const config = require(`${process.cwd()}/${configJsonFilename}`);
|
|
||||||
const inputFile = ('input' in config)
|
|
||||||
? path.resolve(process.cwd(), config.input)
|
|
||||||
: `${process.cwd()}/lib/${basename}.js`;
|
|
||||||
|
|
||||||
async function build() {
|
|
||||||
const bundle = await rollup.rollup({
|
|
||||||
input: inputFile,
|
|
||||||
external: ['crypto'],
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
resolveId(toResolve, referencingModule) {
|
|
||||||
{
|
|
||||||
const m = /^@syndicate-lang\/(.*)$/.exec(toResolve);
|
|
||||||
if (m) {
|
|
||||||
if (mode === 'deps') {
|
|
||||||
console.log(`../${m[1]}/all`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (/^\0?util(\?commonjs.*)?$/.test(toResolve)) {
|
|
||||||
return '../core/src/util_stub.js';
|
|
||||||
}
|
|
||||||
if (/^\0?crypto(\?commonjs.*)?$/.test(toResolve)) {
|
|
||||||
return '../core/src/crypto_stub.js';
|
|
||||||
}
|
|
||||||
if (/^\0?worker_threads(\?commonjs.*)?$/.test(toResolve)) {
|
|
||||||
return '../core/src/worker_stub.js';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolve({
|
|
||||||
browser: true,
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
json(),
|
|
||||||
],
|
|
||||||
onwarn(w, defaultHandler) {
|
|
||||||
if (((w.code === 'UNRESOLVED_IMPORT') ||
|
|
||||||
(w.code === 'MISSING_GLOBAL_NAME')) &&
|
|
||||||
(w.source.startsWith('@syndicate-lang/')))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
defaultHandler(w);
|
|
||||||
},
|
|
||||||
treeshake: {
|
|
||||||
moduleSideEffects: "no-external",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (mode) {
|
|
||||||
case 'deps': {
|
|
||||||
const deps = {};
|
|
||||||
for (const m of bundle.cache.modules) {
|
|
||||||
deps[m.id] = m.dependencies;
|
|
||||||
}
|
|
||||||
const seen = {};
|
|
||||||
function visit(id) {
|
|
||||||
if (id in seen) return;
|
|
||||||
if (!id.startsWith('/')) return;
|
|
||||||
seen[id] = true;
|
|
||||||
console.log(id);
|
|
||||||
for (const dep of (deps[id] || [])) {
|
|
||||||
visit(dep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visit(inputFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'generate': {
|
|
||||||
await bundle.write({
|
|
||||||
output: {
|
|
||||||
file: targetFilename,
|
|
||||||
format: config.format || 'es',
|
|
||||||
name: config.name || 'Syndicate_' + basename,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.error('Unknown mode', mode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
build().then(null, console.error);
|
|
|
@ -1 +0,0 @@
|
||||||
packages/syntax/bin/syndicate-babel.js
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue