raptiye 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +171 -0
  2. package/bench/bench-batch-curve.js +121 -0
  3. package/bench/bench-event-loop.js +91 -0
  4. package/bench/bench-failover.js +172 -0
  5. package/bench/bench-memory.js +107 -0
  6. package/bench/bench-partition.js +134 -0
  7. package/bench/bench-pipeline.js +94 -0
  8. package/bench/bench-recovery.js +134 -0
  9. package/bench/bench-replication.js +107 -0
  10. package/bench/bench-single-node.js +77 -0
  11. package/bench/bench-slow-follower.js +96 -0
  12. package/bench/harness.js +87 -0
  13. package/bench/run-all.js +83 -0
  14. package/bench-results.json +598 -0
  15. package/index.js +30 -0
  16. package/package.json +24 -0
  17. package/src/core/engine.js +907 -0
  18. package/src/core/invariants.js +146 -0
  19. package/src/node/raptiye.js +296 -0
  20. package/src/node/stats.js +82 -0
  21. package/src/protocol/checksum.js +66 -0
  22. package/src/protocol/wire.js +551 -0
  23. package/src/replication/pipeline.js +235 -0
  24. package/src/sim/cluster.js +316 -0
  25. package/src/sim/prng.js +52 -0
  26. package/src/sim/virtual-clock.js +82 -0
  27. package/src/storage/file-log.js +394 -0
  28. package/src/storage/interface.js +113 -0
  29. package/src/storage/memory-log.js +183 -0
  30. package/src/transport/interface.js +47 -0
  31. package/src/transport/memory-transport.js +67 -0
  32. package/src/transport/tcp-transport.js +200 -0
  33. package/src/types.js +87 -0
  34. package/test/chaos/chaos.test.js +112 -0
  35. package/test/integration/cluster.test.js +192 -0
  36. package/test/integration/node.test.js +78 -0
  37. package/test/integration/replication-pipeline.test.js +45 -0
  38. package/test/unit/core.test.js +163 -0
  39. package/test/unit/protocol.test.js +181 -0
  40. package/test/unit/storage.test.js +127 -0
  41. package/test/unit/transport.test.js +90 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Generic point-to-point batch replication pipeline.
