@nodegx/preview 0.2.2 → 0.2.4
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/dist/backend/cli.js +356 -245
- package/dist/browser/noodl-graph-target.js +95 -94
- package/dist/browser/noodl-preview-in-page.js +18 -18
- package/dist/external/deploy/noodl.deploy.js +1 -1
- package/dist/nodegx-deploy.cjs +429 -222
- package/dist/nodegx-deploy.inputs.json +3 -0
- package/dist/noodl-preview.cjs +432 -224
- package/dist/noodl-preview.inputs.json +3 -0
- package/dist/package.json +1 -1
- package/package.json +3 -2
package/dist/nodegx-deploy.cjs
CHANGED
|
@@ -27042,6 +27042,35 @@ var require_simplejavascript = __commonJS({
|
|
|
27042
27042
|
}
|
|
27043
27043
|
return users;
|
|
27044
27044
|
}
|
|
27045
|
+
function placingSignalPorts(node) {
|
|
27046
|
+
const saved = node.model.outputPorts;
|
|
27047
|
+
const params = node.model.parameters || {};
|
|
27048
|
+
const names = [];
|
|
27049
|
+
for (const key in saved) {
|
|
27050
|
+
if (saved[key] && saved[key].type === "signal") names.push(key);
|
|
27051
|
+
}
|
|
27052
|
+
const declare = (key) => {
|
|
27053
|
+
if (!(key in saved) && names.indexOf(key) === -1) names.push(key);
|
|
27054
|
+
};
|
|
27055
|
+
const listed = [];
|
|
27056
|
+
const scriptOutputs = params["scriptOutputs"];
|
|
27057
|
+
if (Array.isArray(scriptOutputs)) {
|
|
27058
|
+
scriptOutputs.forEach((p) => {
|
|
27059
|
+
listed.push("out-" + p.label);
|
|
27060
|
+
if (params["outtype-" + p.label] === "signal") declare("out-" + p.label);
|
|
27061
|
+
});
|
|
27062
|
+
}
|
|
27063
|
+
const script = params["functionScript"];
|
|
27064
|
+
if (typeof script === "string") {
|
|
27065
|
+
const cached = node._internal.codeSignals;
|
|
27066
|
+
const signals = cached && cached.script === script ? cached.names : functionScriptPortNames(script).signals;
|
|
27067
|
+
node._internal.codeSignals = { script, names: signals };
|
|
27068
|
+
signals.forEach((n) => {
|
|
27069
|
+
if (listed.indexOf("out-" + n) === -1) declare("out-" + n);
|
|
27070
|
+
});
|
|
27071
|
+
}
|
|
27072
|
+
return names;
|
|
27073
|
+
}
|
|
27045
27074
|
function publishHeldWrites(node) {
|
|
27046
27075
|
node._internal.heldWrites.forEach((prop) => {
|
|
27047
27076
|
node.registerOutputIfNeeded("out-" + prop);
|
|
@@ -27314,18 +27343,16 @@ var require_simplejavascript = __commonJS({
|
|
|
27314
27343
|
}
|
|
27315
27344
|
const inputs = this._internal.inputValues;
|
|
27316
27345
|
const outputs = this._internal.outputValuesProxy;
|
|
27317
|
-
|
|
27318
|
-
|
|
27319
|
-
|
|
27320
|
-
|
|
27321
|
-
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
|
|
27327
|
-
}
|
|
27328
|
-
}
|
|
27346
|
+
placingSignalPorts(this).forEach((key) => {
|
|
27347
|
+
const _sendSignal = () => {
|
|
27348
|
+
this._internal.outputWrites.add(key.substring("out-".length));
|
|
27349
|
+
publishHeldWrites(this);
|
|
27350
|
+
if (this.hasOutput(key)) this.sendSignalOnOutput(key);
|
|
27351
|
+
};
|
|
27352
|
+
const signalValue = _sendSignal;
|
|
27353
|
+
signalValue.send = _sendSignal;
|
|
27354
|
+
this._internal.outputValues[key.substring("out-".length)] = signalValue;
|
|
27355
|
+
});
|
|
27329
27356
|
const noodlAPI = JavascriptNodeParser4.createNoodlAPI(this.nodeScope.modelScope);
|
|
27330
27357
|
noodlAPI.Inputs = inputs;
|
|
27331
27358
|
noodlAPI.Outputs = outputs;
|
|
@@ -28738,6 +28765,198 @@ var init_resolveBackend = __esm({
|
|
|
28738
28765
|
}
|
|
28739
28766
|
});
|
|
28740
28767
|
|
|
28768
|
+
// ../noodl-runtime/src/api/backends/SessionStore.ts
|
|
28769
|
+
function ambientStorage() {
|
|
28770
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
28771
|
+
return localStorage;
|
|
28772
|
+
}
|
|
28773
|
+
function ambientBroadcaster() {
|
|
28774
|
+
if (typeof window === "undefined" || !window || typeof window.addEventListener !== "function") return void 0;
|
|
28775
|
+
return window;
|
|
28776
|
+
}
|
|
28777
|
+
function parseSessionKey(appId) {
|
|
28778
|
+
return "Parse/" + appId + "/currentUser";
|
|
28779
|
+
}
|
|
28780
|
+
function parseInstallationIdKey(appId) {
|
|
28781
|
+
return "Parse/" + appId + "/installationId";
|
|
28782
|
+
}
|
|
28783
|
+
function parseSessionStore(appId) {
|
|
28784
|
+
const cacheKey = String(appId);
|
|
28785
|
+
let store = _parseStores.get(cacheKey);
|
|
28786
|
+
if (store === void 0) {
|
|
28787
|
+
store = new SessionStore({ key: parseSessionKey(appId) });
|
|
28788
|
+
_parseStores.set(cacheKey, store);
|
|
28789
|
+
}
|
|
28790
|
+
return store;
|
|
28791
|
+
}
|
|
28792
|
+
var _tabCounter, SessionStore, _parseStores;
|
|
28793
|
+
var init_SessionStore = __esm({
|
|
28794
|
+
"../noodl-runtime/src/api/backends/SessionStore.ts"() {
|
|
28795
|
+
_tabCounter = 0;
|
|
28796
|
+
SessionStore = class {
|
|
28797
|
+
constructor(options) {
|
|
28798
|
+
this.key = options.key;
|
|
28799
|
+
this.lockKey = options.key + ".refresh-lock";
|
|
28800
|
+
this.injectedStorage = options.storage;
|
|
28801
|
+
this.injectedBroadcaster = options.broadcaster;
|
|
28802
|
+
this.lockTtlMs = options.lockTtlMs !== void 0 ? options.lockTtlMs : 1e4;
|
|
28803
|
+
this.now = options.now || Date.now;
|
|
28804
|
+
this.tabId = options.tabId || "tab-" + ++_tabCounter + "-" + Math.random().toString(36).slice(2);
|
|
28805
|
+
}
|
|
28806
|
+
/**
|
|
28807
|
+
* Resolved per access, not captured at construction.
|
|
28808
|
+
*
|
|
28809
|
+
* The code this replaced was a bare `localStorage[key]` evaluated on every
|
|
28810
|
+
* request, so a page (or a suite) that swapped the global took effect
|
|
28811
|
+
* immediately. Capturing it once would look identical until something did —
|
|
28812
|
+
* and something does: `parse-wire-adapter.test.ts` replaces
|
|
28813
|
+
* `globalThis.localStorage` between cases, and caching broke it. Found by
|
|
28814
|
+
* running the suite that was not this task's.
|
|
28815
|
+
*/
|
|
28816
|
+
get storage() {
|
|
28817
|
+
if (this.injectedStorage !== void 0) return this.injectedStorage || void 0;
|
|
28818
|
+
return ambientStorage();
|
|
28819
|
+
}
|
|
28820
|
+
get broadcaster() {
|
|
28821
|
+
if (this.injectedBroadcaster !== void 0) return this.injectedBroadcaster || void 0;
|
|
28822
|
+
return ambientBroadcaster();
|
|
28823
|
+
}
|
|
28824
|
+
/** False under SSR and in the cloud runtime. Nothing here throws when it is. */
|
|
28825
|
+
get available() {
|
|
28826
|
+
return this.storage !== void 0;
|
|
28827
|
+
}
|
|
28828
|
+
// ── The session ──────────────────────────────────────────────────────────
|
|
28829
|
+
/** The stored text, or `undefined`. */
|
|
28830
|
+
readRaw() {
|
|
28831
|
+
const storage = this.storage;
|
|
28832
|
+
if (!storage) return void 0;
|
|
28833
|
+
const value2 = storage[this.key];
|
|
28834
|
+
return value2 === void 0 || value2 === null ? void 0 : String(value2);
|
|
28835
|
+
}
|
|
28836
|
+
/**
|
|
28837
|
+
* The stored session, or `undefined` when there is none *or it will not
|
|
28838
|
+
* parse*.
|
|
28839
|
+
*
|
|
28840
|
+
* Unparseable is treated as absent rather than raised, which is what all four
|
|
28841
|
+
* of the replaced call sites did — a corrupt entry must not stop an app from
|
|
28842
|
+
* loading, and the user can always sign in again.
|
|
28843
|
+
*/
|
|
28844
|
+
read() {
|
|
28845
|
+
const raw = this.readRaw();
|
|
28846
|
+
if (raw === void 0) return void 0;
|
|
28847
|
+
try {
|
|
28848
|
+
return JSON.parse(raw);
|
|
28849
|
+
} catch (e) {
|
|
28850
|
+
return void 0;
|
|
28851
|
+
}
|
|
28852
|
+
}
|
|
28853
|
+
write(session) {
|
|
28854
|
+
const storage = this.storage;
|
|
28855
|
+
if (!storage) return;
|
|
28856
|
+
storage[this.key] = JSON.stringify(session);
|
|
28857
|
+
}
|
|
28858
|
+
clear() {
|
|
28859
|
+
const storage = this.storage;
|
|
28860
|
+
if (!storage) return;
|
|
28861
|
+
delete storage[this.key];
|
|
28862
|
+
}
|
|
28863
|
+
/**
|
|
28864
|
+
* Read and write any key in the same storage, with the same absent-storage
|
|
28865
|
+
* tolerance.
|
|
28866
|
+
*
|
|
28867
|
+
* Here for Parse's installation id, which lives beside the session under
|
|
28868
|
+
* `Parse/<appId>/installationId` and is the only other thing `userservice.ts`
|
|
28869
|
+
* kept in local storage. It is not session state, so it gets no method of its
|
|
28870
|
+
* own — but it does need the SSR guard, and duplicating that guard is how the
|
|
28871
|
+
* four copies above happened in the first place.
|
|
28872
|
+
*/
|
|
28873
|
+
readKey(key) {
|
|
28874
|
+
const storage = this.storage;
|
|
28875
|
+
if (!storage) return void 0;
|
|
28876
|
+
const value2 = storage[key];
|
|
28877
|
+
return value2 === void 0 || value2 === null ? void 0 : String(value2);
|
|
28878
|
+
}
|
|
28879
|
+
writeKey(key, value2) {
|
|
28880
|
+
const storage = this.storage;
|
|
28881
|
+
if (!storage) return;
|
|
28882
|
+
storage[key] = value2;
|
|
28883
|
+
}
|
|
28884
|
+
// ── Cross-tab ────────────────────────────────────────────────────────────
|
|
28885
|
+
/**
|
|
28886
|
+
* Hear about session changes made by **other** tabs.
|
|
28887
|
+
*
|
|
28888
|
+
* The `storage` event fires only in tabs that did not make the change, which
|
|
28889
|
+
* is exactly the semantics wanted and is why this is preferred to
|
|
28890
|
+
* `BroadcastChannel`: no echo to filter out, no availability question, and it
|
|
28891
|
+
* comes free with the storage already in use.
|
|
28892
|
+
*
|
|
28893
|
+
* Returns an unsubscribe function. Returns a no-op one when there is no
|
|
28894
|
+
* broadcaster, so callers never branch.
|
|
28895
|
+
*/
|
|
28896
|
+
onExternalChange(handler) {
|
|
28897
|
+
const broadcaster = this.broadcaster;
|
|
28898
|
+
if (!broadcaster) return () => {
|
|
28899
|
+
};
|
|
28900
|
+
const listener = (event) => {
|
|
28901
|
+
if (event && event.key !== null && event.key !== this.key) return;
|
|
28902
|
+
handler(this.read());
|
|
28903
|
+
};
|
|
28904
|
+
broadcaster.addEventListener("storage", listener);
|
|
28905
|
+
return () => broadcaster.removeEventListener("storage", listener);
|
|
28906
|
+
}
|
|
28907
|
+
/**
|
|
28908
|
+
* Try to become the tab that refreshes.
|
|
28909
|
+
*
|
|
28910
|
+
* Read, write, **read back**. `localStorage` writes are serialised per origin,
|
|
28911
|
+
* so in the racing case both tabs write and both read back and exactly one
|
|
28912
|
+
* sees its own id — and the loser discovers that *before issuing any request*,
|
|
28913
|
+
* because the whole sequence is synchronous with no `await` in it. That is
|
|
28914
|
+
* what keeps the cost of a lost race at one tab doing nothing rather than two
|
|
28915
|
+
* tabs spending the same rotating refresh token.
|
|
28916
|
+
*
|
|
28917
|
+
* It is not a mutex and BCN-006-LIFECYCLE-DESIGN §5 does not claim it is. The
|
|
28918
|
+
* residual risk is a same-millisecond interleaving across processes on an
|
|
28919
|
+
* implementation that does not serialise writes; the cost if it happens is one
|
|
28920
|
+
* wasted refresh, and the follower's wait-then-adopt behaviour means the user
|
|
28921
|
+
* still ends up with the session the winner wrote.
|
|
28922
|
+
*
|
|
28923
|
+
* **The lock expires.** A tab closed mid-refresh must not hold it forever —
|
|
28924
|
+
* that would be worse than having no lock at all.
|
|
28925
|
+
*/
|
|
28926
|
+
tryAcquireRefreshLock() {
|
|
28927
|
+
const storage = this.storage;
|
|
28928
|
+
if (!storage) return false;
|
|
28929
|
+
const now = this.now();
|
|
28930
|
+
const existing = this.readLock();
|
|
28931
|
+
if (existing && existing.owner !== this.tabId && existing.expiresAt > now) return false;
|
|
28932
|
+
storage[this.lockKey] = JSON.stringify({ owner: this.tabId, expiresAt: now + this.lockTtlMs });
|
|
28933
|
+
const confirmed = this.readLock();
|
|
28934
|
+
return confirmed !== void 0 && confirmed.owner === this.tabId;
|
|
28935
|
+
}
|
|
28936
|
+
/** Release only our own lock — never one another tab has since taken. */
|
|
28937
|
+
releaseRefreshLock() {
|
|
28938
|
+
const storage = this.storage;
|
|
28939
|
+
if (!storage) return;
|
|
28940
|
+
const existing = this.readLock();
|
|
28941
|
+
if (existing && existing.owner !== this.tabId) return;
|
|
28942
|
+
delete storage[this.lockKey];
|
|
28943
|
+
}
|
|
28944
|
+
readLock() {
|
|
28945
|
+
const raw = this.readKey(this.lockKey);
|
|
28946
|
+
if (raw === void 0) return void 0;
|
|
28947
|
+
try {
|
|
28948
|
+
const parsed = JSON.parse(raw);
|
|
28949
|
+
if (!parsed || typeof parsed.owner !== "string" || typeof parsed.expiresAt !== "number") return void 0;
|
|
28950
|
+
return parsed;
|
|
28951
|
+
} catch (e) {
|
|
28952
|
+
return void 0;
|
|
28953
|
+
}
|
|
28954
|
+
}
|
|
28955
|
+
};
|
|
28956
|
+
_parseStores = /* @__PURE__ */ new Map();
|
|
28957
|
+
}
|
|
28958
|
+
});
|
|
28959
|
+
|
|
28741
28960
|
// ../nodegx-backend-contract/src/realtime.ts
|
|
28742
28961
|
function nextReconnectDelay(attempt, timing = REALTIME_TIMING) {
|
|
28743
28962
|
const n = Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;
|
|
@@ -28898,6 +29117,7 @@ var DEFAULT_TIMING, DEFAULT_HOST, RealtimeSubscription;
|
|
|
28898
29117
|
var init_RealtimeSubscription = __esm({
|
|
28899
29118
|
"../noodl-runtime/src/api/backends/realtime/RealtimeSubscription.ts"() {
|
|
28900
29119
|
init_resolveBackend_pure();
|
|
29120
|
+
init_SessionStore();
|
|
28901
29121
|
init_realtime2();
|
|
28902
29122
|
DEFAULT_TIMING = REALTIME_TIMING;
|
|
28903
29123
|
DEFAULT_HOST = {};
|
|
@@ -29138,9 +29358,20 @@ var init_RealtimeSubscription = __esm({
|
|
|
29138
29358
|
* fallback.
|
|
29139
29359
|
*/
|
|
29140
29360
|
get token() {
|
|
29141
|
-
if (isParseWireType(this.handle.type)) return this.handle.sessionToken || "";
|
|
29361
|
+
if (isParseWireType(this.handle.type)) return this.handle.sessionToken || this.storedSession() || "";
|
|
29142
29362
|
return this.handle.sessionToken || this.handle.publicToken || "";
|
|
29143
29363
|
}
|
|
29364
|
+
/**
|
|
29365
|
+
* 🔴 P121-TWO005-LIVEANON. The signed-in user's session, where a Parse-wire app keeps it: the session store, keyed by
|
|
29366
|
+
* the app id in `publicToken` — the same place `ParseWireAdapter` reads it from for every request
|
|
29367
|
+
* (`readStoredCurrentUser`). Nothing sets `handle.sessionToken` on this wire, so reading the handle alone made every
|
|
29368
|
+
* subscription anonymous for a signed-in member, and a collection only members may read refused it ("denies find
|
|
29369
|
+
* to anonymous") on every retry. SBR-011's arm passed a session ON THE HANDLE, which a real app never has.
|
|
29370
|
+
*/
|
|
29371
|
+
storedSession() {
|
|
29372
|
+
const stored = parseSessionStore(this.handle.publicToken).read();
|
|
29373
|
+
return stored && typeof stored.sessionToken === "string" ? stored.sessionToken : "";
|
|
29374
|
+
}
|
|
29144
29375
|
_armDeadline(generation2) {
|
|
29145
29376
|
this._clearTimer("_deadlineTimer");
|
|
29146
29377
|
this._deadlineTimer = this._setTimeout(() => {
|
|
@@ -29684,12 +29915,11 @@ var init_SseTransport = __esm({
|
|
|
29684
29915
|
helloEvent: "connected",
|
|
29685
29916
|
changeEvents: () => ["change"],
|
|
29686
29917
|
resyncEvent: "resync",
|
|
29687
|
-
subscribeRequest(base,
|
|
29918
|
+
subscribeRequest(base, _token, clientId, subscriptions) {
|
|
29688
29919
|
const body = subscriptions.map(
|
|
29689
29920
|
(subscription) => subscription.where === void 0 ? { collection: subscription.collection } : { collection: subscription.collection, filter: subscription.where }
|
|
29690
29921
|
);
|
|
29691
29922
|
const headers = { "content-type": "application/json" };
|
|
29692
|
-
if (token) headers["authorization"] = "Bearer " + token;
|
|
29693
29923
|
return {
|
|
29694
29924
|
url: base + "/realtime/subscriptions",
|
|
29695
29925
|
init: {
|
|
@@ -31472,8 +31702,8 @@ var init_nodegx = __esm({
|
|
|
31472
31702
|
// The header works. What it cannot do is make a file private on behalf of
|
|
31473
31703
|
// nobody, and an author reading "supported" would not expect that.
|
|
31474
31704
|
"files.private": degraded(
|
|
31475
|
-
"A private upload is locked to the person who made it, so a file uploaded by anything other than a signed-in user \u2014 an admin tool, a cloud function \u2014 ends up private with no owner, and stays readable by everyone.",
|
|
31476
|
-
"BCN-007 live, security enforced: uploaded as a signed-in user, the plain URL is 403 for anonymous AND for a second signed-in user. Uploaded with the master key, the identical request with X-NodeGX-File-Private: true serves 200 to anonymous \u2014 files.ts::upload sets owner=null for a non-user principal"
|
|
31705
|
+
"A private upload is locked to the person who made it, so a file uploaded by anything other than a signed-in user \u2014 an admin tool, a cloud function \u2014 ends up private with no owner, and stays readable by everyone. Upload File's Readers names roles that may read it too, such as role:member; a file with Readers is never readable by everyone.",
|
|
31706
|
+
"BCN-007 live, security enforced: uploaded as a signed-in user, the plain URL is 403 for anonymous AND for a second signed-in user. Uploaded with the master key, the identical request with X-NodeGX-File-Private: true serves 200 to anonymous \u2014 files.ts::upload sets owner=null for a non-user principal. P121-TWO001-MEMBERSPHOTO: X-NodeGX-File-Readers adds read-only role entries to the file's ACL; uploaded with the master key and role:crew, anonymous and a user without the role are 403 (files-http.test.ts)"
|
|
31477
31707
|
),
|
|
31478
31708
|
"files.progress": supported("XHR upload path reports progress"),
|
|
31479
31709
|
"auth.password": supported("nodegx-backend/src/server/users.ts"),
|
|
@@ -33207,198 +33437,6 @@ var init_AdapterEvents = __esm({
|
|
|
33207
33437
|
}
|
|
33208
33438
|
});
|
|
33209
33439
|
|
|
33210
|
-
// ../noodl-runtime/src/api/backends/SessionStore.ts
|
|
33211
|
-
function ambientStorage() {
|
|
33212
|
-
if (typeof localStorage === "undefined") return void 0;
|
|
33213
|
-
return localStorage;
|
|
33214
|
-
}
|
|
33215
|
-
function ambientBroadcaster() {
|
|
33216
|
-
if (typeof window === "undefined" || !window || typeof window.addEventListener !== "function") return void 0;
|
|
33217
|
-
return window;
|
|
33218
|
-
}
|
|
33219
|
-
function parseSessionKey(appId) {
|
|
33220
|
-
return "Parse/" + appId + "/currentUser";
|
|
33221
|
-
}
|
|
33222
|
-
function parseInstallationIdKey(appId) {
|
|
33223
|
-
return "Parse/" + appId + "/installationId";
|
|
33224
|
-
}
|
|
33225
|
-
function parseSessionStore(appId) {
|
|
33226
|
-
const cacheKey = String(appId);
|
|
33227
|
-
let store = _parseStores.get(cacheKey);
|
|
33228
|
-
if (store === void 0) {
|
|
33229
|
-
store = new SessionStore({ key: parseSessionKey(appId) });
|
|
33230
|
-
_parseStores.set(cacheKey, store);
|
|
33231
|
-
}
|
|
33232
|
-
return store;
|
|
33233
|
-
}
|
|
33234
|
-
var _tabCounter, SessionStore, _parseStores;
|
|
33235
|
-
var init_SessionStore = __esm({
|
|
33236
|
-
"../noodl-runtime/src/api/backends/SessionStore.ts"() {
|
|
33237
|
-
_tabCounter = 0;
|
|
33238
|
-
SessionStore = class {
|
|
33239
|
-
constructor(options) {
|
|
33240
|
-
this.key = options.key;
|
|
33241
|
-
this.lockKey = options.key + ".refresh-lock";
|
|
33242
|
-
this.injectedStorage = options.storage;
|
|
33243
|
-
this.injectedBroadcaster = options.broadcaster;
|
|
33244
|
-
this.lockTtlMs = options.lockTtlMs !== void 0 ? options.lockTtlMs : 1e4;
|
|
33245
|
-
this.now = options.now || Date.now;
|
|
33246
|
-
this.tabId = options.tabId || "tab-" + ++_tabCounter + "-" + Math.random().toString(36).slice(2);
|
|
33247
|
-
}
|
|
33248
|
-
/**
|
|
33249
|
-
* Resolved per access, not captured at construction.
|
|
33250
|
-
*
|
|
33251
|
-
* The code this replaced was a bare `localStorage[key]` evaluated on every
|
|
33252
|
-
* request, so a page (or a suite) that swapped the global took effect
|
|
33253
|
-
* immediately. Capturing it once would look identical until something did —
|
|
33254
|
-
* and something does: `parse-wire-adapter.test.ts` replaces
|
|
33255
|
-
* `globalThis.localStorage` between cases, and caching broke it. Found by
|
|
33256
|
-
* running the suite that was not this task's.
|
|
33257
|
-
*/
|
|
33258
|
-
get storage() {
|
|
33259
|
-
if (this.injectedStorage !== void 0) return this.injectedStorage || void 0;
|
|
33260
|
-
return ambientStorage();
|
|
33261
|
-
}
|
|
33262
|
-
get broadcaster() {
|
|
33263
|
-
if (this.injectedBroadcaster !== void 0) return this.injectedBroadcaster || void 0;
|
|
33264
|
-
return ambientBroadcaster();
|
|
33265
|
-
}
|
|
33266
|
-
/** False under SSR and in the cloud runtime. Nothing here throws when it is. */
|
|
33267
|
-
get available() {
|
|
33268
|
-
return this.storage !== void 0;
|
|
33269
|
-
}
|
|
33270
|
-
// ── The session ──────────────────────────────────────────────────────────
|
|
33271
|
-
/** The stored text, or `undefined`. */
|
|
33272
|
-
readRaw() {
|
|
33273
|
-
const storage = this.storage;
|
|
33274
|
-
if (!storage) return void 0;
|
|
33275
|
-
const value2 = storage[this.key];
|
|
33276
|
-
return value2 === void 0 || value2 === null ? void 0 : String(value2);
|
|
33277
|
-
}
|
|
33278
|
-
/**
|
|
33279
|
-
* The stored session, or `undefined` when there is none *or it will not
|
|
33280
|
-
* parse*.
|
|
33281
|
-
*
|
|
33282
|
-
* Unparseable is treated as absent rather than raised, which is what all four
|
|
33283
|
-
* of the replaced call sites did — a corrupt entry must not stop an app from
|
|
33284
|
-
* loading, and the user can always sign in again.
|
|
33285
|
-
*/
|
|
33286
|
-
read() {
|
|
33287
|
-
const raw = this.readRaw();
|
|
33288
|
-
if (raw === void 0) return void 0;
|
|
33289
|
-
try {
|
|
33290
|
-
return JSON.parse(raw);
|
|
33291
|
-
} catch (e) {
|
|
33292
|
-
return void 0;
|
|
33293
|
-
}
|
|
33294
|
-
}
|
|
33295
|
-
write(session) {
|
|
33296
|
-
const storage = this.storage;
|
|
33297
|
-
if (!storage) return;
|
|
33298
|
-
storage[this.key] = JSON.stringify(session);
|
|
33299
|
-
}
|
|
33300
|
-
clear() {
|
|
33301
|
-
const storage = this.storage;
|
|
33302
|
-
if (!storage) return;
|
|
33303
|
-
delete storage[this.key];
|
|
33304
|
-
}
|
|
33305
|
-
/**
|
|
33306
|
-
* Read and write any key in the same storage, with the same absent-storage
|
|
33307
|
-
* tolerance.
|
|
33308
|
-
*
|
|
33309
|
-
* Here for Parse's installation id, which lives beside the session under
|
|
33310
|
-
* `Parse/<appId>/installationId` and is the only other thing `userservice.ts`
|
|
33311
|
-
* kept in local storage. It is not session state, so it gets no method of its
|
|
33312
|
-
* own — but it does need the SSR guard, and duplicating that guard is how the
|
|
33313
|
-
* four copies above happened in the first place.
|
|
33314
|
-
*/
|
|
33315
|
-
readKey(key) {
|
|
33316
|
-
const storage = this.storage;
|
|
33317
|
-
if (!storage) return void 0;
|
|
33318
|
-
const value2 = storage[key];
|
|
33319
|
-
return value2 === void 0 || value2 === null ? void 0 : String(value2);
|
|
33320
|
-
}
|
|
33321
|
-
writeKey(key, value2) {
|
|
33322
|
-
const storage = this.storage;
|
|
33323
|
-
if (!storage) return;
|
|
33324
|
-
storage[key] = value2;
|
|
33325
|
-
}
|
|
33326
|
-
// ── Cross-tab ────────────────────────────────────────────────────────────
|
|
33327
|
-
/**
|
|
33328
|
-
* Hear about session changes made by **other** tabs.
|
|
33329
|
-
*
|
|
33330
|
-
* The `storage` event fires only in tabs that did not make the change, which
|
|
33331
|
-
* is exactly the semantics wanted and is why this is preferred to
|
|
33332
|
-
* `BroadcastChannel`: no echo to filter out, no availability question, and it
|
|
33333
|
-
* comes free with the storage already in use.
|
|
33334
|
-
*
|
|
33335
|
-
* Returns an unsubscribe function. Returns a no-op one when there is no
|
|
33336
|
-
* broadcaster, so callers never branch.
|
|
33337
|
-
*/
|
|
33338
|
-
onExternalChange(handler) {
|
|
33339
|
-
const broadcaster = this.broadcaster;
|
|
33340
|
-
if (!broadcaster) return () => {
|
|
33341
|
-
};
|
|
33342
|
-
const listener = (event) => {
|
|
33343
|
-
if (event && event.key !== null && event.key !== this.key) return;
|
|
33344
|
-
handler(this.read());
|
|
33345
|
-
};
|
|
33346
|
-
broadcaster.addEventListener("storage", listener);
|
|
33347
|
-
return () => broadcaster.removeEventListener("storage", listener);
|
|
33348
|
-
}
|
|
33349
|
-
/**
|
|
33350
|
-
* Try to become the tab that refreshes.
|
|
33351
|
-
*
|
|
33352
|
-
* Read, write, **read back**. `localStorage` writes are serialised per origin,
|
|
33353
|
-
* so in the racing case both tabs write and both read back and exactly one
|
|
33354
|
-
* sees its own id — and the loser discovers that *before issuing any request*,
|
|
33355
|
-
* because the whole sequence is synchronous with no `await` in it. That is
|
|
33356
|
-
* what keeps the cost of a lost race at one tab doing nothing rather than two
|
|
33357
|
-
* tabs spending the same rotating refresh token.
|
|
33358
|
-
*
|
|
33359
|
-
* It is not a mutex and BCN-006-LIFECYCLE-DESIGN §5 does not claim it is. The
|
|
33360
|
-
* residual risk is a same-millisecond interleaving across processes on an
|
|
33361
|
-
* implementation that does not serialise writes; the cost if it happens is one
|
|
33362
|
-
* wasted refresh, and the follower's wait-then-adopt behaviour means the user
|
|
33363
|
-
* still ends up with the session the winner wrote.
|
|
33364
|
-
*
|
|
33365
|
-
* **The lock expires.** A tab closed mid-refresh must not hold it forever —
|
|
33366
|
-
* that would be worse than having no lock at all.
|
|
33367
|
-
*/
|
|
33368
|
-
tryAcquireRefreshLock() {
|
|
33369
|
-
const storage = this.storage;
|
|
33370
|
-
if (!storage) return false;
|
|
33371
|
-
const now = this.now();
|
|
33372
|
-
const existing = this.readLock();
|
|
33373
|
-
if (existing && existing.owner !== this.tabId && existing.expiresAt > now) return false;
|
|
33374
|
-
storage[this.lockKey] = JSON.stringify({ owner: this.tabId, expiresAt: now + this.lockTtlMs });
|
|
33375
|
-
const confirmed = this.readLock();
|
|
33376
|
-
return confirmed !== void 0 && confirmed.owner === this.tabId;
|
|
33377
|
-
}
|
|
33378
|
-
/** Release only our own lock — never one another tab has since taken. */
|
|
33379
|
-
releaseRefreshLock() {
|
|
33380
|
-
const storage = this.storage;
|
|
33381
|
-
if (!storage) return;
|
|
33382
|
-
const existing = this.readLock();
|
|
33383
|
-
if (existing && existing.owner !== this.tabId) return;
|
|
33384
|
-
delete storage[this.lockKey];
|
|
33385
|
-
}
|
|
33386
|
-
readLock() {
|
|
33387
|
-
const raw = this.readKey(this.lockKey);
|
|
33388
|
-
if (raw === void 0) return void 0;
|
|
33389
|
-
try {
|
|
33390
|
-
const parsed = JSON.parse(raw);
|
|
33391
|
-
if (!parsed || typeof parsed.owner !== "string" || typeof parsed.expiresAt !== "number") return void 0;
|
|
33392
|
-
return parsed;
|
|
33393
|
-
} catch (e) {
|
|
33394
|
-
return void 0;
|
|
33395
|
-
}
|
|
33396
|
-
}
|
|
33397
|
-
};
|
|
33398
|
-
_parseStores = /* @__PURE__ */ new Map();
|
|
33399
|
-
}
|
|
33400
|
-
});
|
|
33401
|
-
|
|
33402
33440
|
// ../noodl-runtime/src/api/backends/fileRef.ts
|
|
33403
33441
|
function jwtExpiry(token) {
|
|
33404
33442
|
if (typeof token !== "string") return void 0;
|
|
@@ -33532,6 +33570,13 @@ function withStatus(err, status) {
|
|
|
33532
33570
|
if (status !== void 0 && status > 0) envelope.status = status;
|
|
33533
33571
|
return envelope;
|
|
33534
33572
|
}
|
|
33573
|
+
function uploadHeaders(options) {
|
|
33574
|
+
const headers = {};
|
|
33575
|
+
if (options.private) headers["X-NodeGX-File-Private"] = "true";
|
|
33576
|
+
const readers = typeof options.readers === "string" ? options.readers.trim() : "";
|
|
33577
|
+
if (readers) headers["X-NodeGX-File-Readers"] = readers;
|
|
33578
|
+
return Object.keys(headers).length ? headers : void 0;
|
|
33579
|
+
}
|
|
33535
33580
|
function _removeProtectedFields(data, className) {
|
|
33536
33581
|
const _data = Object.assign({}, data);
|
|
33537
33582
|
_protectedFields._common.forEach((f) => delete _data[f]);
|
|
@@ -33949,10 +33994,10 @@ var init_ParseWireAdapter = __esm({
|
|
|
33949
33994
|
content: options.file,
|
|
33950
33995
|
// Dead — `_makeRequest` never reads `contentType`. See the module note.
|
|
33951
33996
|
contentType: options.file.type,
|
|
33952
|
-
// BAK-006 follow-up: the Upload File node's Private input
|
|
33953
|
-
//
|
|
33954
|
-
// upload to its owner instead of leaving it public.
|
|
33955
|
-
headers: options
|
|
33997
|
+
// BAK-006 follow-up: the Upload File node's Private input, and (P121
|
|
33998
|
+
// MEMBERSPHOTO) its Readers. The backend reads these headers to ACL the
|
|
33999
|
+
// upload to its owner, and to those roles, instead of leaving it public.
|
|
34000
|
+
headers: uploadHeaders(options),
|
|
33956
34001
|
// BCN-007 step 1. This used to be
|
|
33957
34002
|
// `Object.assign({}, options.data, response)` cast to `{name, url}` —
|
|
33958
34003
|
// whatever the backend sent, spread over a caller-supplied object no
|
|
@@ -95294,6 +95339,76 @@ var init_openfilepicker = __esm({
|
|
|
95294
95339
|
}
|
|
95295
95340
|
});
|
|
95296
95341
|
|
|
95342
|
+
// ../noodl-viewer-react/src/nodes/std-library/shrinkphoto.ts
|
|
95343
|
+
function shrunkSize(width, height, largestSide) {
|
|
95344
|
+
const longer = Math.max(width, height);
|
|
95345
|
+
if (!(largestSide > 0) || !(longer > largestSide)) return void 0;
|
|
95346
|
+
const scale = largestSide / longer;
|
|
95347
|
+
return {
|
|
95348
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
95349
|
+
height: Math.max(1, Math.round(height * scale))
|
|
95350
|
+
};
|
|
95351
|
+
}
|
|
95352
|
+
function shrunkType(type) {
|
|
95353
|
+
return KEEPS_ITS_TYPE.has(type) ? type : "image/jpeg";
|
|
95354
|
+
}
|
|
95355
|
+
function isShrinkable(type) {
|
|
95356
|
+
return typeof type === "string" && type.startsWith("image/") && !LEFT_ALONE.has(type);
|
|
95357
|
+
}
|
|
95358
|
+
function shrunkName(name2, fromType, toType) {
|
|
95359
|
+
if (fromType === toType) return name2;
|
|
95360
|
+
const ext = toType === "image/jpeg" ? "jpg" : toType.slice("image/".length);
|
|
95361
|
+
const dot = name2.lastIndexOf(".");
|
|
95362
|
+
return `${dot > 0 ? name2.slice(0, dot) : name2 || "photo"}.${ext}`;
|
|
95363
|
+
}
|
|
95364
|
+
function qualityOf(quality) {
|
|
95365
|
+
const q = typeof quality === "number" && quality > 0 && quality <= 100 ? quality : DEFAULT_PHOTO_QUALITY;
|
|
95366
|
+
return q / 100;
|
|
95367
|
+
}
|
|
95368
|
+
async function shrinkPhoto(file, options, ops = browserPhotoOps) {
|
|
95369
|
+
if (!(options.largestSide > 0) || !isShrinkable(file.type)) return file;
|
|
95370
|
+
let photo;
|
|
95371
|
+
try {
|
|
95372
|
+
photo = await ops.decode(file);
|
|
95373
|
+
const size2 = shrunkSize(photo.width, photo.height, options.largestSide);
|
|
95374
|
+
if (!size2) return file;
|
|
95375
|
+
const type = shrunkType(file.type);
|
|
95376
|
+
const blob = await ops.encode(photo, size2.width, size2.height, type, qualityOf(options.quality));
|
|
95377
|
+
if (!blob || blob.size === 0 || blob.size >= file.size) return file;
|
|
95378
|
+
return new File([blob], shrunkName(file.name, file.type, type), {
|
|
95379
|
+
type,
|
|
95380
|
+
lastModified: file.lastModified
|
|
95381
|
+
});
|
|
95382
|
+
} catch (_e) {
|
|
95383
|
+
return file;
|
|
95384
|
+
} finally {
|
|
95385
|
+
if (photo && photo.close) photo.close();
|
|
95386
|
+
}
|
|
95387
|
+
}
|
|
95388
|
+
var DEFAULT_PHOTO_QUALITY, KEEPS_ITS_TYPE, LEFT_ALONE, browserPhotoOps;
|
|
95389
|
+
var init_shrinkphoto = __esm({
|
|
95390
|
+
"../noodl-viewer-react/src/nodes/std-library/shrinkphoto.ts"() {
|
|
95391
|
+
DEFAULT_PHOTO_QUALITY = 80;
|
|
95392
|
+
KEEPS_ITS_TYPE = /* @__PURE__ */ new Set(["image/png", "image/webp", "image/jpeg"]);
|
|
95393
|
+
LEFT_ALONE = /* @__PURE__ */ new Set(["image/gif", "image/svg+xml"]);
|
|
95394
|
+
browserPhotoOps = {
|
|
95395
|
+
async decode(file) {
|
|
95396
|
+
return createImageBitmap(file, { imageOrientation: "from-image" });
|
|
95397
|
+
},
|
|
95398
|
+
encode(photo, width, height, type, quality) {
|
|
95399
|
+
const canvas = document.createElement("canvas");
|
|
95400
|
+
canvas.width = width;
|
|
95401
|
+
canvas.height = height;
|
|
95402
|
+
const ctx = canvas.getContext("2d");
|
|
95403
|
+
if (!ctx) return Promise.resolve(null);
|
|
95404
|
+
ctx.imageSmoothingQuality = "high";
|
|
95405
|
+
ctx.drawImage(photo, 0, 0, width, height);
|
|
95406
|
+
return new Promise((resolve3) => canvas.toBlob(resolve3, type, quality));
|
|
95407
|
+
}
|
|
95408
|
+
};
|
|
95409
|
+
}
|
|
95410
|
+
});
|
|
95411
|
+
|
|
95297
95412
|
// ../noodl-viewer-react/src/nodes/std-library/uploadfile.ts
|
|
95298
95413
|
var uploadfile_exports = {};
|
|
95299
95414
|
__export(uploadfile_exports, {
|
|
@@ -95314,6 +95429,7 @@ var init_uploadfile = __esm({
|
|
|
95314
95429
|
init_schema_ports();
|
|
95315
95430
|
init_fileRef();
|
|
95316
95431
|
init_outcome();
|
|
95432
|
+
init_shrinkphoto();
|
|
95317
95433
|
UPLOAD_ERROR_CODE = "upload-file/upload-failed";
|
|
95318
95434
|
UploadFile = {
|
|
95319
95435
|
name: "Upload File",
|
|
@@ -95352,6 +95468,42 @@ var init_uploadfile = __esm({
|
|
|
95352
95468
|
this._internal.private = value2;
|
|
95353
95469
|
}
|
|
95354
95470
|
},
|
|
95471
|
+
// P121-TWO001-MEMBERSPHOTO: Private alone is the uploader's alone, and a feed shows every
|
|
95472
|
+
// member's photos to every other member. `X-NodeGX-File-Readers` shares a private file with
|
|
95473
|
+
// roles; the backend refuses a malformed list, or a role the uploader does not hold, in words.
|
|
95474
|
+
readers: {
|
|
95475
|
+
group: "General",
|
|
95476
|
+
displayName: "Readers",
|
|
95477
|
+
type: "string",
|
|
95478
|
+
description: "Roles that may read the file as well as whoever uploaded it, such as role:member, separated by commas; setting it makes the file private. NodeGX backends only",
|
|
95479
|
+
set(value2) {
|
|
95480
|
+
this._internal.readers = value2;
|
|
95481
|
+
}
|
|
95482
|
+
},
|
|
95483
|
+
// ── P121-TWO008-UPLOADSHRINK: Shrink Photos ──────────────────────────
|
|
95484
|
+
//
|
|
95485
|
+
// A phone photo is 3 to 8 MB, and the backend's thumbnails shrink what is served, not what
|
|
95486
|
+
// is sent. Off unless set, so every existing graph sends exactly what it sent before.
|
|
95487
|
+
shrinkTo: {
|
|
95488
|
+
group: "Shrink Photos",
|
|
95489
|
+
displayName: "Shrink To",
|
|
95490
|
+
type: "number",
|
|
95491
|
+
default: 0,
|
|
95492
|
+
description: "Makes a photo smaller in the browser before sending it, so its longer side is at most this many pixels; 0 sends every file as picked",
|
|
95493
|
+
set(value2) {
|
|
95494
|
+
this._internal.shrinkTo = value2;
|
|
95495
|
+
}
|
|
95496
|
+
},
|
|
95497
|
+
photoQuality: {
|
|
95498
|
+
group: "Shrink Photos",
|
|
95499
|
+
displayName: "Photo Quality",
|
|
95500
|
+
type: "number",
|
|
95501
|
+
default: 80,
|
|
95502
|
+
description: "How much detail a shrunk JPEG or WebP keeps, from 1 to 100; PNG keeps every pixel and its transparency, GIF and SVG are never shrunk",
|
|
95503
|
+
set(value2) {
|
|
95504
|
+
this._internal.photoQuality = value2;
|
|
95505
|
+
}
|
|
95506
|
+
},
|
|
95355
95507
|
// ── BCN-007 step 3: File Location ────────────────────────────────────
|
|
95356
95508
|
//
|
|
95357
95509
|
// ⚠️ **Two of the five backends cannot store a file without being told
|
|
@@ -95429,10 +95581,11 @@ var init_uploadfile = __esm({
|
|
|
95429
95581
|
}
|
|
95430
95582
|
const store = this.cloudStore([outcome]);
|
|
95431
95583
|
if (!store) return;
|
|
95432
|
-
store.uploadFile({
|
|
95433
|
-
file,
|
|
95584
|
+
const send = (sent) => store.uploadFile({
|
|
95585
|
+
file: sent,
|
|
95434
95586
|
private: this._internal.private,
|
|
95435
|
-
|
|
95587
|
+
readers: this._internal.readers,
|
|
95588
|
+
target: this.fileTarget(sent),
|
|
95436
95589
|
onUploadProgress: (p) => {
|
|
95437
95590
|
this._internal.progressTotal = p.total;
|
|
95438
95591
|
this._internal.progressLoaded = p.loaded;
|
|
@@ -95448,14 +95601,20 @@ var init_uploadfile = __esm({
|
|
|
95448
95601
|
// structurally rather than imported, because `cloudstore.js` is still
|
|
95449
95602
|
// JavaScript and declares this callback `unknown` (PLAT-006 residual).
|
|
95450
95603
|
success: (response) => {
|
|
95451
|
-
const
|
|
95452
|
-
const filename = response.filename !== void 0 ? response.filename :
|
|
95604
|
+
const sentName = sent && typeof sent.name === "string" ? sent.name : void 0;
|
|
95605
|
+
const filename = response.filename !== void 0 ? response.filename : sentName;
|
|
95453
95606
|
this._internal.cloudFile = new import_cloudfile.default(filename !== void 0 ? Object.assign({}, response, { filename }) : response);
|
|
95454
95607
|
this.flagOutputDirty("cloudFile");
|
|
95455
95608
|
this.reportOutcome(outcome, "done");
|
|
95456
95609
|
},
|
|
95457
95610
|
error: (e) => this.setError(e, [outcome])
|
|
95458
95611
|
});
|
|
95612
|
+
const shrinkTo = this._internal.shrinkTo;
|
|
95613
|
+
if (typeof shrinkTo === "number" && shrinkTo > 0 && isShrinkable(file.type)) {
|
|
95614
|
+
shrinkPhoto(file, { largestSide: shrinkTo, quality: this._internal.photoQuality }).then(send);
|
|
95615
|
+
} else {
|
|
95616
|
+
send(file);
|
|
95617
|
+
}
|
|
95459
95618
|
});
|
|
95460
95619
|
}
|
|
95461
95620
|
}
|
|
@@ -95603,8 +95762,9 @@ var init_uploadfile = __esm({
|
|
|
95603
95762
|
* meaningful: a project has one backend selected, and only one of the two
|
|
95604
95763
|
* backends reads either group.
|
|
95605
95764
|
*/
|
|
95606
|
-
fileTarget() {
|
|
95607
|
-
const { bucket, path: path8, collection, recordId, field
|
|
95765
|
+
fileTarget(sent) {
|
|
95766
|
+
const { bucket, path: path8, collection, recordId, field } = this._internal;
|
|
95767
|
+
const file = sent || this._internal.file;
|
|
95608
95768
|
if (bucket) {
|
|
95609
95769
|
return { kind: "bucket", bucket, path: path8 || file?.name || "" };
|
|
95610
95770
|
}
|
|
@@ -149443,6 +149603,18 @@ var node_catalog_default = {
|
|
|
149443
149603
|
isSignal: false,
|
|
149444
149604
|
description: "Object path inside the bucket, such as avatars/me.png. Supabase only. Leave blank to use the file's own name"
|
|
149445
149605
|
},
|
|
149606
|
+
{
|
|
149607
|
+
name: "photoQuality",
|
|
149608
|
+
displayName: "Photo Quality",
|
|
149609
|
+
group: "Shrink Photos",
|
|
149610
|
+
plug: "input",
|
|
149611
|
+
type: {
|
|
149612
|
+
name: "number"
|
|
149613
|
+
},
|
|
149614
|
+
isSignal: false,
|
|
149615
|
+
default: 80,
|
|
149616
|
+
description: "How much detail a shrunk JPEG or WebP keeps, from 1 to 100; PNG keeps every pixel and its transparency, GIF and SVG are never shrunk"
|
|
149617
|
+
},
|
|
149446
149618
|
{
|
|
149447
149619
|
name: "private",
|
|
149448
149620
|
displayName: "Private",
|
|
@@ -149455,6 +149627,17 @@ var node_catalog_default = {
|
|
|
149455
149627
|
default: false,
|
|
149456
149628
|
description: "Restricts the stored file to whoever uploaded it, so reading it later needs a Sign File URL node"
|
|
149457
149629
|
},
|
|
149630
|
+
{
|
|
149631
|
+
name: "readers",
|
|
149632
|
+
displayName: "Readers",
|
|
149633
|
+
group: "General",
|
|
149634
|
+
plug: "input",
|
|
149635
|
+
type: {
|
|
149636
|
+
name: "string"
|
|
149637
|
+
},
|
|
149638
|
+
isSignal: false,
|
|
149639
|
+
description: "Roles that may read the file as well as whoever uploaded it, such as role:member, separated by commas; setting it makes the file private. NodeGX backends only"
|
|
149640
|
+
},
|
|
149458
149641
|
{
|
|
149459
149642
|
name: "recordId",
|
|
149460
149643
|
displayName: "Record ID (PocketBase)",
|
|
@@ -149466,6 +149649,18 @@ var node_catalog_default = {
|
|
|
149466
149649
|
isSignal: false,
|
|
149467
149650
|
description: "Existing record to attach the file to. PocketBase only. Leave blank to create a new record as part of the upload"
|
|
149468
149651
|
},
|
|
149652
|
+
{
|
|
149653
|
+
name: "shrinkTo",
|
|
149654
|
+
displayName: "Shrink To",
|
|
149655
|
+
group: "Shrink Photos",
|
|
149656
|
+
plug: "input",
|
|
149657
|
+
type: {
|
|
149658
|
+
name: "number"
|
|
149659
|
+
},
|
|
149660
|
+
isSignal: false,
|
|
149661
|
+
default: 0,
|
|
149662
|
+
description: "Makes a photo smaller in the browser before sending it, so its longer side is at most this many pixels; 0 sends every file as picked"
|
|
149663
|
+
},
|
|
149469
149664
|
{
|
|
149470
149665
|
name: "upload",
|
|
149471
149666
|
displayName: "Upload",
|
|
@@ -149608,6 +149803,15 @@ var node_catalog_default = {
|
|
|
149608
149803
|
private: [
|
|
149609
149804
|
"always: held as handed; read when the next Upload goes"
|
|
149610
149805
|
],
|
|
149806
|
+
readers: [
|
|
149807
|
+
"always: held as handed; read when the next Upload goes \u2014 set, those roles may read the file as well as whoever uploaded it, and the file is private"
|
|
149808
|
+
],
|
|
149809
|
+
shrinkTo: [
|
|
149810
|
+
"always: held as handed; read when the next Upload goes \u2014 above 0, a photo (not a GIF or an SVG) is drawn smaller in the browser first, its longer side at most this many pixels, and the smaller file is what goes; it goes as picked when it is already that small, when the smaller one would weigh more, or when the browser cannot read it"
|
|
149811
|
+
],
|
|
149812
|
+
photoQuality: [
|
|
149813
|
+
"always: held as handed; read when the next Upload goes, and only when a photo is shrunk \u2014 1 to 100 for a JPEG or WebP, anything else is 80; a PNG keeps every pixel"
|
|
149814
|
+
],
|
|
149611
149815
|
bucket: [
|
|
149612
149816
|
"always: held as handed; read when the next Upload goes \u2014 set, the file goes to this bucket at Path (or under the file's own name)"
|
|
149613
149817
|
],
|
|
@@ -149624,7 +149828,7 @@ var node_catalog_default = {
|
|
|
149624
149828
|
"always: held as handed; read when the next Upload goes, and only with Collection \u2014 a half-filled group is no location at all"
|
|
149625
149829
|
],
|
|
149626
149830
|
upload: [
|
|
149627
|
-
"always: its own upload at the frame's end \u2014 two presses are two uploads \u2014 with File, Private and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
|
|
149831
|
+
"always: its own upload at the frame's end \u2014 two presses are two uploads \u2014 with File, Private, Readers and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
|
|
149628
149832
|
]
|
|
149629
149833
|
}
|
|
149630
149834
|
},
|
|
@@ -190138,7 +190342,10 @@ function exportNode(node) {
|
|
|
190138
190342
|
id: node.id,
|
|
190139
190343
|
type: node.type.name,
|
|
190140
190344
|
version: node.version,
|
|
190141
|
-
|
|
190345
|
+
// P123-S2-VARIANTLOST: `node.variant` is a lookup through the node library's registered modules, and a project
|
|
190346
|
+
// that is not `ProjectModel.instance` (the kit's in-page preview, the headless render) is not one — the name the
|
|
190347
|
+
// node wears is on the node itself, and the runtime looks the variant up by name.
|
|
190348
|
+
variant: node.variant ? node.variant.name : node.variantName || void 0,
|
|
190142
190349
|
parameters: clone(node.parameters),
|
|
190143
190350
|
stateParameters: node.stateParameters ? clone(node.stateParameters) : void 0,
|
|
190144
190351
|
stateTransitions: node.stateTransitions ? clone(node.stateTransitions) : void 0,
|