@heyputer/puter.js 2.0.15 → 2.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyputer/puter.js",
3
- "version": "2.0.15",
3
+ "version": "2.1.1",
4
4
  "description": "Puter.js - A JavaScript library for interacting with Puter services.",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
@@ -40,6 +40,6 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@heyputer/kv.js": "^0.2.1",
43
- "@heyputer/putility": "^1.0.3"
43
+ "@heyputer/putility": "^1.1.1"
44
44
  }
45
45
  }
package/src/index.js CHANGED
@@ -506,7 +506,7 @@ const puterInit = (function() {
506
506
  this.request_rao_();
507
507
 
508
508
  // perform whoami and cache results
509
- puter.getUser().then((user) => {
509
+ this.getUser().then((user) => {
510
510
  this.whoami = user;
511
511
  });
512
512
  };
@@ -722,25 +722,50 @@ const puterInit = (function() {
722
722
  let documents_path = `/${username}/Documents`;
723
723
  let public_path = `/${username}/Public`;
724
724
 
725
- // Home
725
+ // item:Home
726
+ if(!puter._cache.get('item:' + home_path)){
727
+ console.log(`/${username} item is not cached, refetching cache`);
728
+ // fetch home
729
+ puter.fs.stat(home_path);
730
+ }
731
+ // item:Desktop
732
+ if(!puter._cache.get('item:' + desktop_path)){
733
+ console.log(`/${username}/Desktop item is not cached, refetching cache`);
734
+ // fetch desktop
735
+ puter.fs.stat(desktop_path);
736
+ }
737
+ // item:Documents
738
+ if(!puter._cache.get('item:' + documents_path)){
739
+ console.log(`/${username}/Documents item is not cached, refetching cache`);
740
+ // fetch documents
741
+ puter.fs.stat(documents_path);
742
+ }
743
+ // item:Public
744
+ if(!puter._cache.get('item:' + public_path)){
745
+ console.log(`/${username}/Public item is not cached, refetching cache`);
746
+ // fetch public
747
+ puter.fs.stat(public_path);
748
+ }
749
+
750
+ // readdir:Home
726
751
  if(!puter._cache.get('readdir:' + home_path)){
727
752
  console.log(`/${username} is not cached, refetching cache`);
728
753
  // fetch home
729
754
  puter.fs.readdir(home_path);
730
755
  }
731
- // Desktop
756
+ // readdir:Desktop
732
757
  if(!puter._cache.get('readdir:' + desktop_path)){
733
758
  console.log(`/${username}/Desktop is not cached, refetching cache`);
734
759
  // fetch desktop
735
760
  puter.fs.readdir(desktop_path);
736
761
  }
737
- // Documents
762
+ // readdir:Documents
738
763
  if(!puter._cache.get('readdir:' + documents_path)){
739
764
  console.log(`/${username}/Documents is not cached, refetching cache`);
740
765
  // fetch documents
741
766
  puter.fs.readdir(documents_path);
742
767
  }
743
- // Public
768
+ // readdir:Public
744
769
  if(!puter._cache.get('readdir:' + public_path)){
745
770
  console.log(`/${username}/Public is not cached, refetching cache`);
746
771
  // fetch public
@@ -1,11 +1,10 @@
1
- import * as utils from '../lib/utils.js'
1
+ import * as utils from '../lib/utils.js';
2
2
 
3
3
  class Auth{
4
4
  // Used to generate a unique message id for each message sent to the host environment
5
5
  // we start from 1 because 0 is falsy and we want to avoid that for the message id
6
6
  #messageID = 1;
7
7
 
8
-
9
8
  /**
10
9
  * Creates a new instance with the given authentication token, API origin, and app ID,
11
10
  *
@@ -14,7 +13,7 @@ class Auth{
14
13
  * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
15
14
  * @param {string} appID - ID of the app to use.
16
15
  */
17
- constructor (context) {
16
+ constructor(context) {
18
17
  this.authToken = context.authToken;
19
18
  this.APIOrigin = context.APIOrigin;
20
19
  this.appID = context.appID;
@@ -27,22 +26,22 @@ class Auth{
27
26
  * @memberof [Auth]
28
27
  * @returns {void}
29
28
  */
30
- setAuthToken (authToken) {
29
+ setAuthToken(authToken) {
31
30
  this.authToken = authToken;
32
31
  }
33
32
 
34
33
  /**
35
34
  * Sets the API origin.
36
- *
35
+ *
37
36
  * @param {string} APIOrigin - The new API origin.
38
37
  * @memberof [Auth]
39
38
  * @returns {void}
40
39
  */
41
- setAPIOrigin (APIOrigin) {
40
+ setAPIOrigin(APIOrigin) {
42
41
  this.APIOrigin = APIOrigin;
43
42
  }
44
-
45
- signIn = (options) =>{
43
+
44
+ signIn = (options) => {
46
45
  options = options || {};
47
46
 
48
47
  return new Promise((resolve, reject) => {
@@ -50,17 +49,17 @@ class Auth{
50
49
  let w = 600;
51
50
  let h = 600;
52
51
  let title = 'Puter';
53
- var left = (screen.width/2)-(w/2);
54
- var top = (screen.height/2)-(h/2);
55
-
52
+ var left = (screen.width / 2) - (w / 2);
53
+ var top = (screen.height / 2) - (h / 2);
54
+
56
55
  // Store reference to the popup window
57
- const popup = window.open(puter.defaultGUIOrigin + '/action/sign-in?embedded_in_popup=true&msg_id=' + msg_id + (window.crossOriginIsolated ? '&cross_origin_isolated=true' : '') +(options.attempt_temp_user_creation ? '&attempt_temp_user_creation=true' : ''),
58
- title,
59
- 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
56
+ const popup = window.open(`${puter.defaultGUIOrigin}/action/sign-in?embedded_in_popup=true&msg_id=${msg_id}${window.crossOriginIsolated ? '&cross_origin_isolated=true' : ''}${options.attempt_temp_user_creation ? '&attempt_temp_user_creation=true' : ''}`,
57
+ title,
58
+ `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${w}, height=${h}, top=${top}, left=${left}`);
60
59
 
61
60
  // Set up interval to check if popup was closed
62
61
  const checkClosed = setInterval(() => {
63
- if (popup.closed) {
62
+ if ( popup.closed ) {
64
63
  clearInterval(checkClosed);
65
64
  // Remove the message listener
66
65
  window.removeEventListener('message', messageHandler);
@@ -69,21 +68,23 @@ class Auth{
69
68
  }, 100);
70
69
 
71
70
  function messageHandler(e) {
72
- if(e.data.msg_id == msg_id){
71
+ if ( e.data.msg_id == msg_id ){
73
72
  // Clear the interval since we got a response
74
73
  clearInterval(checkClosed);
75
-
74
+
76
75
  // remove redundant attributes
77
76
  delete e.data.msg_id;
78
77
  delete e.data.msg;
79
78
 
80
- if(e.data.success){
79
+ if ( e.data.success ){
81
80
  // set the auth token
82
81
  puter.setAuthToken(e.data.token);
83
82
 
84
83
  resolve(e.data);
85
- }else
84
+ } else
85
+ {
86
86
  reject(e.data);
87
+ }
87
88
 
88
89
  // delete the listener
89
90
  window.removeEventListener('message', messageHandler);
@@ -92,20 +93,24 @@ class Auth{
92
93
 
93
94
  window.addEventListener('message', messageHandler);
94
95
  });
95
- }
96
+ };
96
97
 
97
- isSignedIn = () =>{
98
- if(puter.authToken)
98
+ isSignedIn = () => {
99
+ if ( puter.authToken )
100
+ {
99
101
  return true;
102
+ }
100
103
  else
104
+ {
101
105
  return false;
102
- }
106
+ }
107
+ };
103
108
 
104
109
  getUser = function(...args){
105
110
  let options;
106
111
 
107
112
  // If first argument is an object, it's the options
108
- if (typeof args[0] === 'object' && args[0] !== null) {
113
+ if ( typeof args[0] === 'object' && args[0] !== null ) {
109
114
  options = args[0];
110
115
  } else {
111
116
  // Otherwise, we assume separate arguments are provided
@@ -122,45 +127,125 @@ class Auth{
122
127
  utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
123
128
 
124
129
  xhr.send();
125
- })
126
- }
130
+ });
131
+ };
127
132
 
128
- signOut = () =>{
133
+ signOut = () => {
129
134
  puter.resetAuthToken();
130
- }
135
+ };
131
136
 
132
- async whoami () {
137
+ async whoami() {
133
138
  try {
134
- const resp = await fetch(this.APIOrigin + '/whoami', {
139
+ const resp = await fetch(`${this.APIOrigin}/whoami`, {
135
140
  headers: {
136
- Authorization: `Bearer ${this.authToken}`
137
- }
141
+ Authorization: `Bearer ${this.authToken}`,
142
+ },
138
143
  });
139
-
144
+
140
145
  const result = await resp.json();
141
-
146
+
142
147
  // Log the response
143
- if (globalThis.puter?.apiCallLogger?.isEnabled()) {
148
+ if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
144
149
  globalThis.puter.apiCallLogger.logRequest({
145
150
  service: 'auth',
146
151
  operation: 'whoami',
147
152
  params: {},
148
- result: result
153
+ result: result,
149
154
  });
150
155
  }
151
-
156
+
152
157
  return result;
153
- } catch (error) {
158
+ } catch( error ) {
154
159
  // Log the error
155
- if (globalThis.puter?.apiCallLogger?.isEnabled()) {
160
+ if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
156
161
  globalThis.puter.apiCallLogger.logRequest({
157
162
  service: 'auth',
158
163
  operation: 'whoami',
159
164
  params: {},
160
165
  error: {
161
166
  message: error.message || error.toString(),
162
- stack: error.stack
163
- }
167
+ stack: error.stack,
168
+ },
169
+ });
170
+ }
171
+ throw error;
172
+ }
173
+ }
174
+
175
+ async getMonthlyUsage() {
176
+ try {
177
+ const resp = await fetch(`${this.APIOrigin}/metering/usage`, {
178
+ headers: {
179
+ Authorization: `Bearer ${this.authToken}`,
180
+ },
181
+ });
182
+
183
+ const result = await resp.json();
184
+
185
+ // Log the response
186
+ if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
187
+ globalThis.puter.apiCallLogger.logRequest({
188
+ service: 'auth',
189
+ operation: 'usage',
190
+ params: {},
191
+ result: result,
192
+ });
193
+ }
194
+
195
+ return result;
196
+ } catch( error ) {
197
+ // Log the error
198
+ if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
199
+ globalThis.puter.apiCallLogger.logRequest({
200
+ service: 'auth',
201
+ operation: 'usage',
202
+ params: {},
203
+ error: {
204
+ message: error.message || error.toString(),
205
+ stack: error.stack,
206
+ },
207
+ });
208
+ }
209
+ throw error;
210
+ }
211
+ }
212
+
213
+ async getDetailedAppUsage(appId) {
214
+ if ( !appId ) {
215
+ throw new Error('appId is required');
216
+ }
217
+
218
+ try {
219
+ const resp = await fetch(`${this.APIOrigin}/metering/usage/${appId}`, {
220
+ headers: {
221
+ Authorization: `Bearer ${this.authToken}`,
222
+ },
223
+ });
224
+
225
+ const result = await resp.json();
226
+
227
+ // Log the response
228
+ if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
229
+ globalThis.puter.apiCallLogger.logRequest({
230
+ service: 'auth',
231
+ operation: 'detailed_app_usage',
232
+ params: { appId },
233
+ result: result,
234
+ });
235
+ }
236
+
237
+ return result;
238
+ } catch( error ) {
239
+ // Log the error
240
+ if ( globalThis.puter?.apiCallLogger?.isEnabled() ) {
241
+ globalThis.puter.apiCallLogger.logRequest({
242
+ service: 'auth',
243
+ operation: 'detailed_app_usage',
244
+ params: { appId },
245
+ error: {
246
+ message: error.message || error.toString(),
247
+ stack: error.stack,
248
+ },
164
249
  });
165
250
  }
166
251
  throw error;
@@ -168,4 +253,4 @@ class Auth{
168
253
  }
169
254
  }
170
255
 
171
- export default Auth
256
+ export default Auth;
@@ -134,8 +134,12 @@ export class PuterJSFileSystemModule extends AdvancedBase {
134
134
  });
135
135
 
136
136
  this.socket.on('item.added', (item) => {
137
- puter._cache.flushall();
138
- console.log('Flushed cache for item.added');
137
+ // remove readdir cache for parent
138
+ puter._cache.del('readdir:' + path.dirname(item.path));
139
+ console.log('deleted cache for readdir:' + path.dirname(item.path));
140
+ // remove item cache for parent directory
141
+ puter._cache.del('item:' + path.dirname(item.path));
142
+ console.log('deleted cache for item:' + path.dirname(item.path));
139
143
  });
140
144
 
141
145
  this.socket.on('item.updated', (item) => {
@@ -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
  }