3
+ * Manages reliable delivery, in-flight windowing, flow control, and ACKs.
4
+ */
5
+
6
+ import { MessageType } from '../types.js';
7
+ import { encodeMessage } from '../protocol/wire.js';
8
+
9
+ export class BatchReceipt {
10
+ /**
11
+ * @param {number} generationId
12
+ * @param {Promise<object>} promise
13
+ */
14
+ constructor(generationId, promise) {
15
+ this.generationId = generationId;
16
+ this._promise = promise;
17
+ }
18
+
19
+ delivered() {
20
+ return this._promise;
21
+ }
22
+ }
23
+
24
+ export class ReplicationPipeline {
25
+ /**
26
+ * @param {object} config
27
+ * @param {import('../transport/interface.js').Transport} config.transport
28
+ * @param {number} [config.localNode=1]
29
+ * @param {number} [config.remoteNode=2]
30
+ * @param {number} [config.maxInflightBatches=8]
31
+ * @param {number} [config.maxInflightBytes=32 * 1024 * 1024]
32
+ */
33
+ constructor({
34
+ transport,
35
+ localNode = 1,
36
+ remoteNode = 2,
37
+ maxInflightBatches = 8,
38
+ maxInflightBytes = 32 * 1024 * 1024
39
+ }) {
40
+ this.transport = transport;
41
+ this.localNode = Number(localNode);
42
+ this.remoteNode = Number(remoteNode);
43
+ this.maxInflightBatches = maxInflightBatches;
44
+ this.maxInflightBytes = maxInflightBytes;
45
+
46
+ this.inflightBatches = 0;
47
+ this.inflightBytes = 0;
48
+
49
+ // generationId -> { resolve, reject, sentAt, bytes }
50
+ this._pendingReceipts = new Map();
51
+ // Capacity waiters
52
+ this._capacityWaiters = [];
53
+ // Receiver callback
54
+ this._batchHandler = null;
55
+
56
+ // Metrics
57
+ this.stats = {
58
+ payloadBytes: 0,
59
+ wireBytes: 0,
60
+ frames: 0,
61
+ tcpWrites: 0,
62
+ tcpDrains: 0,
63
+ inflightBytes: 0,
64
+ inflightBatches: 0,
65
+ rttMs: 0,
66
+ ackLatencyMs: 0,
67
+ reconnects: 0
68
+ };
69
+
70
+ // Listen to transport messages
71
+ this.transport.onMessage((from, msg) => {
72
+ this._handleMessage(from, msg);
73
+ });
74
+ }
75
+
76
+ onBatch(handler) {
77
+ this._batchHandler = handler;
78
+ }
79
+
80
+ canSubmit(bytes = 0) {
81
+ return (
82
+ this.inflightBatches < this.maxInflightBatches &&
83
+ (this.inflightBytes + bytes) <= this.maxInflightBytes
84
+ );
85
+ }
86
+
87
+ async waitForCapacity(bytes = 0) {
88
+ while (!this.canSubmit(bytes)) {
89
+ await new Promise((resolve) => this._capacityWaiters.push(resolve));
90
+ }
91
+ }
92
+
93
+ _notifyCapacity() {
94
+ while (this._capacityWaiters.length > 0) {
95
+ const waiter = this._capacityWaiters.shift();
96
+ waiter();
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Submit a batch to be replicated to the remote peer.
102
+ * @param {object} batch
103
+ * @param {number} batch.generationId
104
+ * @param {number|bigint} batch.firstSequence
105
+ * @param {number|bigint} batch.lastSequence
106
+ * @param {number} batch.entryCount
107
+ * @param {number} [batch.byteLength]
108
+ * @param {Uint8Array[]} batch.buffers
109
+ * @returns {Promise<BatchReceipt>}
110
+ */
111
+ async submitBatch({
112
+ generationId,
113
+ firstSequence,
114
+ lastSequence,
115
+ entryCount,
116
+ byteLength = 0,
117
+ buffers = []
118
+ }) {
119
+ let totalPayload = byteLength;
120
+ if (totalPayload === 0) {
121
+ for (let i = 0; i < buffers.length; i++) {
122
+ totalPayload += buffers[i].byteLength;
123
+ }
124
+ }
125
+
126
+ // Await inflight capacity (backpressure from slow network / slow receiver)
127
+ await this.waitForCapacity(totalPayload);
128
+
129
+ this.inflightBatches++;
130
+ this.inflightBytes += totalPayload;
131
+ this.stats.inflightBatches = this.inflightBatches;
132
+ this.stats.inflightBytes = this.inflightBytes;
133
+
134
+ const deliveryPromise = new Promise((resolve, reject) => {
135
+ this._pendingReceipts.set(generationId, {
136
+ resolve,
137
+ reject,
138
+ sentAt: Date.now(),
139
+ bytes: totalPayload
140
+ });
141
+ });
142
+
143
+ const chunks = encodeMessage({
144
+ type: MessageType.BATCH_DATA,
145
+ sourceNode: this.localNode,
146
+ destNode: this.remoteNode,
147
+ generationId,
148
+ firstSequence: BigInt(firstSequence),
149
+ lastSequence: BigInt(lastSequence),
150
+ entryCount,
151
+ buffers
152
+ });
153
+
154
+ this.stats.frames++;
155
+ this.stats.tcpWrites++;
156
+ this.stats.payloadBytes += totalPayload;
157
+
158
+ // Approximate wire bytes from chunks
159
+ for (let i = 0; i < chunks.length; i++) {
160
+ this.stats.wireBytes += chunks[i].byteLength;
161
+ }
162
+
163
+ this.transport.sendv(this.remoteNode, chunks);
164
+
165
+ return new BatchReceipt(generationId, deliveryPromise);
166
+ }
167
+
168
+ async _handleMessage(from, msg) {
169
+ if (msg.type === MessageType.BATCH_DATA) {
170
+ this.stats.frames++;
171
+ if (this._batchHandler) {
172
+ try {
173
+ await this._batchHandler(msg);
174
+ // Send ACK
175
+ const ackChunks = encodeMessage({
176
+ type: MessageType.BATCH_ACK,
177
+ sourceNode: this.localNode,
178
+ destNode: from,
179
+ generationId: msg.generationId,
180
+ sequence: msg.lastSequence,
181
+ bytesReceived: BigInt(msg.buffers ? msg.buffers.reduce((a, b) => a + b.byteLength, 0) : 0),
182
+ status: 0
183
+ });
184
+ this.transport.sendv(from, ackChunks);
185
+ } catch (err) {
186
+ const nackChunks = encodeMessage({
187
+ type: MessageType.BATCH_ACK,
188
+ sourceNode: this.localNode,
189
+ destNode: from,
190
+ generationId: msg.generationId,
191
+ sequence: msg.lastSequence,
192
+ bytesReceived: 0n,
193
+ status: 1
194
+ });
195
+ this.transport.sendv(from, nackChunks);
196
+ }
197
+ }
198
+ } else if (msg.type === MessageType.BATCH_ACK) {
199
+ const pending = this._pendingReceipts.get(msg.generationId);
200
+ if (pending) {
201
+ this._pendingReceipts.delete(msg.generationId);
202
+
203
+ const rtt = Math.max(0, Date.now() - pending.sentAt);
204
+ this.stats.rttMs = rtt;
205
+ this.stats.ackLatencyMs = rtt;
206
+
207
+ this.inflightBatches = Math.max(0, this.inflightBatches - 1);
208
+ this.inflightBytes = Math.max(0, this.inflightBytes - pending.bytes);
209
+ this.stats.inflightBatches = this.inflightBatches;
210
+ this.stats.inflightBytes = this.inflightBytes;
211
+
212
+ if (msg.status === 0) {
213
+ pending.resolve(msg);
214
+ } else {
215
+ pending.reject(new Error(`Remote node rejected batch ${msg.generationId}`));
216
+ }
217
+
218
+ this._notifyCapacity();
219
+ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Wait until all submitted batches are delivered.
225
+ */
226
+ async drain() {
227
+ while (this._pendingReceipts.size > 0) {
228
+ await new Promise((resolve) => setTimeout(resolve, 10));
229
+ }
230
+ }
231
+
232
+ getStats() {
233
+ return { ...this.stats };
234
+ }
235
+ }
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Deterministic Cluster Simulator for Raptiye.
3
+ * Fully reproducible from random seed with comprehensive fault injection.
4
+ */
5
+
6
+ import { PRNG } from './prng.js';
7
+ import { VirtualClock } from './virtual-clock.js';
8
+ import { ConsensusEngine } from '../core/engine.js';
9
+ import { MemoryLog } from '../storage/memory-log.js';
10
+ import { ClusterInvariants } from '../core/invariants.js';
11
+ import { Role, EffectType, EventType } from '../types.js';
12
+
13
+ export class SimulatedNode {
14
+ constructor({ id, peers, storage, clock, prng, cluster, minElection = 150, maxElection = 300, heartbeatInterval = 50 }) {
15
+ this.id = Number(id);
16
+ this.peers = peers;
17
+ this.storage = storage;
18
+ this.clock = clock;
19
+ this.prng = prng;
20
+ this.cluster = cluster;
21
+
22
+ this.minElection = minElection;
23
+ this.maxElection = maxElection;
24
+ this.heartbeatInterval = heartbeatInterval;
25
+
26
+ this.alive = true;
27
+ this.electionTimerId = null;
28
+ this.heartbeatTimerId = null;
29
+
30
+ this.appliedEntries = [];
31
+ this.committedNotifications = [];
32
+
33
+ this.engine = new ConsensusEngine({
34
+ id: this.id,
35
+ peers: this.peers,
36
+ storage: this.storage
37
+ });
38
+ }
39
+
40
+ start() {
41
+ this.resetElectionTimer();
42
+ }
43
+
44
+ stop() {
45
+ this.alive = false;
46
+ if (this.electionTimerId) {
47
+ this.clock.clearTimeout(this.electionTimerId);
48
+ this.electionTimerId = null;
49
+ }
50
+ if (this.heartbeatTimerId) {
51
+ this.clock.clearTimeout(this.heartbeatTimerId);
52
+ this.heartbeatTimerId = null;
53
+ }
54
+ }
55
+
56
+ restart() {
57
+ this.alive = true;
58
+ // Recreate engine from stored state
59
+ this.engine = new ConsensusEngine({
60
+ id: this.id,
61
+ peers: this.peers,
62
+ storage: this.storage
63
+ });
64
+ this.start();
65
+ }
66
+
67
+ resetElectionTimer() {
68
+ if (!this.alive) return;
69
+ if (this.electionTimerId) {
70
+ this.clock.clearTimeout(this.electionTimerId);
71
+ }
72
+ const timeout = this.prng.nextInt(this.minElection, this.maxElection);
73
+ this.electionTimerId = this.clock.setTimeout(() => {
74
+ this.electionTimerId = null;
75
+ this.step({ type: EventType.ELECTION_TIMEOUT });
76
+ }, timeout);
77
+ }
78
+
79
+ resetHeartbeatTimer() {
80
+ if (!this.alive) return;
81
+ if (this.heartbeatTimerId) {
82
+ this.clock.clearTimeout(this.heartbeatTimerId);
83
+ }
84
+ this.heartbeatTimerId = this.clock.setTimeout(() => {
85
+ this.heartbeatTimerId = null;
86
+ this.step({ type: EventType.HEARTBEAT_TIMEOUT });
87
+ }, this.heartbeatInterval);
88
+ }
89
+
90
+ step(event) {
91
+ if (!this.alive) return [];
92
+ const effects = this.engine.step(event);
93
+ this.processEffects(effects);
94
+ return effects;
95
+ }
96
+
97
+ processEffects(effects) {
98
+ for (const effect of effects) {
99
+ switch (effect.type) {
100
+ case EffectType.SEND:
101
+ this.cluster.deliverMessage(this.id, effect.to, effect.message);
102
+ break;
103
+
104
+ case EffectType.RESET_ELECTION_TIMER:
105
+ this.resetElectionTimer();
106
+ break;
107
+
108
+ case EffectType.RESET_HEARTBEAT_TIMER:
109
+ this.resetHeartbeatTimer();
110
+ break;
111
+
112
+ case EffectType.BECOME_LEADER:
113
+ if (this.electionTimerId) {
114
+ this.clock.clearTimeout(this.electionTimerId);
115
+ this.electionTimerId = null;
116
+ }
117
+ this.resetHeartbeatTimer();
118
+ break;
119
+
120
+ case EffectType.BECOME_FOLLOWER:
121
+ if (this.heartbeatTimerId) {
122
+ this.clock.clearTimeout(this.heartbeatTimerId);
123
+ this.heartbeatTimerId = null;
124
+ }
125
+ this.resetElectionTimer();
126
+ break;
127
+
128
+ case EffectType.APPLY:
129
+ this.appliedEntries.push(effect.entry);
130
+ break;
131
+
132
+ case EffectType.NOTIFY_COMMITTED:
133
+ this.committedNotifications.push(effect.index);
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ export class ClusterSimulator {
141
+ constructor({
142
+ nodeCount = 3,
143
+ seed = 184822,
144
+ minElection = 150,
145
+ maxElection = 300,
146
+ heartbeatInterval = 50,
147
+ packetDelayMin = 1,
148
+ packetDelayMax = 10,
149
+ packetDropRate = 0,
150
+ packetDuplicateRate = 0
151
+ } = {}) {
152
+ this.seed = seed;
153
+ this.prng = new PRNG(seed);
154
+ this.clock = new VirtualClock();
155
+ this.invariants = new ClusterInvariants();
156
+
157
+ this.packetDelayMin = packetDelayMin;
158
+ this.packetDelayMax = packetDelayMax;
159
+ this.packetDropRate = packetDropRate;
160
+ this.packetDuplicateRate = packetDuplicateRate;
161
+
162
+ this.partitions = new Set(); // Strings of "from:to"
163
+ this.nodes = new Map();
164
+
165
+ const nodeIds = [];
166
+ for (let i = 1; i <= nodeCount; i++) {
167
+ nodeIds.push(i);
168
+ }
169
+
170
+ for (const id of nodeIds) {
171
+ const peers = nodeIds.filter(nid => nid !== id);
172
+ const storage = new MemoryLog();
173
+ const node = new SimulatedNode({
174
+ id,
175
+ peers,
176
+ storage,
177
+ clock: this.clock,
178
+ prng: this.prng,
179
+ cluster: this,
180
+ minElection,
181
+ maxElection,
182
+ heartbeatInterval
183
+ });
184
+ this.nodes.set(id, node);
185
+ }
186
+ }
187
+
188
+ start() {
189
+ for (const node of this.nodes.values()) {
190
+ node.start();
191
+ }
192
+ }
193
+
194
+ getNode(id) {
195
+ return this.nodes.get(Number(id));
196
+ }
197
+
198
+ activeNodes() {
199
+ return Array.from(this.nodes.values()).filter(n => n.alive);
200
+ }
201
+
202
+ getLeader() {
203
+ let bestLeader = null;
204
+ let maxTerm = -1n;
205
+ for (const node of this.activeNodes()) {
206
+ if (node.engine.role === Role.LEADER) {
207
+ if (node.engine.currentTerm > maxTerm) {
208
+ maxTerm = node.engine.currentTerm;
209
+ bestLeader = node;
210
+ }
211
+ }
212
+ }
213
+ return bestLeader;
214
+ }
215
+
216
+ deliverMessage(from, to, message) {
217
+ const sender = this.getNode(from);
218
+ const receiver = this.getNode(to);
219
+
220
+ if (!sender || !receiver || !sender.alive || !receiver.alive) {
221
+ return;
222
+ }
223
+
224
+ // Check partition
225
+ if (this.isPartitioned(from, to)) {
226
+ return;
227
+ }
228
+
229
+ // Check packet drop
230
+ if (this.packetDropRate > 0 && this.prng.boolean(this.packetDropRate)) {
231
+ return;
232
+ }
233
+
234
+ const sendPacket = () => {
235
+ const delay = this.prng.nextInt(this.packetDelayMin, this.packetDelayMax);
236
+ this.clock.setTimeout(() => {
237
+ if (receiver.alive && !this.isPartitioned(from, to)) {
238
+ receiver.step({
239
+ type: EventType.MESSAGE,
240
+ message
241
+ });
242
+ this.verify();
243
+ }
244
+ }, delay);
245
+ };
246
+
247
+ sendPacket();
248
+
249
+ // Check packet duplication
250
+ if (this.packetDuplicateRate > 0 && this.prng.boolean(this.packetDuplicateRate)) {
251
+ sendPacket();
252
+ }
253
+ }
254
+
255
+ isPartitioned(from, to) {
256
+ return this.partitions.has(`${from}:${to}`) || this.partitions.has(`${to}:${from}`);
257
+ }
258
+
259
+ partition(groupA, groupB) {
260
+ for (const a of groupA) {
261
+ for (const b of groupB) {
262
+ this.partitions.add(`${a}:${b}`);
263
+ this.partitions.add(`${b}:${a}`);
264
+ }
265
+ }
266
+ }
267
+
268
+ heal() {
269
+ this.partitions.clear();
270
+ }
271
+
272
+ killNode(id) {
273
+ const node = this.getNode(id);
274
+ if (node) {
275
+ node.stop();
276
+ }
277
+ }
278
+
279
+ restartNode(id) {
280
+ const node = this.getNode(id);
281
+ if (node) {
282
+ node.restart();
283
+ }
284
+ }
285
+
286
+ submit(payload) {
287
+ const leader = this.getLeader();
288
+ if (!leader) {
289
+ return { success: false, error: 'NO_LEADER' };
290
+ }
291
+ leader.step({
292
+ type: EventType.SUBMIT,
293
+ payload
294
+ });
295
+ this.verify();
296
+ return { success: true, leaderId: leader.id };
297
+ }
298
+
299
+ advance(ms) {
300
+ this.clock.advance(ms);
301
+ this.verify();
302
+ }
303
+
304
+ runUntilQuiet(maxSteps = 1000) {
305
+ let steps = 0;
306
+ while (this.clock.pendingCount() > 0 && steps < maxSteps) {
307
+ this.clock.step();
308
+ this.verify();
309
+ steps++;
310
+ }
311
+ }
312
+
313
+ verify() {
314
+ this.invariants.check(this.activeNodes());
315
+ }
316
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Seeded pseudo-random number generator for deterministic cluster simulations.
3
+ * Uses 32-bit Mulberry32 algorithm.
4
+ */
5
+
6
+ export class PRNG {
7
+ constructor(seed = 184822) {
8
+ this.initialSeed = seed;
9
+ this.s = seed >>> 0;
10
+ }
11
+
12
+ /**
13
+ * Generates a float in [0, 1).
14
+ * @returns {number}
15
+ */
16
+ nextFloat() {
17
+ let t = (this.s += 0x6D2B79F5) >>> 0;
18
+ t = Math.imul(t ^ (t >>> 15), t | 1);
19
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
20
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
21
+ }
22
+
23
+ /**
24
+ * Generates an integer in [min, max] inclusive.
25
+ * @param {number} min
26
+ * @param {number} max
27
+ * @returns {number}
28
+ */
29
+ nextInt(min, max) {
30
+ return Math.floor(this.nextFloat() * (max - min + 1)) + min;
31
+ }
32
+
33
+ /**
34
+ * Returns true with given probability [0, 1].
35
+ * @param {number} probability
36
+ * @returns {boolean}
37
+ */
38
+ boolean(probability = 0.5) {
39
+ return this.nextFloat() < probability;
40
+ }
41
+
42
+ /**
43
+ * Picks a random element from an array.
44
+ * @template T
45
+ * @param {T[]} array
46
+ * @returns {T}
47
+ */
48
+ pick(array) {
49
+ if (array.length === 0) return null;
50
+ return array[this.nextInt(0, array.length - 1)];
51
+ }
52
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Virtual discrete-event clock for deterministic simulation.
3
+ */
4
+
5
+ export class VirtualClock {
6
+ constructor() {
7
+ this.now = 0;
8
+ this._timers = []; // Array of { id, dueTime, callback, period }
9
+ this._nextId = 1;
10
+ }
11
+
12
+ _insertTimer(timer) {
13
+ let low = 0;
14
+ let high = this._timers.length;
15
+ while (low < high) {
16
+ const mid = (low + high) >>> 1;
17
+ if (this._timers[mid].dueTime <= timer.dueTime) {
18
+ low = mid + 1;
19
+ } else {
20
+ high = mid;
21
+ }
22
+ }
23
+ this._timers.splice(low, 0, timer);
24
+ }
25
+
26
+ setTimeout(callback, delayMs) {
27
+ const id = this._nextId++;
28
+ const dueTime = this.now + Math.max(0, delayMs);
29
+ this._insertTimer({ id, dueTime, callback, period: null });
30
+ return id;
31
+ }
32
+
33
+ setInterval(callback, periodMs) {
34
+ const id = this._nextId++;
35
+ const dueTime = this.now + Math.max(0, periodMs);
36
+ this._insertTimer({ id, dueTime, callback, period: periodMs });
37
+ return id;
38
+ }
39
+
40
+ clearTimeout(id) {
41
+ const idx = this._timers.findIndex(t => t.id === id);
42
+ if (idx !== -1) {
43
+ this._timers.splice(idx, 1);
44
+ }
45
+ }
46
+
47
+ clearInterval(id) {
48
+ this.clearTimeout(id);
49
+ }
50
+
51
+ advance(ms) {
52
+ const target = this.now + ms;
53
+ while (this._timers.length > 0 && this._timers[0].dueTime <= target) {
54
+ const timer = this._timers.shift();
55
+ this.now = timer.dueTime;
56
+ timer.callback();
57
+
58
+ if (timer.period !== null) {
59
+ timer.dueTime = this.now + timer.period;
60
+ this._insertTimer(timer);
61
+ }
62
+ }
63
+ this.now = target;
64
+ }
65
+
66
+ step() {
67
+ if (this._timers.length === 0) return false;
68
+ const timer = this._timers.shift();
69
+ this.now = timer.dueTime;
70
+ timer.callback();
71
+
72
+ if (timer.period !== null) {
73
+ timer.dueTime = this.now + timer.period;
74
+ this._insertTimer(timer);
75
+ }
76
+ return true;
77
+ }
78
+
79
+ pendingCount() {
80
+ return this._timers.length;
81
+ }
82
+ }