@heyputer/puter.js 2.0.14 → 2.0.16
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/puter.cjs +2 -2
- package/package.json +5 -3
- package/src/index.js +83 -11
- package/src/init.cjs +1 -1
- package/src/modules/Auth.js +127 -42
- package/src/modules/FileSystem/index.js +8 -39
- package/src/modules/FileSystem/operations/readdir.js +100 -40
- package/src/modules/FileSystem/operations/stat.js +101 -39
- package/src/modules/UI.js +6 -0
- package/src/modules/Workers.js +51 -1
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import * as utils from '../../../lib/utils.js';
|
|
2
2
|
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
|
|
3
3
|
|
|
4
|
+
// Track in-flight requests to avoid duplicate backend calls
|
|
5
|
+
// Each entry stores: { promise, timestamp }
|
|
6
|
+
const inflightRequests = new Map();
|
|
7
|
+
|
|
8
|
+
// Time window (in ms) to group duplicate requests together
|
|
9
|
+
// Requests made within this window will share the same backend call
|
|
10
|
+
const DEDUPLICATION_WINDOW_MS = 2000; // 2 seconds
|
|
11
|
+
|
|
4
12
|
const readdir = async function (...args) {
|
|
5
13
|
let options;
|
|
6
14
|
|
|
@@ -42,56 +50,108 @@ const readdir = async function (...args) {
|
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
// Generate deduplication key based on all request parameters
|
|
54
|
+
const deduplicationKey = JSON.stringify({
|
|
55
|
+
path: options.path,
|
|
56
|
+
uid: options.uid,
|
|
57
|
+
no_thumbs: options.no_thumbs,
|
|
58
|
+
no_assocs: options.no_assocs,
|
|
59
|
+
consistency: options.consistency,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Check if there's already an in-flight request for the same parameters
|
|
63
|
+
const existingEntry = inflightRequests.get(deduplicationKey);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
|
|
66
|
+
if (existingEntry) {
|
|
67
|
+
const timeSinceRequest = now - existingEntry.timestamp;
|
|
68
|
+
|
|
69
|
+
// Only reuse the request if it's within the deduplication window
|
|
70
|
+
if (timeSinceRequest < DEDUPLICATION_WINDOW_MS) {
|
|
71
|
+
// Wait for the existing request and return its result
|
|
72
|
+
try {
|
|
73
|
+
const result = await existingEntry.promise;
|
|
74
|
+
resolve(result);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
reject(error);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
} else {
|
|
80
|
+
// Request is too old, remove it from the tracker
|
|
81
|
+
inflightRequests.delete(deduplicationKey);
|
|
53
82
|
}
|
|
54
83
|
}
|
|
55
84
|
|
|
56
|
-
//
|
|
57
|
-
const
|
|
85
|
+
// Create a promise for this request and store it to deduplicate concurrent calls
|
|
86
|
+
const requestPromise = new Promise(async (resolveRequest, rejectRequest) => {
|
|
87
|
+
// If auth token is not provided and we are in the web environment,
|
|
88
|
+
// try to authenticate with Puter
|
|
89
|
+
if(!puter.authToken && puter.env === 'web'){
|
|
90
|
+
try{
|
|
91
|
+
await puter.ui.authenticateWithPuter();
|
|
92
|
+
}catch(e){
|
|
93
|
+
// if authentication fails, throw an error
|
|
94
|
+
rejectRequest('Authentication failed.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
58
98
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Calculate the size of the result for cache eligibility check
|
|
62
|
-
const resultSize = JSON.stringify(result).length;
|
|
63
|
-
|
|
64
|
-
// Cache the result if it's not bigger than MAX_CACHE_SIZE
|
|
65
|
-
const MAX_CACHE_SIZE = 100 * 1024 * 1024;
|
|
99
|
+
// create xhr object
|
|
100
|
+
const xhr = utils.initXhr('/readdir', this.APIOrigin, undefined, "post", "text/plain;actually=json");
|
|
66
101
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
102
|
+
// set up event handlers for load and error events
|
|
103
|
+
utils.setupXhrEventHandlers(xhr, options.success, options.error, async (result) => {
|
|
104
|
+
// Calculate the size of the result for cache eligibility check
|
|
105
|
+
const resultSize = JSON.stringify(result).length;
|
|
106
|
+
|
|
107
|
+
// Cache the result if it's not bigger than MAX_CACHE_SIZE
|
|
108
|
+
const MAX_CACHE_SIZE = 100 * 1024 * 1024;
|
|
109
|
+
|
|
110
|
+
if(resultSize <= MAX_CACHE_SIZE){
|
|
111
|
+
// UPSERT the cache
|
|
112
|
+
puter._cache.set(cacheKey, result);
|
|
113
|
+
}
|
|
71
114
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
// set each individual item's cache
|
|
116
|
+
for(const item of result){
|
|
117
|
+
puter._cache.set('item:' + item.path, item);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resolveRequest(result);
|
|
121
|
+
}, rejectRequest);
|
|
122
|
+
|
|
123
|
+
// Build request payload - support both path and uid parameters
|
|
124
|
+
const payload = {
|
|
125
|
+
no_thumbs: options.no_thumbs,
|
|
126
|
+
no_assocs: options.no_assocs,
|
|
127
|
+
auth_token: this.authToken
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Add either uid or path to the payload
|
|
131
|
+
if (options.uid) {
|
|
132
|
+
payload.uid = options.uid;
|
|
133
|
+
} else if (options.path) {
|
|
134
|
+
payload.path = getAbsolutePathForApp(options.path);
|
|
75
135
|
}
|
|
76
|
-
|
|
77
|
-
resolve(result);
|
|
78
|
-
}, reject);
|
|
79
136
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
no_thumbs: options.no_thumbs,
|
|
83
|
-
no_assocs: options.no_assocs,
|
|
84
|
-
auth_token: this.authToken
|
|
85
|
-
};
|
|
137
|
+
xhr.send(JSON.stringify(payload));
|
|
138
|
+
});
|
|
86
139
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
140
|
+
// Store the promise and timestamp in the in-flight tracker
|
|
141
|
+
inflightRequests.set(deduplicationKey, {
|
|
142
|
+
promise: requestPromise,
|
|
143
|
+
timestamp: now,
|
|
144
|
+
});
|
|
93
145
|
|
|
94
|
-
|
|
146
|
+
// Wait for the request to complete and clean up
|
|
147
|
+
try {
|
|
148
|
+
const result = await requestPromise;
|
|
149
|
+
inflightRequests.delete(deduplicationKey);
|
|
150
|
+
resolve(result);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
inflightRequests.delete(deduplicationKey);
|
|
153
|
+
reject(error);
|
|
154
|
+
}
|
|
95
155
|
})
|
|
96
156
|
}
|
|
97
157
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import * as utils from '../../../lib/utils.js';
|
|
2
2
|
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
|
|
3
3
|
|
|
4
|
+
// Track in-flight requests to avoid duplicate backend calls
|
|
5
|
+
// Each entry stores: { promise, timestamp }
|
|
6
|
+
const inflightRequests = new Map();
|
|
7
|
+
|
|
8
|
+
// Time window (in ms) to group duplicate requests together
|
|
9
|
+
// Requests made within this window will share the same backend call
|
|
10
|
+
const DEDUPLICATION_WINDOW_MS = 2000; // 2 seconds
|
|
11
|
+
|
|
4
12
|
const stat = async function (...args) {
|
|
5
13
|
let options;
|
|
6
14
|
|
|
@@ -24,17 +32,6 @@ const stat = async function (...args) {
|
|
|
24
32
|
options.consistency = 'strong';
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
// If auth token is not provided and we are in the web environment,
|
|
28
|
-
// try to authenticate with Puter
|
|
29
|
-
if(!puter.authToken && puter.env === 'web'){
|
|
30
|
-
try{
|
|
31
|
-
await puter.ui.authenticateWithPuter();
|
|
32
|
-
}catch(e){
|
|
33
|
-
// if authentication fails, throw an error
|
|
34
|
-
reject('Authentication failed.');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
35
|
// Generate cache key based on path or uid
|
|
39
36
|
let cacheKey;
|
|
40
37
|
if(options.path){
|
|
@@ -50,41 +47,106 @@ const stat = async function (...args) {
|
|
|
50
47
|
}
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
//
|
|
54
|
-
const
|
|
50
|
+
// Generate deduplication key based on all request parameters
|
|
51
|
+
const deduplicationKey = JSON.stringify({
|
|
52
|
+
path: options.path,
|
|
53
|
+
uid: options.uid,
|
|
54
|
+
returnSubdomains: options.returnSubdomains,
|
|
55
|
+
returnPermissions: options.returnPermissions,
|
|
56
|
+
returnVersions: options.returnVersions,
|
|
57
|
+
returnSize: options.returnSize,
|
|
58
|
+
consistency: options.consistency,
|
|
59
|
+
});
|
|
55
60
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
// Check if there's already an in-flight request for the same parameters
|
|
62
|
+
const existingEntry = inflightRequests.get(deduplicationKey);
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
|
|
65
|
+
if (existingEntry) {
|
|
66
|
+
const timeSinceRequest = now - existingEntry.timestamp;
|
|
60
67
|
|
|
61
|
-
//
|
|
62
|
-
|
|
68
|
+
// Only reuse the request if it's within the deduplication window
|
|
69
|
+
if (timeSinceRequest < DEDUPLICATION_WINDOW_MS) {
|
|
70
|
+
// Wait for the existing request and return its result
|
|
71
|
+
try {
|
|
72
|
+
const result = await existingEntry.promise;
|
|
73
|
+
resolve(result);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
reject(error);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
} else {
|
|
79
|
+
// Request is too old, remove it from the tracker
|
|
80
|
+
inflightRequests.delete(deduplicationKey);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
// Create a promise for this request and store it to deduplicate concurrent calls
|
|
85
|
+
const requestPromise = new Promise(async (resolveRequest, rejectRequest) => {
|
|
86
|
+
// If auth token is not provided and we are in the web environment,
|
|
87
|
+
// try to authenticate with Puter
|
|
88
|
+
if(!puter.authToken && puter.env === 'web'){
|
|
89
|
+
try{
|
|
90
|
+
await puter.ui.authenticateWithPuter();
|
|
91
|
+
}catch(e){
|
|
92
|
+
// if authentication fails, throw an error
|
|
93
|
+
rejectRequest('Authentication failed.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// create xhr object
|
|
99
|
+
const xhr = utils.initXhr('/stat', this.APIOrigin, undefined, "post", "text/plain;actually=json");
|
|
100
|
+
|
|
101
|
+
// set up event handlers for load and error events
|
|
102
|
+
utils.setupXhrEventHandlers(xhr, options.success, options.error, async (result) => {
|
|
103
|
+
// Calculate the size of the result for cache eligibility check
|
|
104
|
+
const resultSize = JSON.stringify(result).length;
|
|
105
|
+
|
|
106
|
+
// Cache the result if it's not bigger than MAX_CACHE_SIZE
|
|
107
|
+
const MAX_CACHE_SIZE = 20 * 1024 * 1024;
|
|
108
|
+
|
|
109
|
+
if(resultSize <= MAX_CACHE_SIZE){
|
|
110
|
+
// UPSERT the cache
|
|
111
|
+
puter._cache.set(cacheKey, result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
resolveRequest(result);
|
|
115
|
+
}, rejectRequest);
|
|
116
|
+
|
|
117
|
+
let dataToSend = {};
|
|
118
|
+
if (options.uid !== undefined) {
|
|
119
|
+
dataToSend.uid = options.uid;
|
|
120
|
+
} else if (options.path !== undefined) {
|
|
121
|
+
// If dirPath is not provided or it's not starting with a slash, it means it's a relative path
|
|
122
|
+
// in that case, we need to prepend the app's root directory to it
|
|
123
|
+
dataToSend.path = getAbsolutePathForApp(options.path);
|
|
67
124
|
}
|
|
68
125
|
|
|
126
|
+
dataToSend.return_subdomains = options.returnSubdomains;
|
|
127
|
+
dataToSend.return_permissions = options.returnPermissions;
|
|
128
|
+
dataToSend.return_versions = options.returnVersions;
|
|
129
|
+
dataToSend.return_size = options.returnSize;
|
|
130
|
+
dataToSend.auth_token = this.authToken;
|
|
131
|
+
|
|
132
|
+
xhr.send(JSON.stringify(dataToSend));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Store the promise and timestamp in the in-flight tracker
|
|
136
|
+
inflightRequests.set(deduplicationKey, {
|
|
137
|
+
promise: requestPromise,
|
|
138
|
+
timestamp: now,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Wait for the request to complete and clean up
|
|
142
|
+
try {
|
|
143
|
+
const result = await requestPromise;
|
|
144
|
+
inflightRequests.delete(deduplicationKey);
|
|
69
145
|
resolve(result);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (options.uid !== undefined) {
|
|
74
|
-
dataToSend.uid = options.uid;
|
|
75
|
-
} else if (options.path !== undefined) {
|
|
76
|
-
// If dirPath is not provided or it's not starting with a slash, it means it's a relative path
|
|
77
|
-
// in that case, we need to prepend the app's root directory to it
|
|
78
|
-
dataToSend.path = getAbsolutePathForApp(options.path);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
inflightRequests.delete(deduplicationKey);
|
|
148
|
+
reject(error);
|
|
79
149
|
}
|
|
80
|
-
|
|
81
|
-
dataToSend.return_subdomains = options.returnSubdomains;
|
|
82
|
-
dataToSend.return_permissions = options.returnPermissions;
|
|
83
|
-
dataToSend.return_versions = options.returnVersions;
|
|
84
|
-
dataToSend.return_size = options.returnSize;
|
|
85
|
-
dataToSend.auth_token = this.authToken;
|
|
86
|
-
|
|
87
|
-
xhr.send(JSON.stringify(dataToSend));
|
|
88
150
|
})
|
|
89
151
|
}
|
|
90
152
|
|
package/src/modules/UI.js
CHANGED
|
@@ -765,6 +765,12 @@ class UI extends EventListener {
|
|
|
765
765
|
})
|
|
766
766
|
}
|
|
767
767
|
|
|
768
|
+
requestUpgrade = function() {
|
|
769
|
+
return new Promise((resolve) => {
|
|
770
|
+
this.#postMessageWithCallback('requestUpgrade', resolve, { });
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
|
|
768
774
|
showSaveFilePicker = function(content, suggestedName, type){
|
|
769
775
|
const undefinedOnCancel = new putility.libs.promise.TeePromise();
|
|
770
776
|
const resolveOnlyPromise = new Promise((resolve, reject) => {
|
package/src/modules/Workers.js
CHANGED
|
@@ -97,7 +97,7 @@ export class WorkersHandler {
|
|
|
97
97
|
|
|
98
98
|
workerName = workerName.toLocaleLowerCase(); // just incase
|
|
99
99
|
// const driverCall = await puter.drivers.call("workers", "worker-service", "destroy", { authorization: puter.authToken, workerName });
|
|
100
|
-
const driverResult = await utils.make_driver_method(['authorization', 'workerName'], 'workers', "worker-service", 'destroy')(puter.authToken, workerName)
|
|
100
|
+
const driverResult = await utils.make_driver_method(['authorization', 'workerName'], 'workers', "worker-service", 'destroy')(puter.authToken, workerName);
|
|
101
101
|
|
|
102
102
|
if (!driverResult.result) {
|
|
103
103
|
if (!driverResult.result) {
|
|
@@ -116,5 +116,55 @@ export class WorkersHandler {
|
|
|
116
116
|
return true;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
async getLoggingHandle (workerName) {
|
|
121
|
+
const loggingEndpoint = await utils.make_driver_method([], 'workers', "worker-service", 'getLoggingUrl')(puter.authToken, workerName);
|
|
122
|
+
const socket = new WebSocket(`${loggingEndpoint}/${puter.authToken}/${workerName}`);
|
|
123
|
+
const logStreamObject = new EventTarget();
|
|
124
|
+
logStreamObject.onLog = (data) => { };
|
|
125
|
+
|
|
126
|
+
// Coercibility to ReadableStream
|
|
127
|
+
Object.defineProperty(logStreamObject, 'start', {
|
|
128
|
+
enumerable: false,
|
|
129
|
+
value: async (controller) => {
|
|
130
|
+
socket.addEventListener("message", (event) => {
|
|
131
|
+
controller.enqueue(JSON.parse(event.data));
|
|
132
|
+
});
|
|
133
|
+
socket.addEventListener("close", (event) => {
|
|
134
|
+
try {
|
|
135
|
+
controller.close();
|
|
136
|
+
} catch (e) { }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
Object.defineProperty(logStreamObject, 'cancel', {
|
|
141
|
+
enumerable: false,
|
|
142
|
+
value: async () => {
|
|
143
|
+
socket.close();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
socket.addEventListener("message", (event) => {
|
|
149
|
+
const logEvent = new MessageEvent("log", { data: JSON.parse(event.data) });
|
|
150
|
+
|
|
151
|
+
logStreamObject.dispatchEvent(logEvent)
|
|
152
|
+
logStreamObject.onLog(logEvent);
|
|
153
|
+
});
|
|
154
|
+
logStreamObject.close = socket.close;
|
|
155
|
+
return new Promise((res, rej) => {
|
|
156
|
+
let done = false;
|
|
157
|
+
socket.onopen = ()=>{
|
|
158
|
+
done = true;
|
|
159
|
+
res(logStreamObject);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
socket.onerror = () => {
|
|
163
|
+
if (!done) {
|
|
164
|
+
rej("Failed to open logging connection");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
119
169
|
|
|
120
170
|
}
|