diff --git a/common/src/app/common/transit.cljc b/common/src/app/common/transit.cljc index 21673bdb4..6d315e613 100644 --- a/common/src/app/common/transit.cljc +++ b/common/src/app/common/transit.cljc @@ -115,6 +115,7 @@ {:id "n" :rfn (fn [value] (js/parseInt value 10))}) + #?(:cljs {:id "u" :rfn parse-uuid}) diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index 975e6afec..3c5bf5d07 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -17,32 +17,32 @@ java.util.UUID java.nio.ByteBuffer))) -(def zero #uuid "00000000-0000-0000-0000-000000000000") - -(defn zero? - [v] - (= zero v)) +(defn uuid + "Parse string uuid representation into proper UUID instance." + [s] + #?(:clj (UUID/fromString s) + :cljs (c/uuid s))) (defn next [] #?(:clj (UUIDv8/create) - :cljs (impl/v8))) + :cljs (uuid (impl/v8)))) (defn random "Alias for clj-uuid/v4." [] #?(:clj (UUID/randomUUID) - :cljs (impl/v4))) - -(defn uuid - "Parse string uuid representation into proper UUID instance." - [s] - #?(:clj (UUID/fromString s) - :cljs (c/parse-uuid s))) + :cljs (uuid (impl/v4)))) (defn custom - ([a] #?(:clj (UUID. 0 a) :cljs (c/parse-uuid (impl/custom 0 a)))) - ([b a] #?(:clj (UUID. b a) :cljs (c/parse-uuid (impl/custom b a))))) + ([a] #?(:clj (UUID. 0 a) :cljs (uuid (impl/custom 0 a)))) + ([b a] #?(:clj (UUID. b a) :cljs (uuid (impl/custom b a))))) + +(def zero (uuid "00000000-0000-0000-0000-000000000000")) + +(defn zero? + [v] + (= zero v)) #?(:clj (defn get-word-high @@ -54,32 +54,41 @@ [id] (.getLeastSignificantBits ^UUID id))) -#?(:clj - (defn get-bytes - [^UUID o] +(defn get-bytes + [^UUID o] + #?(:clj (let [buf (ByteBuffer/allocate 16)] (.putLong buf (.getMostSignificantBits o)) (.putLong buf (.getLeastSignificantBits o)) - (.array buf)))) + (.array buf)) + :cljs + (impl/getBytes (.-uuid o)))) -#?(:clj - (defn from-bytes - [^bytes o] +(defn from-bytes + [^bytes o] + #?(:clj (let [buf (ByteBuffer/wrap o)] (UUID. ^long (.getLong buf) - ^long (.getLong buf))))) + ^long (.getLong buf))) + :cljs + (uuid (impl/fromBytes o)))) #?(:cljs (defn uuid->short-id "Return a shorter string of a safe subset of bytes of an uuid encoded with base62. It is only safe to use with uuid v4 and penpot custom v8" [id] - (impl/short-v8 (dm/str id)))) + (impl/shortV8 (dm/str id)))) #?(:cljs - (defn uuid->u32 - [id] - (impl/get-u32 (dm/str id)))) + (defn get-u32 + [this] + (let [buffer (unchecked-get this "__u32_buffer")] + (if (nil? buffer) + (let [buffer (impl/getUnsignedInt32Array (.-uuid ^UUID this))] + (unchecked-set this "__u32_buffer" buffer) + buffer) + buffer)))) #?(:clj (defn hash-int @@ -88,4 +97,3 @@ b (.getLeastSignificantBits ^UUID id)] (+ (clojure.lang.Murmur3/hashLong a) (clojure.lang.Murmur3/hashLong b))))) - diff --git a/common/src/app/common/uuid_impl.js b/common/src/app/common/uuid_impl.js index 38ad8203f..3267ba3f6 100644 --- a/common/src/app/common/uuid_impl.js +++ b/common/src/app/common/uuid_impl.js @@ -7,12 +7,10 @@ */ "use strict"; -goog.require("cljs.core"); goog.require("app.common.encoding_impl"); goog.provide("app.common.uuid_impl"); goog.scope(function() { - const core = cljs.core; const global = goog.global; const encoding = app.common.encoding_impl; const self = app.common.uuid_impl; @@ -129,7 +127,7 @@ goog.scope(function() { fill(arr); arr[6] = (arr[6] & 0x0f) | 0x40; arr[8] = (arr[8] & 0x3f) | 0x80; - return core.uuid(encoding.bufferToHex(arr, true)); + return encoding.bufferToHex(arr, true); }; })(); @@ -161,7 +159,7 @@ goog.scope(function() { setBigUint64(view, 0, msb, false); setBigUint64(view, 8, lsb, false); - return core.uuid(encoding.bufferToHex(int8, true)); + return encoding.bufferToHex(int8, true); }; const factory = function v8() { @@ -194,6 +192,11 @@ goog.scope(function() { } }; + const fromArray = (u8data) => { + int8.set(u8data); + return encoding.bufferToHex(int8, true); + } + const setTag = (tag) => { tag = BigInt.asUintN(64, "" + tag); if (tag > 0x0000_0000_0000_000fn) { @@ -207,10 +210,11 @@ goog.scope(function() { factory.create = create; factory.setTag = setTag; + factory.fromArray = fromArray; return factory; })(); - self.short_v8 = function(uuid) { + self.shortV8 = function(uuid) { const buff = encoding.hexToBuffer(uuid); const short = new Uint8Array(buff, 4); return encoding.bufferToBase62(short); @@ -222,39 +226,61 @@ goog.scope(function() { return `${most.substring(0, 8)}-${most.substring(8, 12)}-${most.substring(12)}-${least.substring(0, 4)}-${least.substring(4)}`; }; - self.get_u32 = (function() { - const UUID_BYTE_SIZE = 16; - const ab = new ArrayBuffer(UUID_BYTE_SIZE); - const u32buffer = new Uint32Array(ab); - const HYPHEN = '-'.charCodeAt(0); - const A = 'a'.charCodeAt(0); - const A_SUB = A - 10; - const ZERO = '0'.charCodeAt(0); - const MAX_DIGIT = 8; - const HALF_BITS = 4; - return function(uuid) { - let digit = 0; - let numDigit = 0; - let u32index = 0; - let u32 = 0; - for (let i = 0; i < uuid.length; i++) { - const charCode = uuid.charCodeAt(i); - if (charCode === HYPHEN) continue; - if (charCode >= A) { - digit = charCode - A_SUB; - } else { - digit = charCode - ZERO; - } - numDigit++; - const bitPos = (MAX_DIGIT - numDigit) * HALF_BITS; - u32 |= (digit << bitPos); - if (numDigit === MAX_DIGIT) { - u32buffer[u32index++] = u32; - u32 = 0; - numDigit = 0; - } + self.fromBytes = function(data) { + if (data instanceof Uint8Array) { + return self.v8.fromArray(data); + } else if (data instanceof Int8Array) { + data = Uint8Array.from(data); + return self.v8.fromArray(data); + } else { + let buffer = data?.buffer; + if (buffer instanceof ArrayBuffer) { + data = new Uint8Array(buffer); + return self.v8.fromArray(data); + } else { + throw new Error("invalid array type received"); } - return u32buffer; } - })(); + }; + + // Code based from uuidjs/parse.ts + self.getBytes = function parse(uuid) { + const buffer = new ArrayBuffer(16); + const view = new Int8Array(buffer); + let rest; + + // Parse ########-....-....-....-............ + view[0] = (rest = parseInt(uuid.slice(0, 8), 16)) >>> 24; + view[1] = (rest >>> 16) & 0xff; + view[2] = (rest >>> 8) & 0xff; + view[3] = rest & 0xff; + + // Parse ........-####-....-....-............ + view[4] = (rest = parseInt(uuid.slice(9, 13), 16)) >>> 8; + view[5] = rest & 0xff; + + // Parse ........-....-####-....-............ + view[6] = (rest = parseInt(uuid.slice(14, 18), 16)) >>> 8; + view[7] = rest & 0xff; + + // Parse ........-....-....-####-............ + view[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8; + view[9] = rest & 0xff, + + // Parse ........-....-....-....-############ + // (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes) + view[10] = ((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff; + view[11] = (rest / 0x100000000) & 0xff; + view[12] = (rest >>> 24) & 0xff; + view[13] = (rest >>> 16) & 0xff; + view[14] = (rest >>> 8) & 0xff; + view[15] = rest & 0xff; + + return view; + } + + self.getUnsignedInt32Array = function (uuid) { + const bytes = self.getBytes(uuid); + return new Uint32Array(bytes.buffer); + } }); diff --git a/common/test/common_tests/uuid_test.cljc b/common/test/common_tests/uuid_test.cljc new file mode 100644 index 000000000..fac59c529 --- /dev/null +++ b/common/test/common_tests/uuid_test.cljc @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns common-tests.uuid-test + (:require + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(defn create-array + [data] + #?(:clj (byte-array data) + :cljs (.from js/Int8Array (into-array data)))) + +(t/deftest bytes-roundtrip + (let [uuid (uuid/uuid "0227df82-63d7-8016-8005-48d9c0f33011") + result-bytes (uuid/get-bytes uuid) + expected-bytes [2 39 -33 -126 99 -41 -128 22 -128 5 72 -39 -64 -13 48 17]] + (t/testing "get-bytes" + (let [data (uuid/get-bytes uuid)] + (t/is (= (nth expected-bytes 0) (aget data 0))) + (t/is (= (nth expected-bytes 1) (aget data 1))) + (t/is (= (nth expected-bytes 2) (aget data 2))) + (t/is (= (nth expected-bytes 3) (aget data 3))) + (t/is (= (nth expected-bytes 4) (aget data 4))) + (t/is (= (nth expected-bytes 5) (aget data 5))) + (t/is (= (nth expected-bytes 6) (aget data 6))) + (t/is (= (nth expected-bytes 7) (aget data 7))) + (t/is (= (nth expected-bytes 8) (aget data 8))) + (t/is (= (nth expected-bytes 9) (aget data 9))) + (t/is (= (nth expected-bytes 10) (aget data 10))) + (t/is (= (nth expected-bytes 11) (aget data 11))) + (t/is (= (nth expected-bytes 12) (aget data 12))) + (t/is (= (nth expected-bytes 13) (aget data 13))) + (t/is (= (nth expected-bytes 14) (aget data 14))) + (t/is (= (nth expected-bytes 15) (aget data 15))))) + + (t/testing "from-bytes" + (let [data (create-array expected-bytes) + result (uuid/from-bytes data)] + (t/is (= result uuid)))))) + + + + diff --git a/frontend/src/app/render_wasm.cljs b/frontend/src/app/render_wasm.cljs index 7b69c9d40..c8ee0c454 100644 --- a/frontend/src/app/render_wasm.cljs +++ b/frontend/src/app/render_wasm.cljs @@ -27,13 +27,13 @@ (defn create-shape [id] - (let [buffer (uuid/uuid->u32 id) + (let [buffer (uuid/get-u32 id) create-shape (unchecked-get internal-module "_create_shape")] (^function create-shape (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)))) (defn use-shape [id] - (let [buffer (uuid/uuid->u32 id) + (let [buffer (uuid/get-u32 id) use-shape (unchecked-get internal-module "_use_shape")] (^function use-shape (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)))) @@ -68,7 +68,7 @@ add-shape-child (unchecked-get internal-module "_add_shape_child")] (^function clear-shape-children) (doseq [id shape_ids] - (let [buffer (uuid/uuid->u32 id)] + (let [buffer (uuid/get-u32 id)] (^function add-shape-child (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)))))) (defn set-shape-fills diff --git a/frontend/src/app/util/timers.cljs b/frontend/src/app/util/timers.cljs index a1b1267cc..4a750e73d 100644 --- a/frontend/src/app/util/timers.cljs +++ b/frontend/src/app/util/timers.cljs @@ -61,14 +61,16 @@ (cancel-idle-callback sem)))))) (def ^:private request-animation-frame - (or (and (exists? js/window) (.-requestAnimationFrame js/window)) - #(js/setTimeout % 16))) + (if (and (exists? js/globalThis) + (exists? (.-requestAnimationFrame js/globalThis))) + #(.requestAnimationFrame js/globalThis %) + #(js/setTimeout % 16))) (defn raf [f] - (request-animation-frame f)) + (^function request-animation-frame f)) (defn idle-then-raf [f] - (schedule-on-idle #(raf f))) + (schedule-on-idle #(^function raf f)))