@fireemu/darwin-arm64 0.13.0 → 0.15.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.
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @fireemu/darwin-arm64
|
|
2
2
|
|
|
3
3
|
The [fireemu](https://www.npmjs.com/package/fireemu) daemon built for `darwin-arm64`
|
|
4
|
-
(Rust target `aarch64-apple-darwin`), version 0.
|
|
4
|
+
(Rust target `aarch64-apple-darwin`), version 0.15.0.
|
|
5
5
|
|
|
6
6
|
macOS 13 or newer on Apple silicon.
|
|
7
7
|
|
package/bin/fireemu
CHANGED
|
Binary file
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Opt-in application time. Native event-loop and lifecycle deadlines stay real.
|
|
2
|
+
import timers from 'node:timers';
|
|
3
|
+
import promises from 'node:timers/promises';
|
|
4
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
const NativeDate = Date;
|
|
8
|
+
const dateConstructor = Object.getOwnPropertyDescriptor(NativeDate.prototype, 'constructor');
|
|
9
|
+
const nativeImmediate = timers.setImmediate;
|
|
10
|
+
const native = {
|
|
11
|
+
setTimeout: timers.setTimeout, clearTimeout: timers.clearTimeout,
|
|
12
|
+
setInterval: timers.setInterval, clearInterval: timers.clearInterval,
|
|
13
|
+
};
|
|
14
|
+
const nativePromises = { setTimeout: promises.setTimeout, setInterval: promises.setInterval };
|
|
15
|
+
const nanosPerMilli = 1_000_000n;
|
|
16
|
+
const dateLimit = 8_640_000_000_000_000n;
|
|
17
|
+
const maxDrain = 10_000;
|
|
18
|
+
|
|
19
|
+
function nanos(value, name) {
|
|
20
|
+
if (typeof value !== 'string' || !/^(?:0|-?[1-9][0-9]*)$/.test(value)) {
|
|
21
|
+
throw new TypeError(`invalid clock ${name}`);
|
|
22
|
+
}
|
|
23
|
+
return BigInt(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function milliseconds(instant) {
|
|
27
|
+
const value = instant >= 0n ? instant / nanosPerMilli : (instant - nanosPerMilli + 1n) / nanosPerMilli;
|
|
28
|
+
if (value < -dateLimit || value > dateLimit) throw new RangeError('clock outside JavaScript Date range');
|
|
29
|
+
return Number(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function delay(value) {
|
|
33
|
+
const number = Number(value ?? 1);
|
|
34
|
+
return !Number.isFinite(number) || number < 1 || number > 2_147_483_647 ? 1 : Math.trunc(number);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function abortError(reason) {
|
|
38
|
+
const error = new Error('The operation was aborted', {cause:reason});
|
|
39
|
+
error.name = 'AbortError';
|
|
40
|
+
error.code = 'ABORT_ERR';
|
|
41
|
+
return error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function options(value) {
|
|
45
|
+
if (value === null || typeof value !== 'object') throw new TypeError('timer options must be an object');
|
|
46
|
+
if (value.signal !== undefined && !(value.signal instanceof AbortSignal)) throw new TypeError('invalid AbortSignal');
|
|
47
|
+
if (value.ref !== undefined && typeof value.ref !== 'boolean') throw new TypeError('timer ref must be boolean');
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createRuntimeClock({date = 'real', timers: timerPolicy = 'real', instantNanos = '0', elapsedNanos = '0'} = {}) {
|
|
52
|
+
if (!['real','virtual'].includes(date) || !['real','virtual'].includes(timerPolicy) ||
|
|
53
|
+
(timerPolicy === 'virtual' && date !== 'virtual')) throw new TypeError('invalid runtime clock policy');
|
|
54
|
+
let instant = nanos(instantNanos, 'instant');
|
|
55
|
+
let epochMillis = date === 'virtual' ? milliseconds(instant) : 0;
|
|
56
|
+
let elapsed = nanos(elapsedNanos, 'elapsed');
|
|
57
|
+
if (elapsed < 0n) throw new RangeError('negative timer elapsed time');
|
|
58
|
+
let installed = false;
|
|
59
|
+
let sequence = 0;
|
|
60
|
+
let draining = false;
|
|
61
|
+
const queue = new Map();
|
|
62
|
+
const handles = new Map();
|
|
63
|
+
|
|
64
|
+
class Timer {
|
|
65
|
+
constructor(callback, duration, args, repeat) {
|
|
66
|
+
this.id = ++sequence;
|
|
67
|
+
this.callback = callback;
|
|
68
|
+
this.duration = BigInt(delay(duration)) * nanosPerMilli;
|
|
69
|
+
this.args = args;
|
|
70
|
+
this.repeat = repeat;
|
|
71
|
+
this.referenced = true;
|
|
72
|
+
this.cancelled = false;
|
|
73
|
+
this.refresh();
|
|
74
|
+
}
|
|
75
|
+
refresh() {
|
|
76
|
+
if (!this.cancelled) {
|
|
77
|
+
this.deadline = elapsed + this.duration;
|
|
78
|
+
this.order = ++sequence;
|
|
79
|
+
queue.set(this.id, this);
|
|
80
|
+
handles.set(this.id, this);
|
|
81
|
+
}
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
ref() { this.referenced = true; return this; }
|
|
85
|
+
unref() { this.referenced = false; return this; }
|
|
86
|
+
hasRef() { return this.referenced; }
|
|
87
|
+
close() { cancel(this); return this; }
|
|
88
|
+
[Symbol.toPrimitive]() { return this.id; }
|
|
89
|
+
[Symbol.dispose]() { cancel(this); }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function schedule(callback, duration, args, repeat) {
|
|
93
|
+
if (typeof callback !== 'function') throw new TypeError('timer callback must be a function');
|
|
94
|
+
return new Timer(callback, duration, args, repeat);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cancel(value) {
|
|
98
|
+
const handle = value instanceof Timer ? value : handles.get(Number(value));
|
|
99
|
+
if (!handle) return;
|
|
100
|
+
handle.cancelled = true;
|
|
101
|
+
queue.delete(handle.id);
|
|
102
|
+
handles.delete(handle.id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function promiseTimeout(duration, value, config = {}) {
|
|
106
|
+
try { options(config); } catch (error) { return Promise.reject(error); }
|
|
107
|
+
if (config.signal?.aborted) return Promise.reject(abortError(config.signal.reason));
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const complete = () => {
|
|
110
|
+
config.signal?.removeEventListener('abort', abort);
|
|
111
|
+
resolve(value);
|
|
112
|
+
};
|
|
113
|
+
const handle = schedule(complete, duration, [], false);
|
|
114
|
+
if (config.ref === false) handle.unref();
|
|
115
|
+
const abort = () => {
|
|
116
|
+
cancel(handle);
|
|
117
|
+
config.signal.removeEventListener('abort', abort);
|
|
118
|
+
reject(abortError(config.signal.reason));
|
|
119
|
+
};
|
|
120
|
+
config.signal?.addEventListener('abort', abort, {once:true});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function* promiseInterval(duration, value, config = {}) {
|
|
125
|
+
options(config);
|
|
126
|
+
if (config.signal?.aborted) throw abortError(config.signal.reason);
|
|
127
|
+
let count = 0;
|
|
128
|
+
let wake;
|
|
129
|
+
let aborted = false;
|
|
130
|
+
const handle = schedule(() => { count++; wake?.(); }, duration, [], true);
|
|
131
|
+
if (config.ref === false) handle.unref();
|
|
132
|
+
const abort = () => { aborted = true; cancel(handle); wake?.(); };
|
|
133
|
+
config.signal?.addEventListener('abort', abort, {once:true});
|
|
134
|
+
try {
|
|
135
|
+
while (!aborted) {
|
|
136
|
+
if (count === 0) await new Promise(resolve => {wake = resolve;});
|
|
137
|
+
wake = undefined;
|
|
138
|
+
if (aborted) break;
|
|
139
|
+
count--;
|
|
140
|
+
yield value;
|
|
141
|
+
}
|
|
142
|
+
throw abortError(config.signal.reason);
|
|
143
|
+
} finally {
|
|
144
|
+
cancel(handle);
|
|
145
|
+
config.signal?.removeEventListener('abort', abort);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const now = () => epochMillis;
|
|
150
|
+
const VirtualDate = new Proxy(NativeDate, {
|
|
151
|
+
apply() { return new NativeDate(epochMillis).toString(); },
|
|
152
|
+
construct(target, args, newTarget) {
|
|
153
|
+
return Reflect.construct(target, args.length === 0 ? [epochMillis] : args, newTarget);
|
|
154
|
+
},
|
|
155
|
+
get(target, key, receiver) {
|
|
156
|
+
return key === 'now' ? now : Reflect.get(target, key, receiver);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const virtual = {
|
|
160
|
+
setTimeout: (callback, duration, ...args) => schedule(callback, duration, args, false),
|
|
161
|
+
setInterval: (callback, duration, ...args) => schedule(callback, duration, args, true),
|
|
162
|
+
clearTimeout: cancel, clearInterval: cancel,
|
|
163
|
+
};
|
|
164
|
+
Object.defineProperty(virtual.setTimeout, promisify.custom, {value:promiseTimeout});
|
|
165
|
+
|
|
166
|
+
function status() {
|
|
167
|
+
let due = 0;
|
|
168
|
+
for (const handle of queue.values()) if (handle.deadline <= elapsed) due++;
|
|
169
|
+
return {pending:queue.size, due, instantNanos:instant.toString(), elapsedNanos:elapsed.toString()};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
install() {
|
|
174
|
+
if (installed) return;
|
|
175
|
+
installed = true;
|
|
176
|
+
if (date === 'virtual') {
|
|
177
|
+
globalThis.Date = VirtualDate;
|
|
178
|
+
Object.defineProperty(NativeDate.prototype, 'constructor', {...dateConstructor, value:VirtualDate});
|
|
179
|
+
}
|
|
180
|
+
if (timerPolicy === 'virtual') {
|
|
181
|
+
Object.assign(globalThis, virtual);
|
|
182
|
+
Object.assign(timers, virtual);
|
|
183
|
+
promises.setTimeout = promiseTimeout;
|
|
184
|
+
promises.setInterval = promiseInterval;
|
|
185
|
+
syncBuiltinESMExports();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
restore() {
|
|
189
|
+
if (!installed) return;
|
|
190
|
+
installed = false;
|
|
191
|
+
if (date === 'virtual') {
|
|
192
|
+
globalThis.Date = NativeDate;
|
|
193
|
+
Object.defineProperty(NativeDate.prototype, 'constructor', dateConstructor);
|
|
194
|
+
}
|
|
195
|
+
if (timerPolicy === 'virtual') {
|
|
196
|
+
Object.assign(globalThis, native);
|
|
197
|
+
Object.assign(timers, native);
|
|
198
|
+
Object.assign(promises, nativePromises);
|
|
199
|
+
syncBuiltinESMExports();
|
|
200
|
+
queue.clear();
|
|
201
|
+
handles.clear();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
update(value) {
|
|
205
|
+
const next = nanos(value.instantNanos, 'instant');
|
|
206
|
+
const nextMillis = date === 'virtual' ? milliseconds(next) : 0;
|
|
207
|
+
const nextElapsed = value.elapsedNanos === undefined
|
|
208
|
+
? elapsed + (next > instant ? next - instant : 0n)
|
|
209
|
+
: nanos(value.elapsedNanos, 'elapsed');
|
|
210
|
+
if (nextElapsed < elapsed) throw new RangeError('timer elapsed time cannot rewind');
|
|
211
|
+
instant = next;
|
|
212
|
+
epochMillis = nextMillis;
|
|
213
|
+
elapsed = nextElapsed;
|
|
214
|
+
return status();
|
|
215
|
+
},
|
|
216
|
+
async runDue(budget = 1000) {
|
|
217
|
+
if (!Number.isSafeInteger(budget) || budget < 1 || budget > maxDrain) throw new RangeError('invalid timer drain budget');
|
|
218
|
+
if (draining) throw new Error('timer drain is already running');
|
|
219
|
+
draining = true;
|
|
220
|
+
let executed = 0;
|
|
221
|
+
try {
|
|
222
|
+
while (executed < budget) {
|
|
223
|
+
let next;
|
|
224
|
+
for (const handle of queue.values()) {
|
|
225
|
+
if (handle.deadline <= elapsed && (!next || handle.deadline < next.deadline ||
|
|
226
|
+
(handle.deadline === next.deadline && handle.order < next.order))) next = handle;
|
|
227
|
+
}
|
|
228
|
+
if (!next) break;
|
|
229
|
+
queue.delete(next.id);
|
|
230
|
+
handles.delete(next.id);
|
|
231
|
+
if (next.repeat) {
|
|
232
|
+
next.deadline += next.duration;
|
|
233
|
+
queue.set(next.id, next);
|
|
234
|
+
handles.set(next.id, next);
|
|
235
|
+
}
|
|
236
|
+
executed++;
|
|
237
|
+
Reflect.apply(next.callback, next, next.args);
|
|
238
|
+
// Finish Promise and nextTick continuations, without waiting for the callback's
|
|
239
|
+
// returned promise (which may depend on a future virtual timer).
|
|
240
|
+
await new Promise(resolve => nativeImmediate(resolve));
|
|
241
|
+
}
|
|
242
|
+
} finally { draining = false; }
|
|
243
|
+
return {executed,...status()};
|
|
244
|
+
},
|
|
245
|
+
status,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -13,6 +13,9 @@ import { pathToFileURL } from "node:url";
|
|
|
13
13
|
import { existsSync, readFileSync, readlinkSync } from "node:fs";
|
|
14
14
|
import { resolve, join, dirname } from "node:path";
|
|
15
15
|
import { createServer } from "node:http";
|
|
16
|
+
import timers from "node:timers";
|
|
17
|
+
const {setTimeout, clearTimeout} = timers;
|
|
18
|
+
import { createRuntimeClock } from "./clock.mjs";
|
|
16
19
|
import { execFileSync, spawn } from "node:child_process";
|
|
17
20
|
import { url as inspectorUrl } from "node:inspector";
|
|
18
21
|
import { instrumentCallables } from "./callable-app-check.mjs";
|
|
@@ -1522,6 +1525,10 @@ async function invoke(functions, manifest, msg) {
|
|
|
1522
1525
|
|
|
1523
1526
|
|
|
1524
1527
|
async function main() {
|
|
1528
|
+
const clockOptions = JSON.parse(process.env.FIREEMU_CLOCK_JSON ?? '{}');
|
|
1529
|
+
const runtimeClock = createRuntimeClock(clockOptions);
|
|
1530
|
+
let clockRevision = BigInt(clockOptions.revision ?? '0');
|
|
1531
|
+
runtimeClock.install();
|
|
1525
1532
|
let functions;
|
|
1526
1533
|
let manifest;
|
|
1527
1534
|
let runnerReady = false;
|
|
@@ -1538,6 +1545,25 @@ async function main() {
|
|
|
1538
1545
|
finishOutput();
|
|
1539
1546
|
return false;
|
|
1540
1547
|
}
|
|
1548
|
+
if (msg.type === 'clock:set' || msg.type === 'clock:runDue') {
|
|
1549
|
+
void (async () => {
|
|
1550
|
+
try {
|
|
1551
|
+
let status;
|
|
1552
|
+
if (msg.type === 'clock:set') {
|
|
1553
|
+
const revision = BigInt(msg.revision);
|
|
1554
|
+
if (revision < clockRevision) throw new Error('stale clock revision');
|
|
1555
|
+
status = runtimeClock.update(msg);
|
|
1556
|
+
clockRevision = revision;
|
|
1557
|
+
} else {
|
|
1558
|
+
status = await runtimeClock.runDue(msg.budget);
|
|
1559
|
+
}
|
|
1560
|
+
send({type:'result',invocationId:msg.invocationId,ok:true,timers:status});
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
send({type:'result',invocationId:msg.invocationId,ok:false,error:invocationFailure(error).message});
|
|
1563
|
+
}
|
|
1564
|
+
})();
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1541
1567
|
if (!runnerReady) {
|
|
1542
1568
|
process.stderr.write('[functions] invocation received before runner hello\n');
|
|
1543
1569
|
process.exit(2);
|
|
@@ -1670,6 +1696,7 @@ async function main() {
|
|
|
1670
1696
|
send({
|
|
1671
1697
|
type: "hello",
|
|
1672
1698
|
runner: "node",
|
|
1699
|
+
clock: {version:1,date:true,timers:true},
|
|
1673
1700
|
codebase: args.codebase,
|
|
1674
1701
|
version: process.version,
|
|
1675
1702
|
inspectorPort: activeInspectorPort,
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Limits are local resource policies, not Firebase quotas. They count encoded
|
|
3
3
|
// pending frames (including the stream's in-flight write), not process-wide RSS.
|
|
4
4
|
import { performance } from 'node:perf_hooks';
|
|
5
|
+
import timers from 'node:timers';
|
|
6
|
+
const {setTimeout, clearTimeout} = timers;
|
|
5
7
|
import { MAX_FRAME_BYTES } from './protocol.mjs';
|
|
6
8
|
|
|
7
9
|
export const MAX_PENDING_BYTES = 32 * 1024 * 1024;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// This is a trusted local pipe, not an authentication or arbitrary-code sandbox.
|
|
4
4
|
import { TextDecoder } from 'node:util';
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
|
-
import
|
|
6
|
+
import timers from 'node:timers';
|
|
7
|
+
const {setTimeout, clearTimeout} = timers;
|
|
7
8
|
|
|
8
9
|
export const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
9
10
|
// Local pipe safety policies, not Firebase invocation deadlines or quotas.
|
|
@@ -37,6 +38,12 @@ function decodeMessage(payload) {
|
|
|
37
38
|
}
|
|
38
39
|
if (!object(message)) throw new ProtocolError('message object');
|
|
39
40
|
if (message.type === 'shutdown') return message;
|
|
41
|
+
const decimal = (value, signed = false) => typeof value === 'string' && value.length <= 40 &&
|
|
42
|
+
(signed ? /^(?:0|-?[1-9][0-9]*)$/ : /^(?:0|[1-9][0-9]*)$/).test(value);
|
|
43
|
+
if (message.type === 'clock:set' && text(message.invocationId) &&
|
|
44
|
+
decimal(message.instantNanos, true) && decimal(message.elapsedNanos) && decimal(message.revision)) return message;
|
|
45
|
+
if (message.type === 'clock:runDue' && text(message.invocationId) &&
|
|
46
|
+
(message.budget === undefined || (Number.isSafeInteger(message.budget) && message.budget >= 1 && message.budget <= 10000))) return message;
|
|
40
47
|
if (message.type !== 'invoke' || !text(message.invocationId) ||
|
|
41
48
|
!text(message.function) || !text(message.trigger) || !object(message.event) ||
|
|
42
49
|
(message.entryPoint !== undefined && !text(message.entryPoint))) {
|