();
120 | const c2 = new CellLoop();
121 |
122 | //As an attempted fix, put everything in lambdas
123 | c1.loop(
124 | s.snapshot3(c1, c2, lambda3((n1, n2, n3) => n1 * n2 * n3, [s, c1, c2])).hold(2)
125 | );
126 |
127 | c2.loop(
128 | s.snapshot3(c1, c2, lambda3((n1, n2, n3) => n1 * n2 * n3, [s, c1, c2])).hold(2)
129 | );
130 |
131 | return c1;
132 | });
133 |
134 |
135 | //need to delay the handler so it can call kill
136 | const kill = cResult.listen(n => setTimeout(() => onValue(n), 0))
137 |
138 | //Uncomment this dummy listener for a "fix" (remember to kill it below too)
139 | //const killDummy = s.listen(() => {});
140 |
141 | //As an attempted fix - put it in another transaction, and delay that
142 | setTimeout(
143 | () => Transaction.run(() => {
144 | s.send(4);
145 | }),
146 | 0
147 | );
148 |
149 |
150 | //now we're getting the initial cell value too so we need to collect it
151 | const out = [];
152 | const onValue = n => {
153 | out.push(n);
154 | if(out.length == 2) {
155 | expect([2, 16]).toEqual(out);
156 | kill();
157 | //killDummy();
158 | done();
159 | }
160 | }
161 | });
162 |
163 |
--------------------------------------------------------------------------------
/src/lib/sodium/Operational.ts:
--------------------------------------------------------------------------------
1 | import { Stream, StreamWithSend } from "./Stream";
2 | import { Cell } from "./Cell";
3 | import { Transaction } from "./Transaction";
4 | import { Unit } from "./Unit";
5 | import { Source, Vertex } from "./Vertex";
6 |
7 | export class Operational {
8 | /**
9 | * A stream that gives the updates/steps for a {@link Cell}.
10 | *
11 | * This is an OPERATIONAL primitive, which is not part of the main Sodium
12 | * API. It breaks the property of non-detectability of cell steps/updates.
13 | * The rule with this primitive is that you should only use it in functions
14 | * that do not allow the caller to detect the cell updates.
15 | */
16 | static updates(c : Cell) : Stream {
17 | /* Don't think this is needed
18 | const out = new StreamWithSend(null);
19 | out.setVertex__(new Vertex("updates", 0, [
20 | new Source(
21 | c.getStream__().getVertex__(),
22 | () => {
23 | return c.getStream__().listen_(out.getVertex__(), (a : A) => {
24 | out.send_(a);
25 | }, false);
26 | }
27 | ),
28 | new Source(
29 | c.getVertex__(),
30 | () => {
31 | return () => { };
32 | }
33 | )
34 | ]
35 | ));
36 | return out;
37 | */
38 | return c.getStream__();
39 | }
40 |
41 | /**
42 | * A stream that is guaranteed to fire once in the transaction where value() is invoked, giving
43 | * the current value of the cell, and thereafter behaves like {@link updates(Cell)},
44 | * firing for each update/step of the cell's value.
45 | *
46 | * This is an OPERATIONAL primitive, which is not part of the main Sodium
47 | * API. It breaks the property of non-detectability of cell steps/updates.
48 | * The rule with this primitive is that you should only use it in functions
49 | * that do not allow the caller to detect the cell updates.
50 | */
51 | static value(c : Cell) : Stream {
52 | return Transaction.run(() => {
53 | const sSpark = new StreamWithSend();
54 | Transaction.currentTransaction.prioritized(sSpark.getVertex__(), () => {
55 | sSpark.send_(Unit.UNIT);
56 | });
57 | const sInitial = sSpark.snapshot1(c);
58 | return Operational.updates(c).orElse(sInitial);
59 | });
60 | }
61 |
62 | /**
63 | * Push each event onto a new transaction guaranteed to come before the next externally
64 | * initiated transaction. Same as {@link split(Stream)} but it works on a single value.
65 | */
66 | static defer(s : Stream) : Stream {
67 | return Operational.split(s.map((a : A) => {
68 | return [a];
69 | }));
70 | }
71 |
72 | /**
73 | * Push each event in the list onto a newly created transaction guaranteed
74 | * to come before the next externally initiated transaction. Note that the semantics
75 | * are such that two different invocations of split() can put events into the same
76 | * new transaction, so the resulting stream's events could be simultaneous with
77 | * events output by split() or {@link defer(Stream)} invoked elsewhere in the code.
78 | */
79 | static split(s : Stream>) : Stream {
80 | const out = new StreamWithSend(null);
81 | out.setVertex__(new Vertex("split", 0, [
82 | new Source(
83 | s.getVertex__(),
84 | () => {
85 | out.getVertex__().childrn.push(s.getVertex__());
86 | let cleanups: (()=>void)[] = [];
87 | cleanups.push(
88 | s.listen_(Vertex.NULL, (as : Array) => {
89 | for (let i = 0; i < as.length; i++) {
90 | Transaction.currentTransaction.post(i, () => {
91 | Transaction.run(() => {
92 | out.send_(as[i]);
93 | });
94 | });
95 | }
96 | }, false)
97 | );
98 | cleanups.push(() => {
99 | let chs = out.getVertex__().childrn;
100 | for (let i = chs.length-1; i >= 0; --i) {
101 | if (chs[i] == s.getVertex__()) {
102 | chs.splice(i, 1);
103 | break;
104 | }
105 | }
106 | });
107 | return () => {
108 | cleanups.forEach(cleanup => cleanup());
109 | cleanups.splice(0, cleanups.length);
110 | }
111 | }
112 | )
113 | ]
114 | ));
115 | return out;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/lib/sodium/IntrusiveIndexedPriorityQueue.ts:
--------------------------------------------------------------------------------
1 | import { Vertex } from "./Vertex";
2 |
3 | const SANITY_CHECKS = false;
4 |
5 | export class IntrusiveIndexedPriorityQueue<
6 | A extends {
7 | pqRank: number,
8 | inPq: boolean,
9 | pqPrev: A | null,
10 | pqNext: A | null,
11 | }
12 | > {
13 | private entries: {
14 | head: A | null,
15 | tail: A | null,
16 | }[] = new Array(1000).fill(undefined).map((_) => ({ head: null, tail: null, }));
17 | private last: {
18 | head: A | null,
19 | tail: A | null,
20 | } = {
21 | head: null,
22 | tail: null,
23 | };
24 | private minRank: number = 0;
25 | maxRank: number = -1;
26 |
27 | private checkQueue() {
28 | for (let i = 0; i < this.entries.length; ++i) {
29 | let expectedPqRank = i;
30 | let e = this.entries[i];
31 | if (e == undefined) {
32 | continue;
33 | }
34 | if (e.head != null && e.head.pqPrev != null) {
35 | throw new Error("PQ");
36 | }
37 | if (e.tail != null && e.tail.pqNext != null) {
38 | throw new Error("PQ");
39 | }
40 | let at = e.head;
41 | while (at != null) {
42 | if (at.pqRank !== expectedPqRank) {
43 | throw new Error("PQ");
44 | }
45 | if (!at.inPq) {
46 | throw new Error("PQ");
47 | }
48 | at = at.pqNext;
49 | }
50 | }
51 | }
52 |
53 | isEmpty(): boolean {
54 | if (this.minRank <= this.maxRank) {
55 | return false;
56 | }
57 | this.minRank = 0;
58 | this.maxRank = -1;
59 | return this.last.head == null;
60 | }
61 |
62 | enqueue(a: A) {
63 | if (a.pqRank != (a as any).rank.rank) {
64 | throw new Error("PQ");
65 | }
66 | if (a.inPq) {
67 | return;
68 | }
69 | a.inPq = true;
70 | let entry: {
71 | head: A | null,
72 | tail: A | null,
73 | };
74 | if (a.pqRank == Vertex.NULL.rank) {
75 | entry = this.last;
76 | } else {
77 | if (a.pqRank < this.minRank) {
78 | this.minRank = a.pqRank;
79 | }
80 | if (a.pqRank > this.maxRank) {
81 | this.maxRank = a.pqRank;
82 | }
83 | entry = this.entries[a.pqRank];
84 | if (entry == undefined) {
85 | this.entries[a.pqRank] = {
86 | head: a,
87 | tail: a,
88 | };
89 | if (SANITY_CHECKS) {
90 | this.checkQueue();
91 | }
92 | return;
93 | }
94 | }
95 | if (entry.head == null) {
96 | entry.head = entry.tail = a;
97 | if (SANITY_CHECKS) {
98 | this.checkQueue();
99 | }
100 | return;
101 | }
102 | entry.tail!.pqNext = a;
103 | a.pqPrev = entry.tail!;
104 | entry.tail = a;
105 | if (SANITY_CHECKS) {
106 | this.checkQueue();
107 | }
108 | }
109 |
110 | dequeue(): A | undefined {
111 | while (true) {
112 | if (this.minRank > this.maxRank) {
113 | this.minRank = 0;
114 | this.maxRank = -1;
115 | if (this.last.head != null) {
116 | let result = this.last.head;
117 | this.remove(result);
118 | return result;
119 | }
120 | if (SANITY_CHECKS) {
121 | this.checkQueue();
122 | }
123 | return undefined;
124 | }
125 | let entry = this.entries[this.minRank];
126 | if (entry == undefined || entry.head == null) {
127 | this.minRank++;
128 | continue;
129 | }
130 | let result = entry.head;
131 | this.remove(result);
132 | while (true) {
133 | entry = this.entries[this.minRank];
134 | if (entry == undefined || entry.head == null) {
135 | this.minRank++;
136 | if (this.minRank > this.maxRank) {
137 | this.minRank = 0;
138 | this.maxRank = -1;
139 | break;
140 | }
141 | continue;
142 | }
143 | break;
144 | }
145 | if (SANITY_CHECKS) {
146 | // sanity check, find it there is something else with a smaller rank
147 | for (let entry of this.entries) {
148 | if (entry != undefined) {
149 | let at = entry.head;
150 | while (at != null) {
151 | if (at.pqRank < result.pqRank) {
152 | throw new Error("priority queue failed");
153 | }
154 | at = at.pqNext;
155 | }
156 | }
157 | }
158 | this.checkQueue();
159 | }
160 | return result;
161 | }
162 | }
163 |
164 | private remove(a: A) {
165 | if (!a.inPq) {
166 | return;
167 | }
168 | a.inPq = false;
169 | let entry: {
170 | head: A | null,
171 | tail: A | null,
172 | };
173 | if (a.pqRank == Vertex.NULL.rank) {
174 | entry = this.last;
175 | } else {
176 | entry = this.entries[a.pqRank];
177 | }
178 | if (a.pqPrev !== null) {
179 | a.pqPrev.pqNext = a.pqNext;
180 | }
181 | if (a.pqNext !== null) {
182 | a.pqNext.pqPrev = a.pqPrev;
183 | }
184 | if (entry.head == a) {
185 | entry.head = entry.head.pqNext;
186 | }
187 | if (entry.tail == a) {
188 | entry.tail = entry.tail.pqPrev;
189 | }
190 | if (entry.head == null) {
191 | entry.tail = null;
192 | }
193 | a.pqNext = null;
194 | a.pqPrev = null;
195 | if (SANITY_CHECKS) {
196 | this.checkQueue();
197 | // sanity check
198 | {
199 | let at = entry.head;
200 | while (at != null) {
201 | if (!at.inPq) {
202 | throw new Error("PQ");
203 | }
204 | at = at.pqNext;
205 | }
206 | }
207 | //
208 | }
209 | }
210 |
211 | changeRank(a: A, newRank: number) {
212 | this.remove(a);
213 | a.pqRank = newRank;
214 | this.enqueue(a);
215 | }
216 | }
--------------------------------------------------------------------------------
/src/lib/sodium/TimerSystem.ts:
--------------------------------------------------------------------------------
1 | import { Vertex, Source } from "./Vertex";
2 | import * as Collections from 'typescript-collections';
3 | import { Stream, StreamWithSend } from "./Stream";
4 | import { StreamSink } from "./StreamSink";
5 | import { Cell } from "./Cell";
6 | import { CellSink } from "./CellSink";
7 | import { Transaction } from "./Transaction";
8 |
9 | /**
10 | * An interface for implementations of FRP timer systems.
11 | */
12 | export abstract class TimerSystemImpl {
13 | /**
14 | * Set a timer that will execute the specified callback at the specified time.
15 | * @return A function that can be used to cancel the timer.
16 | */
17 | abstract setTimer(t : number, callback : () => void) : () => void;
18 |
19 | /**
20 | * Return the current clock time.
21 | */
22 | abstract now() : number;
23 | }
24 |
25 | let nextSeq : number = 0;
26 |
27 | class Event {
28 | constructor(t : number, sAlarm : StreamWithSend) {
29 | this.t = t;
30 | this.sAlarm = sAlarm;
31 | this.seq = ++nextSeq;
32 | }
33 | t : number;
34 | sAlarm : StreamWithSend;
35 | seq : number; // Used to guarantee uniqueness
36 | }
37 |
38 | export class TimerSystem {
39 | constructor(impl : TimerSystemImpl) {
40 | Transaction.run(() => {
41 | this.impl = impl;
42 | this.tMinimum = 0;
43 | const timeSnk = new CellSink(impl.now());
44 | this.time = timeSnk;
45 | // A dummy listener to time to keep it alive even when there are no other listeners.
46 | this.time.listen((t : number) => { });
47 | Transaction.onStart(() => {
48 | // Ensure the time is always increasing from the FRP's point of view.
49 | const t = this.tMinimum = Math.max(this.tMinimum, impl.now());
50 | // Pop and execute all events earlier than or equal to t (the current time).
51 | while (true) {
52 | let ev : Event = null;
53 | if (!this.eventQueue.isEmpty()) {
54 | let mev = this.eventQueue.minimum();
55 | if (mev.t <= t) {
56 | ev = mev;
57 | // TO DO: Detect infinite loops!
58 | }
59 | }
60 | if (ev != null) {
61 | timeSnk.send(ev.t);
62 | Transaction.run(() => ev.sAlarm.send_(ev.t));
63 | }
64 | else
65 | break;
66 | }
67 | timeSnk.send(t);
68 | });
69 | });
70 | }
71 |
72 | private impl : TimerSystemImpl;
73 | private tMinimum : number; // A guard to allow us to guarantee that the time as
74 | // seen by the FRP is always increasing.
75 |
76 | /**
77 | * A cell giving the current clock time.
78 | */
79 | time : Cell;
80 |
81 | private eventQueue : Collections.BSTree = new Collections.BSTree((a, b) => {
82 | if (a.t < b.t) return -1;
83 | if (a.t > b.t) return 1;
84 | if (a.seq < b.seq) return -1;
85 | if (a.seq > b.seq) return 1;
86 | return 0;
87 | });
88 |
89 | /**
90 | * A timer that fires at the specified time, which can be null, meaning
91 | * that the alarm is not set.
92 | */
93 | at(tAlarm : Cell) : Stream {
94 | let current : Event = null,
95 | cancelCurrent : () => void = null,
96 | active : boolean = false,
97 | tAl : number = null,
98 | sampled : boolean = false;
99 | const sAlarm = new StreamWithSend(null),
100 | updateTimer = () => {
101 | if (cancelCurrent !== null) {
102 | cancelCurrent();
103 | this.eventQueue.remove(current);
104 | }
105 | cancelCurrent = null;
106 | current = null;
107 | if (active) {
108 | if (!sampled) {
109 | sampled = true;
110 | tAl = tAlarm.sampleNoTrans__();
111 | }
112 | if (tAl !== null) {
113 | current = new Event(tAl, sAlarm);
114 | this.eventQueue.add(current);
115 | cancelCurrent = this.impl.setTimer(tAl, () => {
116 | // Correction to ensure the clock time appears to be >= the
117 | // alarm time. It can be a few milliseconds early, and
118 | // this breaks things otherwise, because it doesn't think
119 | // it's time to fire the alarm yet.
120 | this.tMinimum = Math.max(this.tMinimum, tAl);
121 | // Open and close a transaction to trigger queued
122 | // events to run.
123 | Transaction.run(() => {});
124 | });
125 | }
126 | }
127 | };
128 | sAlarm.setVertex__(new Vertex("at", 0, [
129 | new Source(
130 | tAlarm.getVertex__(),
131 | () => {
132 | active = true;
133 | sampled = false;
134 | Transaction.currentTransaction.prioritized(sAlarm.getVertex__(), updateTimer);
135 | const kill = tAlarm.getStream__().listen_(sAlarm.getVertex__(), (oAlarm : number) => {
136 | tAl = oAlarm;
137 | sampled = true;
138 | updateTimer();
139 | }, false);
140 | return () => {
141 | active = false;
142 | updateTimer();
143 | kill();
144 | };
145 | }
146 | )
147 | ]
148 | ));
149 | return sAlarm;
150 | }
151 | }
152 |
153 |
--------------------------------------------------------------------------------
/src/lib/sodium/Lambda.ts:
--------------------------------------------------------------------------------
1 | import { Stream } from "./Stream";
2 | import { Cell } from "./Cell";
3 | import { Source } from "./Vertex";
4 |
5 | export class Lambda1 {
6 | constructor(f : (a : A) => B,
7 | deps : Array|Cell>) {
8 | this.f = f;
9 | this.deps = deps;
10 | }
11 | f : (a : A) => B;
12 | deps : Array|Cell>;
13 | }
14 |
15 | export function lambda1(f : (a : A) => B,
16 | deps : Array|Cell>) : Lambda1
17 | {
18 | return new Lambda1(f, deps);
19 | }
20 |
21 | export function Lambda1_deps(f : ((a : A) => B) | Lambda1) : Array|Cell> {
22 | if (f instanceof Lambda1)
23 | return f.deps;
24 | else
25 | return [];
26 | }
27 |
28 | export function Lambda1_toFunction(f : ((a : A) => B) | Lambda1) : (a : A) => B {
29 | if (f instanceof Lambda1)
30 | return f.f;
31 | else
32 | return <(a : A) => B>f;
33 | }
34 |
35 | export class Lambda2 {
36 | constructor(f : (a : A, b : B) => C,
37 | deps : Array|Cell>) {
38 | this.f = f;
39 | this.deps = deps;
40 | }
41 | f : (a : A, b : B) => C;
42 | deps : Array|Cell>;
43 | }
44 |
45 | export function lambda2(f : (a : A, b : B) => C,
46 | deps : Array|Cell>) : Lambda2
47 | {
48 | return new Lambda2(f, deps);
49 | }
50 |
51 | export function Lambda2_deps(f : ((a : A, b : B) => C) | Lambda2) : Array|Cell> {
52 | if (f instanceof Lambda2)
53 | return f.deps;
54 | else
55 | return [];
56 | }
57 |
58 | export function Lambda2_toFunction(f : ((a : A, b : B) => C) | Lambda2) : (a : A, b : B) => C {
59 | if (f instanceof Lambda2)
60 | return f.f;
61 | else
62 | return <(a : A, b : B) => C>f;
63 | }
64 |
65 | export class Lambda3 {
66 | constructor(f : (a : A, b : B, c : C) => D,
67 | deps : Array|Cell>) {
68 | this.f = f;
69 | this.deps = deps;
70 | }
71 | f : (a : A, b : B, c : C) => D;
72 | deps : Array|Cell>;
73 | }
74 |
75 | export function lambda3(f : (a : A, b : B, c : C) => D,
76 | deps : Array|Cell>) : Lambda3
77 | {
78 | return new Lambda3(f, deps);
79 | }
80 |
81 | export function Lambda3_deps(f : ((a : A, b : B, c : C) => D)
82 | | Lambda3) : Array|Cell> {
83 | if (f instanceof Lambda3)
84 | return f.deps;
85 | else
86 | return [];
87 | }
88 |
89 | export function Lambda3_toFunction(f : ((a : A, b : B, c : C) => D) | Lambda3) : (a : A, b : B, c : C) => D {
90 | if (f instanceof Lambda3)
91 | return f.f;
92 | else
93 | return <(a : A, b : B, c : C) => D>f;
94 | }
95 |
96 | export class Lambda4 {
97 | constructor(f : (a : A, b : B, c : C, d : D) => E,
98 | deps : Array|Cell>) {
99 | this.f = f;
100 | this.deps = deps;
101 | }
102 | f : (a : A, b : B, c : C, d : D) => E;
103 | deps : Array|Cell>;
104 | }
105 |
106 | export function lambda4(f : (a : A, b : B, c : C, d : D) => E,
107 | deps : Array|Cell>) : Lambda4
108 | {
109 | return new Lambda4(f, deps);
110 | }
111 |
112 | export function Lambda4_deps(f : ((a : A, b : B, c : C, d : D) => E)
113 | | Lambda4) : Array|Cell> {
114 | if (f instanceof Lambda4)
115 | return f.deps;
116 | else
117 | return [];
118 | }
119 |
120 | export function Lambda4_toFunction(f : ((a : A, b : B, c : C, d : D) => E)
121 | | Lambda4) : (a : A, b : B, c : C, d : D) => E {
122 | if (f instanceof Lambda4)
123 | return f.f;
124 | else
125 | return <(a : A, b : B, c : C, d : D) => E>f;
126 | }
127 |
128 | export class Lambda5 {
129 | constructor(f : (a : A, b : B, c : C, d : D, e : E) => F,
130 | deps : Array|Cell>) {
131 | this.f = f;
132 | this.deps = deps;
133 | }
134 | f : (a : A, b : B, c : C, d : D, e : E) => F;
135 | deps : Array|Cell>;
136 | }
137 |
138 | export function lambda5(f : (a : A, b : B, c : C, d : D, e : E) => F,
139 | deps : Array|Cell>) : Lambda5
140 | {
141 | return new Lambda5(f, deps);
142 | }
143 |
144 | export function Lambda5_deps(f : ((a : A, b : B, c : C, d : D, e : E) => F)
145 | | Lambda5) : Array|Cell> {
146 | if (f instanceof Lambda5)
147 | return f.deps;
148 | else
149 | return [];
150 | }
151 |
152 | export function Lambda5_toFunction(f : ((a : A, b : B, c : C, d : D, e : E) => F)
153 | | Lambda5) : (a : A, b : B, c : C, d : D, e : E) => F {
154 | if (f instanceof Lambda5)
155 | return f.f;
156 | else
157 | return <(a : A, b : B, c : C, d : D, e : E) => F>f;
158 | }
159 |
160 | export class Lambda6 {
161 | constructor(f : (a : A, b : B, c : C, d : D, e : E, f : F) => G,
162 | deps : Array|Cell>) {
163 | this.f = f;
164 | this.deps = deps;
165 | }
166 | f : (a : A, b : B, c : C, d : D, e : E, f : F) => G;
167 | deps : Array|Cell>;
168 | }
169 |
170 | export function lambda6(f : (a : A, b : B, c : C, d : D, e : E, f : F) => G,
171 | deps : Array|Cell>) : Lambda6
172 | {
173 | return new Lambda6(f, deps);
174 | }
175 |
176 | export function Lambda6_deps(f : ((a : A, b : B, c : C, d : D, e : E, f : F) => G)
177 | | Lambda6) : Array|Cell> {
178 | if (f instanceof Lambda6)
179 | return f.deps;
180 | else
181 | return [];
182 | }
183 |
184 | export function Lambda6_toFunction(f : ((a : A, b : B, c : C, d : D, e : E, f : F) => G)
185 | | Lambda6) : (a : A, b : B, c : C, d : D, e : E, f : F) => G {
186 | if (f instanceof Lambda6)
187 | return f.f;
188 | else
189 | return <(a : A, b : B, c : C, d : D, e : E, f : F) => G>f;
190 | }
191 |
192 | export function toSources(deps : Array|Cell>) : Source[] {
193 | const ss : Source[] = [];
194 | for (let i = 0; i < deps.length; i++) {
195 | const dep = deps[i];
196 | ss.push(new Source(dep.getVertex__(), null));
197 | }
198 | return ss;
199 | }
200 |
--------------------------------------------------------------------------------
/src/lib/sodium/Transaction.ts:
--------------------------------------------------------------------------------
1 | import {Vertex} from './Vertex';
2 | import * as Collections from 'typescript-collections';
3 | import { IntrusiveIndexedPriorityQueue } from "./IntrusiveIndexedPriorityQueue";
4 |
5 | export class Entry
6 | {
7 | constructor(rank: Vertex, action: () => void)
8 | {
9 | this.rank = rank;
10 | this.action = action;
11 | this.seq = Entry.nextSeq++;
12 | this.pqRank = rank.rank;
13 | this.inPq = false;
14 | this.pqNext = null;
15 | this.pqPrev = null;
16 | rank.entries.push(this);
17 | }
18 |
19 | private static nextSeq: number = 0;
20 | rank: Vertex;
21 | action: () => void;
22 | seq: number;
23 | pqRank: number;
24 | inPq: boolean;
25 | pqNext: Entry | null;
26 | pqPrev: Entry | null;
27 |
28 | dispose() {
29 | for (let i = 0; i < this.rank.entries.length; ++i) {
30 | if (this.rank.entries[i] === this) {
31 | this.rank.entries.splice(i, 1);
32 | break;
33 | }
34 | }
35 | }
36 |
37 | toString(): string
38 | {
39 | return this.seq.toString();
40 | }
41 | }
42 |
43 | export class Transaction
44 | {
45 | public static currentTransaction: Transaction = null;
46 | private static onStartHooks: (() => void)[] = [];
47 | private static runningOnStartHooks: boolean = false;
48 |
49 | constructor() {}
50 |
51 | inCallback: number = 0;
52 | rerankEntriesSet = new Set();
53 |
54 | private static prioritizedQ = new IntrusiveIndexedPriorityQueue();
55 |
56 | private entries: Set = new Set();
57 | private sampleQ: Array<() => void> = [];
58 | private lastQ: Array<() => void> = [];
59 | private postQ: Array<() => void> = null;
60 | private static collectCyclesAtEnd: boolean = false;
61 |
62 | requestRegen() {
63 | // no longer required
64 | }
65 |
66 | prioritized(target: Vertex, action: () => void): void
67 | {
68 | const e = new Entry(target, action);
69 | Transaction.prioritizedQ.enqueue(e);
70 | this.entries.add(e);
71 | }
72 |
73 | sample(h: () => void): void
74 | {
75 | this.sampleQ.push(h);
76 | }
77 |
78 | last(h: () => void): void
79 | {
80 | this.lastQ.push(h);
81 | }
82 |
83 | public static _collectCyclesAtEnd(): void
84 | {
85 | Transaction.run(() => Transaction.collectCyclesAtEnd = true);
86 | }
87 |
88 | /**
89 | * Add an action to run after all last() actions.
90 | */
91 | post(childIx: number, action: () => void): void
92 | {
93 | if (this.postQ == null)
94 | this.postQ = [];
95 | // If an entry exists already, combine the old one with the new one.
96 | while (this.postQ.length <= childIx)
97 | this.postQ.push(null);
98 | const existing = this.postQ[childIx],
99 | neu =
100 | existing === null ? action
101 | : () =>
102 | {
103 | existing();
104 | action();
105 | };
106 | this.postQ[childIx] = neu;
107 | }
108 |
109 | // If the priority queue has entries in it when we modify any of the nodes'
110 | // ranks, then we need to re-generate it to make sure it's up-to-date.
111 | private checkRegen(): void
112 | {
113 | for (let entry of this.rerankEntriesSet) {
114 | Transaction.prioritizedQ.changeRank(entry, entry.rank.rank);
115 | }
116 | this.rerankEntriesSet.clear();
117 | }
118 |
119 | public isActive() : boolean
120 | {
121 | return Transaction.currentTransaction ? true : false;
122 | }
123 |
124 | close(): void
125 | {
126 | while(true)
127 | {
128 | while (true)
129 | {
130 | this.checkRegen();
131 | if (Transaction.prioritizedQ.isEmpty()) break;
132 | const e = Transaction.prioritizedQ.dequeue();
133 | this.entries.delete(e);
134 | e.action();
135 | e.dispose();
136 | }
137 |
138 | const sq = this.sampleQ;
139 | this.sampleQ = [];
140 | for (let i = 0; i < sq.length; i++)
141 | sq[i]();
142 |
143 | if(Transaction.prioritizedQ.isEmpty() && this.sampleQ.length < 1) break;
144 | }
145 |
146 | for (let i = 0; i < this.lastQ.length; i++)
147 | this.lastQ[i]();
148 | this.lastQ = [];
149 | if (this.postQ != null)
150 | {
151 | for (let i = 0; i < this.postQ.length; i++)
152 | {
153 | if (this.postQ[i] != null)
154 | {
155 | const parent = Transaction.currentTransaction;
156 | try
157 | {
158 | if (i > 0)
159 | {
160 | Transaction.currentTransaction = new Transaction();
161 | try
162 | {
163 | this.postQ[i]();
164 | Transaction.currentTransaction.close();
165 | }
166 | catch (err)
167 | {
168 | Transaction.currentTransaction.close();
169 | throw err;
170 | }
171 | }
172 | else
173 | {
174 | Transaction.currentTransaction = null;
175 | this.postQ[i]();
176 | }
177 | Transaction.currentTransaction = parent;
178 | }
179 | catch (err)
180 | {
181 | Transaction.currentTransaction = parent;
182 | throw err;
183 | }
184 | }
185 | }
186 | this.postQ = null;
187 | }
188 | }
189 |
190 | /**
191 | * Add a runnable that will be executed whenever a transaction is started.
192 | * That runnable may start transactions itself, which will not cause the
193 | * hooks to be run recursively.
194 | *
195 | * The main use case of this is the implementation of a time/alarm system.
196 | */
197 | static onStart(r: () => void): void
198 | {
199 | Transaction.onStartHooks.push(r);
200 | }
201 |
202 | public static run(f: () => A): A
203 | {
204 | const transWas: Transaction = Transaction.currentTransaction;
205 | if (transWas === null)
206 | {
207 | if (!Transaction.runningOnStartHooks)
208 | {
209 | Transaction.runningOnStartHooks = true;
210 | try
211 | {
212 | for (let i = 0; i < Transaction.onStartHooks.length; i++)
213 | Transaction.onStartHooks[i]();
214 | }
215 | finally
216 | {
217 | Transaction.runningOnStartHooks = false;
218 | }
219 | }
220 | Transaction.currentTransaction = new Transaction();
221 | }
222 | try
223 | {
224 | const a: A = f();
225 | if (transWas === null)
226 | {
227 | Transaction.currentTransaction.close();
228 | Transaction.currentTransaction = null;
229 | if (Transaction.collectCyclesAtEnd) {
230 | Vertex.collectCycles();
231 | Transaction.collectCyclesAtEnd = false;
232 | }
233 | }
234 | return a;
235 | }
236 | catch (err)
237 | {
238 | if (transWas === null)
239 | {
240 | Transaction.currentTransaction.close();
241 | Transaction.currentTransaction = null;
242 | }
243 | throw err;
244 | }
245 | }
246 | }
247 |
248 |
249 |
--------------------------------------------------------------------------------
/src/lib/sodium/Vertex.ts:
--------------------------------------------------------------------------------
1 | import { Transaction, Entry } from "./Transaction";
2 | import { Set } from "typescript-collections";
3 |
4 | let totalRegistrations : number = 0;
5 | export function getTotalRegistrations() : number {
6 | return totalRegistrations;
7 | }
8 |
9 | export class Source {
10 | // Note:
11 | // When register_ == null, a rank-independent source is constructed (a vertex which is just kept alive for the
12 | // lifetime of vertex that contains this source).
13 | // When register_ != null it is likely to be a rank-dependent source, but this will depend on the code inside register_.
14 | //
15 | // rank-independent souces DO NOT bump up the rank of the vertex containing those sources.
16 | // rank-depdendent sources DO bump up the rank of the vertex containing thoses sources when required.
17 | constructor(
18 | origin : Vertex,
19 | register_ : () => () => void
20 | ) {
21 | if (origin === null)
22 | throw new Error("null origin!");
23 | this.origin = origin;
24 | this.register_ = register_;
25 | }
26 | origin : Vertex;
27 | private register_ : () => () => void;
28 | private registered : boolean = false;
29 | private deregister_ : () => void = null;
30 |
31 | register(target : Vertex) : void {
32 | if (!this.registered) {
33 | this.registered = true;
34 | if (this.register_ !== null)
35 | this.deregister_ = this.register_();
36 | else {
37 | // Note: The use of Vertex.NULL here instead of "target" is not a bug, this is done to create a
38 | // rank-independent source. (see note at constructor for more details.). The origin vertex still gets
39 | // added target vertex's children for the memory management algorithm.
40 | this.origin.increment(Vertex.NULL);
41 | target.childrn.push(this.origin);
42 | this.deregister_ = () => {
43 | this.origin.decrement(Vertex.NULL);
44 | for (let i = target.childrn.length-1; i >= 0; --i) {
45 | if (target.childrn[i] === this.origin) {
46 | target.childrn.splice(i, 1);
47 | break;
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | deregister(target : Vertex) : void {
55 | if (this.registered) {
56 | this.registered = false;
57 | if (this.deregister_ !== null)
58 | this.deregister_();
59 | }
60 | }
61 | }
62 |
63 | export enum Color { black, gray, white, purple };
64 | let roots : Vertex[] = [];
65 | let nextID : number = 0;
66 | let verbose : boolean = false;
67 |
68 | export function setVerbose(v : boolean) : void { verbose = v; }
69 |
70 | export function describeAll(v : Vertex, visited : Set)
71 | {
72 | if (visited.contains(v.id)) return;
73 | console.log(v.descr());
74 | visited.add(v.id);
75 | let chs = v.children();
76 | for (let i = 0; i < chs.length; i++)
77 | describeAll(chs[i], visited);
78 | }
79 |
80 | export class Vertex {
81 | static NULL : Vertex = new Vertex("user", 1e12, []);
82 | static collectingCycles : boolean = false;
83 | static toBeFreedList : Vertex[] = [];
84 | id : number;
85 |
86 | constructor(name : string, rank : number, sources : Source[]) {
87 | this.name = name;
88 | this.rank = rank;
89 | this.sources = sources;
90 | this.id = nextID++;
91 | }
92 | name : string;
93 | rank : number;
94 | sources : Source[];
95 | targets : Vertex[] = [];
96 | childrn : Vertex[] = [];
97 | refCount() : number { return this.targets.length; };
98 | visited : boolean = false;
99 | entries: Entry[] = [];
100 | register(target : Vertex) : boolean {
101 | return this.increment(target);
102 | }
103 | deregister(target : Vertex) : void {
104 | if (verbose)
105 | console.log("deregister "+this.descr()+" => "+target.descr());
106 | this.decrement(target);
107 | Transaction._collectCyclesAtEnd();
108 | }
109 | private incRefCount(target : Vertex) : boolean {
110 | let anyChanged : boolean = false;
111 | if (this.refCount() == 0) {
112 | for (let i = 0; i < this.sources.length; i++)
113 | this.sources[i].register(this);
114 | }
115 | this.targets.push(target);
116 | target.childrn.push(this);
117 | if (target.ensureBiggerThan(this.rank))
118 | anyChanged = true;
119 | totalRegistrations++;
120 | return anyChanged;
121 | }
122 |
123 | private decRefCount(target : Vertex) : void {
124 | if (verbose)
125 | console.log("DEC "+this.descr());
126 | let matched = false;
127 | for (let i = target.childrn.length-1; i >= 0; i--)
128 | if (target.childrn[i] === this) {
129 | target.childrn.splice(i, 1);
130 | break;
131 | }
132 | for (let i = 0; i < this.targets.length; i++)
133 | if (this.targets[i] === target) {
134 | this.targets.splice(i, 1);
135 | matched = true;
136 | break;
137 | }
138 | if (matched) {
139 | if (this.refCount() == 0) {
140 | for (let i = 0; i < this.sources.length; i++)
141 | this.sources[i].deregister(this);
142 | }
143 | totalRegistrations--;
144 | }
145 | }
146 |
147 | addSource(src : Source) : void {
148 | this.sources.push(src);
149 | if (this.refCount() > 0)
150 | src.register(this);
151 | }
152 |
153 | private ensureBiggerThan(limit : number) : boolean {
154 | if (this.visited) {
155 | // Undoing cycle detection for now until TimerSystem.ts ranks are checked.
156 | //throw new Error("Vertex cycle detected.");
157 | return false;
158 | }
159 | if (this.rank > limit)
160 | return false;
161 |
162 | this.visited = true;
163 | this.rank = limit + 1;
164 | for (let e of this.entries) {
165 | Transaction.currentTransaction.rerankEntriesSet.add(e);
166 | }
167 | for (let i = 0; i < this.targets.length; i++)
168 | this.targets[i].ensureBiggerThan(this.rank);
169 | this.visited = false;
170 | return true;
171 | }
172 |
173 | descr() : string {
174 | let colStr : string = null;
175 | switch (this.color) {
176 | case Color.black: colStr = "black"; break;
177 | case Color.gray: colStr = "gray"; break;
178 | case Color.white: colStr = "white"; break;
179 | case Color.purple: colStr = "purple"; break;
180 | }
181 | let str = this.id+" "+this.name+" ["+this.refCount()+"/"+this.refCountAdj+"] "+colStr+" ->";
182 | let chs = this.children();
183 | for (let i = 0; i < chs.length; i++) {
184 | str = str + " " + chs[i].id;
185 | }
186 | return str;
187 | }
188 |
189 | // --------------------------------------------------------
190 | // Synchronous Cycle Collection algorithm presented in "Concurrent
191 | // Cycle Collection in Reference Counted Systems" by David F. Bacon
192 | // and V.T. Rajan.
193 |
194 | color : Color = Color.black;
195 | buffered : boolean = false;
196 | refCountAdj : number = 0;
197 |
198 | children() : Vertex[] { return this.childrn; }
199 |
200 | increment(referrer : Vertex) : boolean {
201 | return this.incRefCount(referrer);
202 | }
203 |
204 | decrement(referrer : Vertex) : void {
205 | this.decRefCount(referrer);
206 | if (this.refCount() == 0)
207 | this.release();
208 | else
209 | this.possibleRoots();
210 | }
211 |
212 | release() : void {
213 | this.color = Color.black;
214 | if (!this.buffered)
215 | this.free();
216 | }
217 |
218 | free() : void {
219 | while (this.targets.length > 0)
220 | this.decRefCount(this.targets[0]);
221 | }
222 |
223 | possibleRoots() : void {
224 | if (this.color != Color.purple) {
225 | this.color = Color.purple;
226 | if (!this.buffered) {
227 | this.buffered = true;
228 | roots.push(this);
229 | }
230 | }
231 | }
232 |
233 | static collectCycles() : void {
234 | if (Vertex.collectingCycles) {
235 | return;
236 | }
237 | try {
238 | Vertex.collectingCycles = true;
239 | Vertex.markRoots();
240 | Vertex.scanRoots();
241 | Vertex.collectRoots();
242 | for (let i = Vertex.toBeFreedList.length-1; i >= 0; --i) {
243 | let vertex = Vertex.toBeFreedList.splice(i, 1)[0];
244 | vertex.free();
245 | }
246 | } finally {
247 | Vertex.collectingCycles = false;
248 | }
249 | }
250 |
251 | static markRoots() : void {
252 | const newRoots : Vertex[] = [];
253 | // check refCountAdj was restored to zero before mark roots
254 | if (verbose) {
255 | let stack: Vertex[] = roots.slice(0);
256 | let visited: Set = new Set();
257 | while (stack.length != 0) {
258 | let vertex = stack.pop();
259 | if (visited.contains(vertex.id)) {
260 | continue;
261 | }
262 | visited.add(vertex.id);
263 | if (vertex.refCountAdj != 0) {
264 | console.log("markRoots(): reachable refCountAdj was not reset to zero: " + vertex.descr());
265 | }
266 | for (let i = 0; i < vertex.childrn.length; ++i) {
267 | let child = vertex.childrn[i];
268 | stack.push(child);
269 | }
270 | }
271 | }
272 | //
273 | for (let i = 0; i < roots.length; i++) {
274 | if (verbose)
275 | console.log("markRoots "+roots[i].descr()); // ###
276 | if (roots[i].color == Color.purple) {
277 | roots[i].markGray();
278 | newRoots.push(roots[i]);
279 | }
280 | else {
281 | roots[i].buffered = false;
282 | if (roots[i].color == Color.black && roots[i].refCount() == 0)
283 | Vertex.toBeFreedList.push(roots[i]);
284 | }
285 | }
286 | roots = newRoots;
287 | }
288 |
289 | static scanRoots() : void {
290 | for (let i = 0; i < roots.length; i++)
291 | roots[i].scan();
292 | }
293 |
294 | static collectRoots() : void {
295 | for (let i = 0; i < roots.length; i++) {
296 | roots[i].buffered = false;
297 | roots[i].collectWhite();
298 | }
299 | if (verbose) { // double check adjRefCount is zero for all vertices reachable by roots
300 | let stack: Vertex[] = roots.slice(0);
301 | let visited: Set = new Set();
302 | while (stack.length != 0) {
303 | let vertex = stack.pop();
304 | if (visited.contains(vertex.id)) {
305 | continue;
306 | }
307 | visited.add(vertex.id);
308 | if (vertex.refCountAdj != 0) {
309 | console.log("collectRoots(): reachable refCountAdj was not reset to zero: " + vertex.descr());
310 | }
311 | for (let i = 0; i < vertex.childrn.length; ++i) {
312 | let child = vertex.childrn[i];
313 | stack.push(child);
314 | }
315 | }
316 | }
317 | roots = [];
318 | }
319 |
320 | markGray() : void {
321 | if (this.color != Color.gray) {
322 | this.color = Color.gray;
323 | let chs = this.children();
324 | for (let i = 0; i < chs.length; i++) {
325 | chs[i].refCountAdj--;
326 | if (verbose)
327 | console.log("markGray "+this.descr());
328 | chs[i].markGray();
329 | }
330 | }
331 | }
332 |
333 | scan() : void {
334 | if (verbose)
335 | console.log("scan "+this.descr());
336 | if (this.color == Color.gray) {
337 | if (this.refCount()+this.refCountAdj > 0)
338 | this.scanBlack();
339 | else {
340 | this.color = Color.white;
341 | if (verbose)
342 | console.log("scan WHITE "+this.descr());
343 | let chs = this.children();
344 | for (let i = 0; i < chs.length; i++)
345 | chs[i].scan();
346 | }
347 | }
348 | }
349 |
350 | scanBlack() : void {
351 | this.refCountAdj = 0;
352 | this.color = Color.black;
353 | let chs = this.children();
354 | for (let i = 0; i < chs.length; i++) {
355 | if (verbose)
356 | console.log("scanBlack "+this.descr());
357 | if (chs[i].color != Color.black)
358 | chs[i].scanBlack();
359 | }
360 | }
361 |
362 | collectWhite() : void {
363 | if (this.color == Color.white && !this.buffered) {
364 | if (verbose)
365 | console.log("collectWhite "+this.descr());
366 | this.color = Color.black;
367 | this.refCountAdj = 0;
368 | let chs = this.children();
369 | for (let i = 0; i < chs.length; i++)
370 | chs[i].collectWhite();
371 | Vertex.toBeFreedList.push(this);
372 | }
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/src/tests/unit/StreamSink.spec.ts:
--------------------------------------------------------------------------------
1 |
2 | import {
3 | lambda1,
4 | StreamSink,
5 | StreamLoop,
6 | CellSink,
7 | Transaction,
8 | Tuple2,
9 | Operational,
10 | Cell,
11 | CellLoop,
12 | getTotalRegistrations
13 | } from '../../lib/Lib';
14 |
15 | afterEach(() => {
16 | if (getTotalRegistrations() != 0) {
17 | throw new Error('listeners were not deregistered');
18 | }
19 | });
20 |
21 | test('should test map()', (done) => {
22 | const s = new StreamSink();
23 | const out: number[] = [];
24 | const kill = s.map(a => a + 1)
25 | .listen(a => {
26 | out.push(a);
27 | done();
28 | });
29 | s.send(7);
30 | kill();
31 |
32 | expect([8]).toEqual(out);
33 | });
34 |
35 | test('should throw an error send_with_no_listener_1', () => {
36 | const s = new StreamSink();
37 |
38 | try {
39 | s.send(7);
40 | } catch (e) {
41 | expect(e.message).toBe('send() was invoked before listeners were registered');
42 | }
43 |
44 | });
45 |
46 | test('should (not?) throw an error send_with_no_listener_2', () => {
47 | const s = new StreamSink();
48 | const out: number[] = [];
49 | const kill = s.map(a => a + 1)
50 | .listen(a => out.push(a));
51 |
52 | s.send(7);
53 | kill();
54 |
55 | try {
56 | // TODO: the message below is bit misleading, need to verify with Stephen B.
57 | // - "this should not throw, because once() uses this mechanism"
58 | s.send(9);
59 | } catch (e) {
60 | expect(e.message).toBe('send() was invoked before listeners were registered');
61 | }
62 | });
63 |
64 | test('should map_tack', (done) => {
65 | const s = new StreamSink(),
66 | t = new StreamSink(),
67 | out: number[] = [],
68 | kill = s.map(lambda1((a: number) => a + 1, [t]))
69 | .listen(a => {
70 | out.push(a);
71 | done();
72 | });
73 |
74 | s.send(7);
75 | t.send("banana");
76 | kill();
77 |
78 | expect([8]).toEqual(out);
79 | });
80 |
81 | test('should test mapTo()', (done) => {
82 | const s = new StreamSink(),
83 | out: string[] = [],
84 | kill = s.mapTo("fusebox")
85 | .listen(a => {
86 | out.push(a);
87 | if (out.length === 2) {
88 | done();
89 | }
90 | });
91 |
92 | s.send(7);
93 | s.send(9);
94 | kill();
95 |
96 | expect(['fusebox', 'fusebox']).toEqual(out);
97 | });
98 |
99 | test('should do mergeNonSimultaneous', (done) => {
100 | const s1 = new StreamSink(),
101 | s2 = new StreamSink(),
102 | out: number[] = [];
103 |
104 | const kill = s2.orElse(s1)
105 | .listen(a => {
106 | out.push(a);
107 | if (out.length === 3) {
108 | done();
109 | }
110 | });
111 |
112 | s1.send(7);
113 | s2.send(9);
114 | s1.send(8);
115 | kill();
116 |
117 | expect([7, 9, 8]).toEqual(out);
118 | });
119 |
120 | test('should do mergeSimultaneous', (done) => {
121 | const s1 = new StreamSink((l: number, r: number) => { return r; }),
122 | s2 = new StreamSink((l: number, r: number) => { return r; }),
123 | out: number[] = [],
124 | kill = s2.orElse(s1)
125 | .listen(a => {
126 | out.push(a);
127 | if (out.length === 5) {
128 | done();
129 | }
130 | });
131 |
132 | Transaction.run(() => {
133 | s1.send(7);
134 | s2.send(60);
135 | });
136 | Transaction.run(() => {
137 | s1.send(9);
138 | });
139 | Transaction.run(() => {
140 | s1.send(7);
141 | s1.send(60);
142 | s2.send(8);
143 | s2.send(90);
144 | });
145 | Transaction.run(() => {
146 | s2.send(8);
147 | s2.send(90);
148 | s1.send(7);
149 | s1.send(60);
150 | });
151 | Transaction.run(() => {
152 | s2.send(8);
153 | s1.send(7);
154 | s2.send(90);
155 | s1.send(60);
156 | });
157 | kill();
158 |
159 | expect([60, 9, 90, 90, 90]).toEqual(out);
160 | });
161 |
162 | test('should do coalesce', (done) => {
163 | const s = new StreamSink