713 lines
34 KiB
JavaScript
713 lines
34 KiB
JavaScript
// Thin polyfill: all real logic lives in the Zig WASM module. JS only handles
|
|
// ECMAScript-spec coercion rules (which the host language must enforce) and
|
|
// surface plumbing (brand checks, prototypes, descriptors, error mapping).
|
|
(function () {
|
|
if (!globalThis.__TEMPORALZ_WASM_BYTES__) {
|
|
throw new Error("WASM bytes not injected into test context");
|
|
}
|
|
|
|
const wasmModule = new WebAssembly.Module(globalThis.__TEMPORALZ_WASM_BYTES__);
|
|
const wasmInstance = new WebAssembly.Instance(wasmModule, {
|
|
env: { console: (ptr, len) => globalThis.console.log(decodeUtf8(memory.buffer, ptr, len)) },
|
|
});
|
|
|
|
const x = wasmInstance.exports;
|
|
const memory = x.memory;
|
|
|
|
// --- utf8 codec (vm.Script context has no TextEncoder) -----------------
|
|
function encodeUtf8(str) {
|
|
const buf = [];
|
|
for (let i = 0; i < str.length; i++) {
|
|
const c = str.charCodeAt(i);
|
|
if (c < 0x80) buf.push(c);
|
|
else if (c < 0x800) buf.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
else if (c < 0xd800 || c >= 0xe000) buf.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
else {
|
|
const c2 = str.charCodeAt(++i);
|
|
const cp = 0x10000 + (((c & 0x3ff) << 10) | (c2 & 0x3ff));
|
|
buf.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
|
|
}
|
|
}
|
|
return new Uint8Array(buf);
|
|
}
|
|
function decodeUtf8(buf, ptr, len) {
|
|
const bytes = new Uint8Array(buf, ptr, len);
|
|
let s = "";
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
const b = bytes[i];
|
|
if (b < 0x80) s += String.fromCharCode(b);
|
|
else if ((b & 0xe0) === 0xc0) s += String.fromCharCode(((b & 0x1f) << 6) | (bytes[++i] & 0x3f));
|
|
else if ((b & 0xf0) === 0xe0) s += String.fromCharCode(((b & 0x0f) << 12) | ((bytes[++i] & 0x3f) << 6) | (bytes[++i] & 0x3f));
|
|
else {
|
|
const cp = ((b & 0x07) << 18) | ((bytes[++i] & 0x3f) << 12) | ((bytes[++i] & 0x3f) << 6) | (bytes[++i] & 0x3f);
|
|
const hi = ((cp - 0x10000) >> 10) + 0xd800;
|
|
const lo = ((cp - 0x10000) & 0x3ff) + 0xdc00;
|
|
s += String.fromCharCode(hi, lo);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// --- error mapping using Zig's reported error kind ---------------------
|
|
function lastError() {
|
|
const kind = Number(x.temporalz_last_error_kind());
|
|
const ptr = Number(x.temporalz_last_error_ptr());
|
|
const len = Number(x.temporalz_last_error_len());
|
|
const msg = ptr && len ? decodeUtf8(memory.buffer, ptr, len) : "temporalz error";
|
|
x.temporalz_last_error_clear();
|
|
switch (kind) {
|
|
case 2: return new TypeError(msg);
|
|
case 3: return new RangeError(msg);
|
|
case 4: return new SyntaxError(msg);
|
|
default: return new RangeError(msg);
|
|
}
|
|
}
|
|
function need(h) {
|
|
if (!h) throw lastError();
|
|
return Number(h);
|
|
}
|
|
function takeString(packed) {
|
|
if (!packed) throw lastError();
|
|
const v = BigInt(packed);
|
|
const ptr = Number(v >> 32n);
|
|
const len = Number(v & 0xffffffffn);
|
|
const text = decodeUtf8(memory.buffer, ptr, len);
|
|
x.temporalz_string_free(ptr, len);
|
|
return text;
|
|
}
|
|
function withCString(str, fn) {
|
|
const bytes = encodeUtf8(str);
|
|
const ptr = Number(x.temporalz_alloc(bytes.length));
|
|
if (!ptr && bytes.length) throw lastError();
|
|
if (bytes.length) new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
|
|
try { return fn(ptr, bytes.length); }
|
|
finally { if (bytes.length) x.temporalz_free(ptr, bytes.length); }
|
|
}
|
|
|
|
// --- enums (must match wasm.zig codes) ---------------------------------
|
|
const UNIT_CODES = {
|
|
nanosecond: 1, nanoseconds: 1,
|
|
microsecond: 2, microseconds: 2,
|
|
millisecond: 3, milliseconds: 3,
|
|
second: 4, seconds: 4,
|
|
minute: 5, minutes: 5,
|
|
hour: 6, hours: 6,
|
|
day: 7, days: 7,
|
|
week: 8, weeks: 8,
|
|
month: 9, months: 9,
|
|
year: 10, years: 10,
|
|
auto: 11,
|
|
};
|
|
const ROUNDING_MODE_CODES = { ceil: 1, floor: 2, expand: 3, trunc: 4, halfCeil: 5, halfFloor: 6, halfExpand: 7, halfTrunc: 8, halfEven: 9 };
|
|
|
|
function toStringOrThrow(v, name) {
|
|
if (typeof v === "symbol") throw new TypeError(`Cannot convert symbol to string for ${name}`);
|
|
return String(v);
|
|
}
|
|
function unitCode(value) {
|
|
if (value === undefined) return 255;
|
|
const s = toStringOrThrow(value, "unit");
|
|
const c = UNIT_CODES[s];
|
|
if (c === undefined) throw new RangeError(`Invalid unit: ${s}`);
|
|
return c;
|
|
}
|
|
function roundingModeCode(value) {
|
|
if (value === undefined) return 255;
|
|
const s = toStringOrThrow(value, "roundingMode");
|
|
const c = ROUNDING_MODE_CODES[s];
|
|
if (c === undefined) throw new RangeError(`Invalid roundingMode: ${s}`);
|
|
return c;
|
|
}
|
|
function roundingIncrementValue(opts) {
|
|
if (!opts || opts.roundingIncrement === undefined) return 0;
|
|
const v = opts.roundingIncrement;
|
|
if (typeof v === "bigint") throw new TypeError("Cannot convert BigInt to number for roundingIncrement");
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) throw new RangeError("Invalid roundingIncrement");
|
|
const i = Math.trunc(n);
|
|
if (i < 1 || i > 1e9) throw new RangeError("Invalid roundingIncrement");
|
|
return i;
|
|
}
|
|
function requireOptionsObject(o, name) {
|
|
if (o === undefined) return {};
|
|
if (o === null) throw new TypeError(`${name} options cannot be null`);
|
|
if (typeof o !== "object" && typeof o !== "function") {
|
|
throw new TypeError(`${name} options must be an object`);
|
|
}
|
|
return o;
|
|
}
|
|
|
|
// --- BigInt i128 split for the wasm boundary ---------------------------
|
|
function splitI128(value) {
|
|
const v = typeof value === "bigint" ? value : BigInt(value);
|
|
return { hi: v >> 64n, lo: v & 0xffffffffffffffffn };
|
|
}
|
|
function joinI128(hi, lo) {
|
|
return (BigInt(hi) << 64n) | (BigInt(lo) & 0xffffffffffffffffn);
|
|
}
|
|
|
|
// --- brand-check helpers -----------------------------------------------
|
|
const HANDLE = Symbol("temporalz.handle");
|
|
function brand(obj, klass, name) {
|
|
if (typeof obj !== "object" || obj === null || !klass.prototype.isPrototypeOf(obj)) {
|
|
throw new TypeError(`this is not a ${name}`);
|
|
}
|
|
const h = obj[HANDLE];
|
|
if (h === undefined) throw new TypeError(`this is not a ${name} (missing brand)`);
|
|
return h;
|
|
}
|
|
|
|
function defineMethods(proto, methods) {
|
|
for (const name of Object.keys(methods)) {
|
|
Object.defineProperty(proto, name, {
|
|
value: methods[name],
|
|
writable: true,
|
|
enumerable: false,
|
|
configurable: true,
|
|
});
|
|
}
|
|
}
|
|
function defineGetters(proto, getters) {
|
|
for (const name of Object.keys(getters)) {
|
|
Object.defineProperty(proto, name, {
|
|
get: getters[name],
|
|
enumerable: false,
|
|
configurable: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
// =======================================================================
|
|
// Instant
|
|
// =======================================================================
|
|
const NS_LIMIT = 8_640_000_000_000_000_000_000n;
|
|
function Instant(epochNanoseconds) {
|
|
if (!new.target) throw new TypeError("Instant must be called with new");
|
|
if (typeof epochNanoseconds !== "bigint") {
|
|
throw new TypeError("epochNanoseconds must be a BigInt");
|
|
}
|
|
if (epochNanoseconds > NS_LIMIT || epochNanoseconds < -NS_LIMIT) {
|
|
throw new RangeError("epochNanoseconds out of range");
|
|
}
|
|
const { hi, lo } = splitI128(epochNanoseconds);
|
|
const handle = need(x.temporalz_instant_from_epoch_nanoseconds_parts(hi, lo));
|
|
Object.defineProperty(this, HANDLE, { value: handle });
|
|
}
|
|
function instantFromHandle(handle) {
|
|
const obj = Object.create(Instant.prototype);
|
|
Object.defineProperty(obj, HANDLE, { value: handle });
|
|
return obj;
|
|
}
|
|
function isInstant(v) {
|
|
return typeof v === "object" && v !== null && Instant.prototype.isPrototypeOf(v) && v[HANDLE] !== undefined;
|
|
}
|
|
|
|
// Static methods
|
|
Object.defineProperty(Instant, "from", {
|
|
value: { from(item) {
|
|
if (isInstant(item)) return new Instant(getInstantEpochNs(item));
|
|
return parseInstantString(coerceToInstantString(item));
|
|
} }.from,
|
|
writable: true, enumerable: false, configurable: true,
|
|
});
|
|
Object.defineProperty(Instant, "fromEpochMilliseconds", {
|
|
value: { fromEpochMilliseconds(epochMilliseconds) {
|
|
if (typeof epochMilliseconds === "bigint") throw new TypeError("epochMilliseconds must not be BigInt");
|
|
const n = Number(epochMilliseconds);
|
|
if (!Number.isFinite(n)) throw new RangeError("epochMilliseconds must be a finite integer");
|
|
if (!Number.isInteger(n)) throw new RangeError("epochMilliseconds must be an integer");
|
|
const MAX = 8.64e15;
|
|
if (n > MAX || n < -MAX) throw new RangeError("epochMilliseconds out of range");
|
|
return instantFromHandle(need(x.temporalz_instant_from_epoch_milliseconds(BigInt(n))));
|
|
} }.fromEpochMilliseconds,
|
|
writable: true, enumerable: false, configurable: true,
|
|
});
|
|
Object.defineProperty(Instant, "fromEpochNanoseconds", {
|
|
value: { fromEpochNanoseconds(epochNanoseconds) {
|
|
if (typeof epochNanoseconds !== "bigint") throw new TypeError("epochNanoseconds must be a BigInt");
|
|
const MAX = 8_640_000_000_000_000_000_000n;
|
|
if (epochNanoseconds > MAX || epochNanoseconds < -MAX) throw new RangeError("epochNanoseconds out of range");
|
|
const { hi, lo } = splitI128(epochNanoseconds);
|
|
return instantFromHandle(need(x.temporalz_instant_from_epoch_nanoseconds_parts(hi, lo)));
|
|
} }.fromEpochNanoseconds,
|
|
writable: true, enumerable: false, configurable: true,
|
|
});
|
|
Object.defineProperty(Instant, "compare", {
|
|
value: { compare(a, b) {
|
|
const ha = toInstantHandle(a);
|
|
const hb = toInstantHandle(b);
|
|
return Number(x.temporalz_instant_compare(ha, hb));
|
|
} }.compare,
|
|
writable: true, enumerable: false, configurable: true,
|
|
});
|
|
|
|
function parseInstantString(str) {
|
|
return withCString(str, (ptr, len) => instantFromHandle(need(x.temporalz_instant_from_utf8(ptr, len))));
|
|
}
|
|
// Spec: ToTemporalInstant. If Instant, copy. Else, ToPrimitive(string-hint) then
|
|
// parse as ISO string. bigint/symbol/Temporal.Instant.prototype branding fail → TypeError.
|
|
function coerceToInstantString(value) {
|
|
if (value === undefined || value === null) throw new TypeError("Instant argument is required");
|
|
if (typeof value === "boolean") throw new TypeError("Boolean is not a valid Instant");
|
|
if (typeof value === "number") throw new TypeError("Number is not a valid Instant");
|
|
if (typeof value === "bigint") throw new TypeError("BigInt is not a valid Instant");
|
|
if (typeof value === "symbol") throw new TypeError("Symbol is not a valid Instant");
|
|
if (typeof value === "string") return value;
|
|
// Object case: must not be an Instant.prototype (brand fails) — but ordinary object → ToString → "[object Object]" parsed → RangeError
|
|
if (value === Instant.prototype) throw new TypeError("Instant.prototype is not a valid Instant");
|
|
return String(value);
|
|
}
|
|
function toInstantHandle(value) {
|
|
if (isInstant(value)) return value[HANDLE];
|
|
const str = coerceToInstantString(value);
|
|
return withCString(str, (ptr, len) => need(x.temporalz_instant_from_utf8(ptr, len)));
|
|
}
|
|
function getInstantEpochNs(inst) {
|
|
const h = inst[HANDLE];
|
|
const hi = x.temporalz_instant_epoch_nanoseconds_hi(h);
|
|
const lo = x.temporalz_instant_epoch_nanoseconds_lo(h);
|
|
return joinI128(hi, lo);
|
|
}
|
|
|
|
defineGetters(Instant.prototype, {
|
|
epochMilliseconds() {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
return Number(x.temporalz_instant_epoch_milliseconds(h));
|
|
},
|
|
epochNanoseconds() {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
const hi = x.temporalz_instant_epoch_nanoseconds_hi(h);
|
|
const lo = x.temporalz_instant_epoch_nanoseconds_lo(h);
|
|
return joinI128(hi, lo);
|
|
},
|
|
});
|
|
|
|
defineMethods(Instant.prototype, {
|
|
add(durationLike) {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
const dh = toDurationHandle(durationLike);
|
|
return instantFromHandle(need(x.temporalz_instant_add(h, dh)));
|
|
},
|
|
subtract(durationLike) {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
const dh = toDurationHandle(durationLike);
|
|
return instantFromHandle(need(x.temporalz_instant_subtract(h, dh)));
|
|
},
|
|
until(other, options) {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
const oh = toInstantHandle(other);
|
|
const o = requireOptionsObject(options, "until");
|
|
return durationFromHandle(need(x.temporalz_instant_until(
|
|
h, oh,
|
|
unitCode(o.largestUnit),
|
|
unitCode(o.smallestUnit),
|
|
roundingModeCode(o.roundingMode),
|
|
roundingIncrementValue(o),
|
|
)));
|
|
},
|
|
since(other, options) {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
const oh = toInstantHandle(other);
|
|
const o = requireOptionsObject(options, "since");
|
|
return durationFromHandle(need(x.temporalz_instant_since(
|
|
h, oh,
|
|
unitCode(o.largestUnit),
|
|
unitCode(o.smallestUnit),
|
|
roundingModeCode(o.roundingMode),
|
|
roundingIncrementValue(o),
|
|
)));
|
|
},
|
|
round(options) {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
let o;
|
|
if (typeof options === "string") o = { smallestUnit: options };
|
|
else if (options === undefined) throw new TypeError("round options required");
|
|
else o = requireOptionsObject(options, "round");
|
|
const su = unitCode(o.smallestUnit);
|
|
if (su === 255) throw new RangeError("smallestUnit is required");
|
|
return instantFromHandle(need(x.temporalz_instant_round(
|
|
h, su, roundingModeCode(o.roundingMode), roundingIncrementValue(o),
|
|
)));
|
|
},
|
|
equals(other) {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
const oh = toInstantHandle(other);
|
|
return x.temporalz_instant_equals(h, oh) === 1;
|
|
},
|
|
toString(options) {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
const o = requireOptionsObject(options, "toString");
|
|
const su = unitCode(o.smallestUnit);
|
|
const rm = roundingModeCode(o.roundingMode);
|
|
let fd = -1;
|
|
if (o.fractionalSecondDigits !== undefined) {
|
|
const v = o.fractionalSecondDigits;
|
|
if (typeof v === "symbol") throw new TypeError("Cannot convert Symbol to fractionalSecondDigits");
|
|
if (typeof v === "bigint") throw new TypeError("Cannot convert BigInt to number for fractionalSecondDigits");
|
|
if (typeof v === "number") {
|
|
if (!Number.isFinite(v)) throw new RangeError("Invalid fractionalSecondDigits");
|
|
const i = Math.floor(v);
|
|
if (i < 0 || i > 9) throw new RangeError("Invalid fractionalSecondDigits");
|
|
fd = i;
|
|
} else {
|
|
// ToString path; only "auto" is acceptable
|
|
const s = String(v);
|
|
if (s !== "auto") throw new RangeError("Invalid fractionalSecondDigits");
|
|
fd = -1;
|
|
}
|
|
}
|
|
let tzPtr = 0, tzLen = 0;
|
|
const tz = o.timeZone;
|
|
if (tz !== undefined) {
|
|
if (typeof tz !== "string") {
|
|
// primitive bool/number/bigint convert to a string but won't be a valid time zone → RangeError
|
|
// null/symbol/object → TypeError per spec
|
|
if (tz === null) throw new TypeError("timeZone must be a string");
|
|
if (typeof tz === "object") throw new TypeError("timeZone must be a string");
|
|
if (typeof tz === "symbol") throw new TypeError("Cannot convert Symbol to string for timeZone");
|
|
// boolean/number/bigint → string conversion, then range check happens via Zig parse
|
|
}
|
|
const tzStr = typeof tz === "string" ? tz : String(tz);
|
|
const bytes = encodeUtf8(tzStr);
|
|
tzPtr = Number(x.temporalz_alloc(bytes.length));
|
|
if (!tzPtr && bytes.length) throw lastError();
|
|
if (bytes.length) new Uint8Array(memory.buffer, tzPtr, bytes.length).set(bytes);
|
|
tzLen = bytes.length;
|
|
}
|
|
try {
|
|
return takeString(x.temporalz_instant_to_string_with(h, su, rm, fd, tzPtr, tzLen));
|
|
} finally {
|
|
if (tzLen) x.temporalz_free(tzPtr, tzLen);
|
|
}
|
|
},
|
|
toJSON() {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
return takeString(x.temporalz_instant_to_string(h));
|
|
},
|
|
toLocaleString() {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
return takeString(x.temporalz_instant_to_string(h));
|
|
},
|
|
valueOf() {
|
|
throw new TypeError("Cannot convert Temporal.Instant to a primitive value");
|
|
},
|
|
toZonedDateTimeISO(timeZone) {
|
|
const h = brand(this, Instant, "Temporal.Instant");
|
|
if (typeof timeZone !== "string") throw new TypeError("timeZone must be a string");
|
|
return withCString(timeZone, (ptr, len) => {
|
|
const zh = need(x.temporalz_instant_to_zoned_date_time_iso(h, ptr, len));
|
|
return zonedDateTimeFromHandle(zh);
|
|
});
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Instant.prototype, Symbol.toStringTag, {
|
|
value: "Temporal.Instant", writable: false, enumerable: false, configurable: true,
|
|
});
|
|
Object.defineProperty(Instant, "prototype", { writable: false, enumerable: false, configurable: false });
|
|
|
|
// =======================================================================
|
|
// ZonedDateTime (minimal: only what Instant.toZonedDateTimeISO returns)
|
|
// =======================================================================
|
|
function ZonedDateTime() {
|
|
throw new TypeError("Use Temporal.Instant.prototype.toZonedDateTimeISO instead");
|
|
}
|
|
function zonedDateTimeFromHandle(handle) {
|
|
const obj = Object.create(ZonedDateTime.prototype);
|
|
Object.defineProperty(obj, HANDLE, { value: handle });
|
|
return obj;
|
|
}
|
|
defineGetters(ZonedDateTime.prototype, {
|
|
epochNanoseconds() {
|
|
const h = brand(this, ZonedDateTime, "Temporal.ZonedDateTime");
|
|
const hi = x.temporalz_zoned_date_time_epoch_nanoseconds_hi(h);
|
|
const lo = x.temporalz_zoned_date_time_epoch_nanoseconds_lo(h);
|
|
return joinI128(hi, lo);
|
|
},
|
|
});
|
|
Object.defineProperty(ZonedDateTime.prototype, Symbol.toStringTag, {
|
|
value: "Temporal.ZonedDateTime", writable: false, enumerable: false, configurable: true,
|
|
});
|
|
|
|
// =======================================================================
|
|
// Duration
|
|
// =======================================================================
|
|
function Duration(years, months, weeks, days, hours, minutes, seconds, ms, us, ns) {
|
|
if (!new.target) throw new TypeError("Duration must be called with new");
|
|
const Y = intArg(years, "years");
|
|
const Mo = intArg(months, "months");
|
|
const W = intArg(weeks, "weeks");
|
|
const D = intArg(days, "days");
|
|
const H = intArg(hours, "hours");
|
|
const Mi = intArg(minutes, "minutes");
|
|
const S = intArg(seconds, "seconds");
|
|
const MS = intArg(ms, "milliseconds");
|
|
const US = numArg(us, "microseconds");
|
|
const NS = numArg(ns, "nanoseconds");
|
|
const handle = need(x.temporalz_duration_init(Y, Mo, W, D, H, Mi, S, MS, US, NS));
|
|
Object.defineProperty(this, HANDLE, { value: handle });
|
|
}
|
|
function intArg(v, name) {
|
|
if (v === undefined) return 0n;
|
|
if (typeof v === "bigint") return v;
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) throw new RangeError(`${name} must be finite`);
|
|
if (!Number.isInteger(n)) throw new RangeError(`${name} must be an integer`);
|
|
return BigInt(n);
|
|
}
|
|
function numArg(v, name) {
|
|
if (v === undefined) return 0;
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) throw new RangeError(`${name} must be finite`);
|
|
if (!Number.isInteger(n)) throw new RangeError(`${name} must be an integer`);
|
|
return n;
|
|
}
|
|
function durationFromHandle(handle) {
|
|
const obj = Object.create(Duration.prototype);
|
|
Object.defineProperty(obj, HANDLE, { value: handle });
|
|
return obj;
|
|
}
|
|
function isDuration(v) {
|
|
return typeof v === "object" && v !== null && Duration.prototype.isPrototypeOf(v) && v[HANDLE] !== undefined;
|
|
}
|
|
function toDurationHandle(value) {
|
|
if (isDuration(value)) return value[HANDLE];
|
|
if (typeof value === "string") {
|
|
return withCString(value, (ptr, len) => need(x.temporalz_duration_from_utf8(ptr, len)));
|
|
}
|
|
if (value === null || typeof value !== "object") {
|
|
throw new TypeError("expected a Duration, string, or object");
|
|
}
|
|
// Wasm-side parts order (matches mask bits): years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds
|
|
const wireOrder = ["years","months","weeks","days","hours","minutes","seconds","milliseconds","microseconds","nanoseconds"];
|
|
// Spec: properties are read in alphabetical order.
|
|
const readOrder = ["days","hours","microseconds","milliseconds","minutes","months","nanoseconds","seconds","weeks","years"];
|
|
const vals = [0n,0n,0n,0n,0n,0n,0n,0n,0,0];
|
|
let mask = 0;
|
|
let any = false;
|
|
for (const f of readOrder) {
|
|
const raw = value[f];
|
|
if (raw === undefined) continue;
|
|
any = true;
|
|
const i = wireOrder.indexOf(f);
|
|
mask |= 1 << i;
|
|
vals[i] = (f === "microseconds" || f === "nanoseconds") ? numArg(raw, f) : intArg(raw, f);
|
|
}
|
|
if (!any) throw new TypeError("Duration object must have at least one duration field");
|
|
return need(x.temporalz_duration_from_parts(mask, vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6], vals[7], vals[8], vals[9]));
|
|
}
|
|
Object.defineProperty(Duration, "from", {
|
|
value: function from(value) {
|
|
if (isDuration(value)) return durationFromHandle(need(x.temporalz_duration_init(
|
|
x.temporalz_duration_years(value[HANDLE]),
|
|
x.temporalz_duration_months(value[HANDLE]),
|
|
x.temporalz_duration_weeks(value[HANDLE]),
|
|
x.temporalz_duration_days(value[HANDLE]),
|
|
x.temporalz_duration_hours(value[HANDLE]),
|
|
x.temporalz_duration_minutes(value[HANDLE]),
|
|
x.temporalz_duration_seconds(value[HANDLE]),
|
|
x.temporalz_duration_milliseconds(value[HANDLE]),
|
|
x.temporalz_duration_microseconds(value[HANDLE]),
|
|
x.temporalz_duration_nanoseconds(value[HANDLE]),
|
|
)));
|
|
return durationFromHandle(toDurationHandle(value));
|
|
},
|
|
writable: true, enumerable: false, configurable: true,
|
|
});
|
|
Object.defineProperty(Duration, "compare", {
|
|
value: function compare(a, b, options) {
|
|
const ah = toDurationHandle(a);
|
|
const bh = toDurationHandle(b);
|
|
const relativeTo = options && options.relativeTo;
|
|
if (relativeTo !== undefined && relativeTo !== null) {
|
|
const date = plainDateFromAny(relativeTo);
|
|
return Number(x.temporalz_duration_compare_plain_date(ah, bh, date[HANDLE]));
|
|
}
|
|
return Number(x.temporalz_duration_compare(ah, bh));
|
|
},
|
|
writable: true, enumerable: false, configurable: true,
|
|
});
|
|
|
|
defineGetters(Duration.prototype, {
|
|
years() { return Number(x.temporalz_duration_years(brand(this, Duration, "Temporal.Duration"))); },
|
|
months() { return Number(x.temporalz_duration_months(brand(this, Duration, "Temporal.Duration"))); },
|
|
weeks() { return Number(x.temporalz_duration_weeks(brand(this, Duration, "Temporal.Duration"))); },
|
|
days() { return Number(x.temporalz_duration_days(brand(this, Duration, "Temporal.Duration"))); },
|
|
hours() { return Number(x.temporalz_duration_hours(brand(this, Duration, "Temporal.Duration"))); },
|
|
minutes() { return Number(x.temporalz_duration_minutes(brand(this, Duration, "Temporal.Duration"))); },
|
|
seconds() { return Number(x.temporalz_duration_seconds(brand(this, Duration, "Temporal.Duration"))); },
|
|
milliseconds() { return Number(x.temporalz_duration_milliseconds(brand(this, Duration, "Temporal.Duration"))); },
|
|
microseconds() { return Number(x.temporalz_duration_microseconds(brand(this, Duration, "Temporal.Duration"))); },
|
|
nanoseconds() { return Number(x.temporalz_duration_nanoseconds(brand(this, Duration, "Temporal.Duration"))); },
|
|
sign() { return Number(x.temporalz_duration_sign(brand(this, Duration, "Temporal.Duration"))); },
|
|
blank() { return x.temporalz_duration_blank(brand(this, Duration, "Temporal.Duration")) === 1; },
|
|
});
|
|
|
|
defineMethods(Duration.prototype, {
|
|
add(other) {
|
|
const h = brand(this, Duration, "Temporal.Duration");
|
|
return durationFromHandle(need(x.temporalz_duration_add(h, toDurationHandle(other))));
|
|
},
|
|
subtract(other) {
|
|
const h = brand(this, Duration, "Temporal.Duration");
|
|
return durationFromHandle(need(x.temporalz_duration_subtract(h, toDurationHandle(other))));
|
|
},
|
|
abs() {
|
|
const h = brand(this, Duration, "Temporal.Duration");
|
|
return durationFromHandle(need(x.temporalz_duration_abs(h)));
|
|
},
|
|
negated() {
|
|
const h = brand(this, Duration, "Temporal.Duration");
|
|
return durationFromHandle(need(x.temporalz_duration_negated(h)));
|
|
},
|
|
round(options) {
|
|
const h = brand(this, Duration, "Temporal.Duration");
|
|
const o = options ?? {};
|
|
if (o === null || typeof o !== "object") throw new TypeError("round options must be an object");
|
|
const relativeTo = o.relativeTo;
|
|
if (relativeTo !== undefined && relativeTo !== null) {
|
|
const date = plainDateFromAny(relativeTo);
|
|
return durationFromHandle(need(x.temporalz_duration_round_plain_date(
|
|
h, unitCode(o.smallestUnit), unitCode(o.largestUnit), roundingModeCode(o.roundingMode), roundingIncrementValue(o), date[HANDLE],
|
|
)));
|
|
}
|
|
return durationFromHandle(need(x.temporalz_duration_round(
|
|
h, unitCode(o.smallestUnit), unitCode(o.largestUnit), roundingModeCode(o.roundingMode), roundingIncrementValue(o),
|
|
)));
|
|
},
|
|
total(options) {
|
|
const h = brand(this, Duration, "Temporal.Duration");
|
|
if (!options || typeof options !== "object") throw new TypeError("total options must be an object");
|
|
const unit = unitCode(options.unit);
|
|
if (unit === 255) throw new RangeError("unit is required");
|
|
const relativeTo = options.relativeTo;
|
|
if (relativeTo !== undefined && relativeTo !== null) {
|
|
const date = plainDateFromAny(relativeTo);
|
|
return x.temporalz_duration_total_plain_date(h, unit, date[HANDLE]);
|
|
}
|
|
return x.temporalz_duration_total(h, unit);
|
|
},
|
|
toString() {
|
|
return takeString(x.temporalz_duration_to_string(brand(this, Duration, "Temporal.Duration")));
|
|
},
|
|
toJSON() {
|
|
return takeString(x.temporalz_duration_to_string(brand(this, Duration, "Temporal.Duration")));
|
|
},
|
|
toLocaleString() {
|
|
return takeString(x.temporalz_duration_to_string(brand(this, Duration, "Temporal.Duration")));
|
|
},
|
|
valueOf() {
|
|
throw new TypeError("Cannot convert Temporal.Duration to a primitive value");
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Duration.prototype, Symbol.toStringTag, {
|
|
value: "Temporal.Duration", writable: false, enumerable: false, configurable: true,
|
|
});
|
|
Object.defineProperty(Duration, "prototype", { writable: false, enumerable: false, configurable: false });
|
|
|
|
// =======================================================================
|
|
// PlainDate (minimal — used as relativeTo for Duration)
|
|
// =======================================================================
|
|
function PlainDate(year, month, day) {
|
|
if (!new.target) throw new TypeError("PlainDate must be called with new");
|
|
const Y = Number(year), M = Number(month), D = Number(day);
|
|
if (!Number.isInteger(Y) || !Number.isInteger(M) || !Number.isInteger(D)) {
|
|
throw new RangeError("PlainDate fields must be integers");
|
|
}
|
|
const handle = need(x.temporalz_plain_date_init(Y, M, D));
|
|
Object.defineProperty(this, HANDLE, { value: handle });
|
|
}
|
|
function plainDateFromHandle(handle) {
|
|
const obj = Object.create(PlainDate.prototype);
|
|
Object.defineProperty(obj, HANDLE, { value: handle });
|
|
return obj;
|
|
}
|
|
function isPlainDate(v) {
|
|
return typeof v === "object" && v !== null && PlainDate.prototype.isPrototypeOf(v) && v[HANDLE] !== undefined;
|
|
}
|
|
function plainDateFromAny(value) {
|
|
if (isPlainDate(value)) return value;
|
|
if (typeof value === "string") {
|
|
return withCString(value, (ptr, len) => plainDateFromHandle(need(x.temporalz_plain_date_from_utf8(ptr, len))));
|
|
}
|
|
if (value === null || typeof value !== "object") throw new TypeError("Invalid relativeTo");
|
|
const Y = Number(value.year), M = Number(value.month), D = Number(value.day);
|
|
if (!Number.isInteger(Y) || !Number.isInteger(M) || !Number.isInteger(D)) {
|
|
throw new RangeError("Invalid relativeTo fields");
|
|
}
|
|
return plainDateFromHandle(need(x.temporalz_plain_date_init(Y, M, D)));
|
|
}
|
|
Object.defineProperty(PlainDate, "from", {
|
|
value: function from(value) { return plainDateFromAny(value); },
|
|
writable: true, enumerable: false, configurable: true,
|
|
});
|
|
defineMethods(PlainDate.prototype, {
|
|
toString() { return takeString(x.temporalz_plain_date_to_string(brand(this, PlainDate, "Temporal.PlainDate"))); },
|
|
});
|
|
Object.defineProperty(PlainDate.prototype, Symbol.toStringTag, {
|
|
value: "Temporal.PlainDate", writable: false, enumerable: false, configurable: true,
|
|
});
|
|
|
|
// =======================================================================
|
|
// Unimplemented stubs
|
|
// =======================================================================
|
|
function notImplemented(name) {
|
|
return function () { throw new Error(`${name} is not implemented`); };
|
|
}
|
|
|
|
const TemporalNow = {};
|
|
defineMethods(TemporalNow, {
|
|
instant() {
|
|
// Delegate creation semantics to the Zig-backed Instant constructor path.
|
|
return Instant.fromEpochMilliseconds(Date.now());
|
|
},
|
|
plainDateISO() {
|
|
throw new Error("Temporal.Now.plainDateISO is not implemented");
|
|
},
|
|
plainDateTimeISO() {
|
|
throw new Error("Temporal.Now.plainDateTimeISO is not implemented");
|
|
},
|
|
plainTimeISO() {
|
|
throw new Error("Temporal.Now.plainTimeISO is not implemented");
|
|
},
|
|
timeZoneId() {
|
|
// Keep this deterministic for now; Zig Now.zig currently returns UTC too.
|
|
return "UTC";
|
|
},
|
|
zonedDateTimeISO() {
|
|
const tz = arguments.length === 0 || arguments[0] === undefined ? TemporalNow.timeZoneId() : arguments[0];
|
|
if (tz === null) throw new TypeError("timeZone must be a string");
|
|
if (typeof tz === "symbol") throw new TypeError("Cannot convert Symbol to string for timeZone");
|
|
return TemporalNow.instant().toZonedDateTimeISO(String(tz));
|
|
},
|
|
});
|
|
Object.defineProperty(TemporalNow, Symbol.toStringTag, {
|
|
value: "Temporal.Now", writable: false, enumerable: false, configurable: true,
|
|
});
|
|
|
|
const Temporal = {
|
|
Instant,
|
|
Duration,
|
|
PlainDate,
|
|
ZonedDateTime,
|
|
PlainTime: notImplemented("Temporal.PlainTime"),
|
|
PlainDateTime: notImplemented("Temporal.PlainDateTime"),
|
|
PlainYearMonth: notImplemented("Temporal.PlainYearMonth"),
|
|
PlainMonthDay: notImplemented("Temporal.PlainMonthDay"),
|
|
Now: TemporalNow,
|
|
};
|
|
|
|
Object.defineProperty(Temporal, Symbol.toStringTag, {
|
|
value: "Temporal", writable: false, enumerable: false, configurable: true,
|
|
});
|
|
|
|
// Ensure constructors have non-enumerable descriptors expected by prop-desc tests
|
|
for (const [name, ctor] of Object.entries({ Instant, Duration, PlainDate, ZonedDateTime, Now: TemporalNow })) {
|
|
Object.defineProperty(Temporal, name, {
|
|
value: ctor, writable: true, enumerable: false, configurable: true,
|
|
});
|
|
}
|
|
|
|
globalThis.Temporal = Temporal;
|
|
})();
|