@qikdev/sdk 1.0.8 → 1.0.10

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": "@qikdev/sdk",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "type": "module",
5
5
  "description": "Promise based Javascript SDK",
6
6
  "main": "./dist/main.js",
@@ -67,21 +67,23 @@ var QikAccess = function (QikCore) {
67
67
  * const childScopes = sdk.access.getAllDescendants({title:'Global', children:[{title:'Australia', ...}, {title:'New Zealand', ...}]});
68
68
  */
69
69
  service.getAllDescendants = function (scope) {
70
- //Start by including the scope id
71
- var results = [];
70
+ //Start by including the scope id. A Set because the same scope can be
71
+ //reachable by more than one path through the tree, and the duplicates
72
+ //were being carried all the way into hashPermissions.
73
+ var results = new Set();
72
74
 
73
75
  addScope(scope);
74
76
 
75
77
  function addScope(entry) {
76
78
  //Add this scope to the results
77
79
  var id = QikCore.utils.id(entry);
78
- results.push(id);
80
+ results.add(id);
79
81
 
80
82
  (entry.children || []).forEach(function (child) {
81
83
  addScope(child);
82
84
  });
83
85
  }
84
- return results;
86
+ return Array.from(results);
85
87
  };
86
88
 
87
89
  ///////////////////////////////////////////////
@@ -93,8 +95,48 @@ var QikAccess = function (QikCore) {
93
95
  * @example
94
96
  * const permissions = sdk.access.hashPermissions({firstName:'Mighty', lastName:'Mouse', permissionSets:[...], ...});
95
97
  */
98
+ // Memo for hashPermissions, keyed on the session's cacheKey.
99
+ //
100
+ // actionableScopes() calls hashPermissions() on every single invocation, and
101
+ // a dashboard navigation performs well over a hundred access checks. For an
102
+ // ordinary session that is invisible, but a heavy one — hundreds of scopes
103
+ // and thousands of permission strings — rebuilds a six-figure number of
104
+ // (permission, scope) pairs each time. Measured on one production session:
105
+ // 127 calls x 28ms = 3.6 SECONDS of synchronous main-thread work per
106
+ // navigation, which is the whole of an observed 3.6s UI freeze.
107
+ //
108
+ // cacheKey already changes whenever the session does (it encodes the user,
109
+ // organisation, persona and the persona's updated timestamp), so it is the
110
+ // same key the server memoises this on. Only one entry is kept: the session
111
+ // in hand is the only one being asked about, and holding more would pin
112
+ // permission trees for users who have since signed out.
113
+ // A short TTL as a backstop, mirroring the server's own memo. cacheKey moves
114
+ // whenever the session is reissued, but permissions can in principle change
115
+ // underneath a session that keeps its key — a role being edited, say, which
116
+ // the server handles by invalidating the session rather than by bumping the
117
+ // persona. The TTL means any such staleness heals on its own within seconds
118
+ // instead of lasting for the life of the tab, while still collapsing the
119
+ // hundred-plus calls a single navigation makes into one.
120
+ var PERMISSION_HASH_TTL_MS = 5000;
121
+ var permissionHashMemo = null;
122
+
96
123
  service.hashPermissions = function (user) {
97
- var results = {};
124
+ var cacheKey = user?.cacheKey;
125
+
126
+ if (
127
+ cacheKey &&
128
+ permissionHashMemo &&
129
+ permissionHashMemo.key === cacheKey &&
130
+ Date.now() - permissionHashMemo.at < PERMISSION_HASH_TTL_MS
131
+ ) {
132
+ return permissionHashMemo.value;
133
+ }
134
+
135
+ // Accumulate into Sets rather than rebuilding an array per permission
136
+ // string. The previous form did `[...new Set(existing.concat(allIDs))]`
137
+ // for every string in every scope — copy, rebuild, copy again — which is
138
+ // quadratic in the number of scopes granting that permission.
139
+ var accumulator = new Map();
98
140
 
99
141
  for (var key in user?.permissions || {}) {
100
142
  //Get the permission set
@@ -105,18 +147,28 @@ var QikAccess = function (QikCore) {
105
147
 
106
148
  //For each permission defined in the set
107
149
  (permissionSet.permissions || []).forEach(function (permissionString) {
108
- //Include the top scope
109
- if (!results[permissionString]) {
110
- results[permissionString] = [];
150
+ var set = accumulator.get(permissionString);
151
+ if (!set) {
152
+ set = new Set();
153
+ accumulator.set(permissionString, set);
154
+ }
155
+ for (var i = 0; i < allIDs.length; i++) {
156
+ set.add(allIDs[i]);
111
157
  }
112
- results[permissionString] = [
113
- ...new Set(results[permissionString].concat(allIDs)),
114
- ];
115
158
  });
116
159
  }
117
160
 
161
+ var results = {};
162
+ accumulator.forEach(function (set, permissionString) {
163
+ results[permissionString] = Array.from(set);
164
+ });
165
+
118
166
  ///////////////////////////////////////////////
119
167
 
168
+ if (cacheKey) {
169
+ permissionHashMemo = { key: cacheKey, value: results, at: Date.now() };
170
+ }
171
+
120
172
  return results;
121
173
  };
122
174
 
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "1.0.8";
1
+ export const version = "1.0.10";