@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.
@@ -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
- // If auth token is not provided and we are in the web environment,
46
- // try to authenticate with Puter
47
- if(!puter.authToken && puter.env === 'web'){
48
- try{
49
- await puter.ui.authenticateWithPuter();
50
- }catch(e){
51
- // if authentication fails, throw an error
52
- reject('Authentication failed.');
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
- // create xhr object
57
- const xhr = utils.initXhr('/readdir', this.APIOrigin, undefined, "post", "text/plain;actually=json");
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
- // set up event handlers for load and error events
60
- utils.setupXhrEventHandlers(xhr, options.success, options.error, async (result) => {
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
- if(resultSize <= MAX_CACHE_SIZE){
68
- // UPSERT the cache
69
- puter._cache.set(cacheKey, result);
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
- // set each individual item's cache
73
- for(const item of result){
74
- puter._cache.set('item:' + item.path, item);
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
- // Build request payload - support both path and uid parameters
81
- const payload = {
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
- // Add either uid or path to the payload
88
- if (options.uid) {
89
- payload.uid = options.uid;
90
- } else if (options.path) {
91
- payload.path = getAbsolutePathForApp(options.path);
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
- xhr.send(JSON.stringify(payload));
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
- // create xhr object
54
- const xhr = utils.initXhr('/stat', this.APIOrigin, undefined, "post", "text/plain;actually=json");
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
- // set up event handlers for load and error events
57
- utils.setupXhrEventHandlers(xhr, options.success, options.error, async (result) => {
58
- // Calculate the size of the result for cache eligibility check
59
- const resultSize = JSON.stringify(result).length;
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
- // Cache the result if it's not bigger than MAX_CACHE_SIZE
62
- const MAX_CACHE_SIZE = 20 * 1024 * 1024;
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
- if(resultSize <= MAX_CACHE_SIZE){
65
- // UPSERT the cache
66
- puter._cache.set(cacheKey, result);
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
- }, reject);
71
-
72
- let dataToSend = {};
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) => {
@@ -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
  }