@manifest-editor/server-vault 1.0.2
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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/index.cjs +240 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +204 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 digirati-co-uk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Vault
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
**.vaultfile**
|
|
6
|
+
```
|
|
7
|
+
{
|
|
8
|
+
"source": { "id": "https://example.org/manifest", "type": "Manifest" },
|
|
9
|
+
"resources": {
|
|
10
|
+
"https://example.org/manifest": "./manifest.json",
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
The server vault will take this optional config and use it to load the manifest from the file system. The instance of
|
|
17
|
+
vault will live on the server for a period of time and can be stopped or started.
|
|
18
|
+
|
|
19
|
+
Multiple clients can then connect to the vault and use the manifest. They can connect as a read-only user or as a
|
|
20
|
+
read-write user. The read-write user can make changes to the manifest and save it back to the vault. These changes will
|
|
21
|
+
be reflected in the vault and will be available to all users. The changes will also be reflected in the file system
|
|
22
|
+
where the manifest is stored.
|
|
23
|
+
|
|
24
|
+
Vault communicates using atomic actions. These actions are sent to the vault and the vault will respond with the result
|
|
25
|
+
of the action. The vault will also broadcast the action to all connected clients. The clients can then update their
|
|
26
|
+
state based on the action. If the actions are applied in the same order on all clients then the state of the clients
|
|
27
|
+
will be the same.
|
|
28
|
+
|
|
29
|
+
Each action will have a unique id. This id will be used to ensure that the actions are applied in the same order on all
|
|
30
|
+
clients. Clients are required to send the previous action id with each action. The vault will then check that the
|
|
31
|
+
previous action id matches the last action id that was applied to the vault. If the previous action id does not match
|
|
32
|
+
then the action will be rejected. The client will then need to update its state to match the vault and then try again.
|
|
33
|
+
|
|
34
|
+
## Post Message communication
|
|
35
|
+
|
|
36
|
+
The server will also host a webpage that can be used to communicate with the vault. This webpage will be loaded in an
|
|
37
|
+
iframe on the client. The client will then be able to connect to the vault and send actions to the vault. The vault
|
|
38
|
+
will then broadcast the actions to all connected clients. The client will then update its state based on the actions.
|
|
39
|
+
|
|
40
|
+
This allows the client to be a static webpage that can be hosted on any server. The client will not need to be hosted
|
|
41
|
+
on the same server as the vault. However, the server will need to allow cross-origin requests from the client, and will
|
|
42
|
+
ensure that only select hosts are allowed to connect to the vault.
|
|
43
|
+
|
|
44
|
+
For post-message you have the server iframe, communicating to the server via web-sockets and the client application
|
|
45
|
+
that has the server in an iframe. When an event originates from the client, the following happens:
|
|
46
|
+
|
|
47
|
+
- The client sends a message to the server iframe via post-message
|
|
48
|
+
- The server iframe sends a message to the server via web-sockets
|
|
49
|
+
- The server broadcasts the message to all connected clients via web-sockets
|
|
50
|
+
- The server iframe receives the message from the server via web-sockets
|
|
51
|
+
- The server iframe sends the message to the other clients via post-message
|
|
52
|
+
|
|
53
|
+
When an event originates from the server, the following happens:
|
|
54
|
+
- The server sends a message to the server iframe via web-sockets
|
|
55
|
+
- The server iframe sends the message to the other clients via post-message
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
- ClientPostMessageVault.dispatch() -> sends message to server iframe
|
|
59
|
+
- ServerPostMessageVault.dispatch() -> sends message to server
|
|
60
|
+
- ServerWebSocketVault.dispatch() -> sends message to all clients
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## GitHub integration
|
|
65
|
+
|
|
66
|
+
On the server there will be a GitHub integration. This integration will allow the vault to be connected to a GitHub
|
|
67
|
+
repository. The vault will then be able to load the manifest from the GitHub repository. The vault will also be able
|
|
68
|
+
to save the manifest to the GitHub repository.
|
|
69
|
+
|
|
70
|
+
As changes are made to the manifest the vault will commit the changes to the GitHub repository. The server will also
|
|
71
|
+
create a pull request for the changes.
|
|
72
|
+
|
|
73
|
+
When a client requests a vault instance, the server will create a new branch in the GitHub repository. The vault will
|
|
74
|
+
then load the manifest from the new branch. The client will then be able to make changes to the manifest. Alternatively
|
|
75
|
+
the client can request a read-only instance of the vault. The client will then be able to view the manifest but will
|
|
76
|
+
not be able to make changes to the manifest.
|
|
77
|
+
|
|
78
|
+
Clients can also request an existing branch. The vault will then load the manifest from the existing branch. The client
|
|
79
|
+
will then be able to make changes to the manifest. The client will also be able to save the changes to the existing
|
|
80
|
+
branch. If other clients are also connected to the vault then they will be able to see the changes that the client has
|
|
81
|
+
made.
|
|
82
|
+
|
|
83
|
+
## Authentication
|
|
84
|
+
|
|
85
|
+
On the client the user will be able to login to GitHub through the server. This will be done by opening a popup window served by the server. The user will then be able to login to GitHub and grant the server access to their GitHub account.
|
|
86
|
+
Initially this will only be for authentication and will grant access to manifests hosted in SQLite on the server. Eventually
|
|
87
|
+
this will also allow the server to access manifests hosted in GitHub repositories.
|
|
88
|
+
|
|
89
|
+
Once logged in the server will redirect back to the client. The client will then be able to use the access token to
|
|
90
|
+
create an iFrame that can be used to communicate with the vault.
|
|
91
|
+
|
|
92
|
+
## Listing projects
|
|
93
|
+
|
|
94
|
+
Before a client can connect to a vault it will need to know the id of the vault. The client will be able to request a
|
|
95
|
+
list of vaults from the server. This is also done using a post message. The server will then respond with a list of
|
|
96
|
+
vaults that the user has access to. The client will then be able to connect to the vault.
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Server responsibilities and routes
|
|
100
|
+
|
|
101
|
+
- [ ] Request login status of the user using a post message
|
|
102
|
+
- [ ] Request a list of vaults using a post message
|
|
103
|
+
- [ ] Request a vault instance using a post message
|
|
104
|
+
- [ ] Login via Github in a popup
|
|
105
|
+
- [ ] Create a new vault instance
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
2 new types of Vault instance
|
|
109
|
+
|
|
110
|
+
- Server vault
|
|
111
|
+
- Client vault
|
|
112
|
+
|
|
113
|
+
**Server vault**
|
|
114
|
+
1. Creates normal internal vault instance
|
|
115
|
+
2. Loads manifest from file system
|
|
116
|
+
3. Handles client opening and closing connections
|
|
117
|
+
4. Accepts client actions
|
|
118
|
+
- Applies action to internal vault instance
|
|
119
|
+
- Responds to client with result of action
|
|
120
|
+
- Broadcasts action to all connected clients
|
|
121
|
+
5. Saves manifest to file system periodically
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
**Client vault**
|
|
125
|
+
1. Connects to server vault
|
|
126
|
+
- Opens connection
|
|
127
|
+
- Loads initial state from server vault, including last action id
|
|
128
|
+
2. Sends actions to server vault
|
|
129
|
+
- Includes last action id
|
|
130
|
+
- Waits for response from server vault
|
|
131
|
+
- Updates state based on response from server vault
|
|
132
|
+
- If response is an error then update state to match server vault
|
|
133
|
+
- May retry action if response is a conflict
|
|
134
|
+
3. Receives actions from server vault
|
|
135
|
+
4. Applies actions to internal vault instance
|
|
136
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
var __publicField = (obj, key, value) => {
|
|
31
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/index.ts
|
|
36
|
+
var src_exports = {};
|
|
37
|
+
__export(src_exports, {
|
|
38
|
+
MultiVaultServer: () => MultiVaultServer,
|
|
39
|
+
ServerVault: () => ServerVault
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(src_exports);
|
|
42
|
+
|
|
43
|
+
// src/server-vault.ts
|
|
44
|
+
var import_vault = require("@iiif/helpers/vault");
|
|
45
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
46
|
+
var import_uuid = require("uuid");
|
|
47
|
+
var import_timers = require("timers");
|
|
48
|
+
var GenesisId = "@genesis";
|
|
49
|
+
var ServerVault = class {
|
|
50
|
+
constructor(options = {}) {
|
|
51
|
+
__publicField(this, "vault");
|
|
52
|
+
__publicField(this, "ws");
|
|
53
|
+
__publicField(this, "lastActionId", GenesisId);
|
|
54
|
+
__publicField(this, "pingInterval");
|
|
55
|
+
__publicField(this, "_handleMessage", /* @__PURE__ */ new Map());
|
|
56
|
+
__publicField(this, "_handleOpen", /* @__PURE__ */ new Map());
|
|
57
|
+
__publicField(this, "_handleClose", /* @__PURE__ */ new Map());
|
|
58
|
+
__publicField(this, "setupWebSocket", (socket) => {
|
|
59
|
+
socket.isAlive = true;
|
|
60
|
+
socket.on("pong", () => {
|
|
61
|
+
socket.isAlive = true;
|
|
62
|
+
});
|
|
63
|
+
socket.on("open", this.handleOpen(socket));
|
|
64
|
+
socket.on("message", this.handleMessage(socket));
|
|
65
|
+
socket.on("close", this.handleClose(socket));
|
|
66
|
+
socket.send(this.createMessage("init-response", { data: this.vault.getState() }));
|
|
67
|
+
});
|
|
68
|
+
__publicField(this, "handleMessage", (socket) => {
|
|
69
|
+
if (this._handleMessage.has(socket)) {
|
|
70
|
+
return this._handleMessage.get(socket);
|
|
71
|
+
}
|
|
72
|
+
const handler = (data, isBinary) => {
|
|
73
|
+
const parsed = JSON.parse(data.toString());
|
|
74
|
+
if (parsed._type === "init-request") {
|
|
75
|
+
socket.send(this.createMessage("init-response", { data: this.vault.getState() }));
|
|
76
|
+
}
|
|
77
|
+
if (parsed._type === "vault-action") {
|
|
78
|
+
try {
|
|
79
|
+
this.vault.dispatch(parsed.action);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
socket.send(
|
|
82
|
+
this.createMessage("vault-action-rejection", {
|
|
83
|
+
action: parsed._id
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.lastActionId = parsed._id;
|
|
89
|
+
this.rebroadcast(socket, data, parsed, isBinary);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
this._handleMessage.set(socket, handler);
|
|
93
|
+
return handler;
|
|
94
|
+
});
|
|
95
|
+
__publicField(this, "send", (data) => {
|
|
96
|
+
this.ws.clients.forEach(function each(client) {
|
|
97
|
+
if (client.readyState === import_ws.default.OPEN) {
|
|
98
|
+
client.send(data, { binary: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
__publicField(this, "rebroadcast", (source, data, action, isBinary = true) => {
|
|
103
|
+
this.ws.clients.forEach((client) => {
|
|
104
|
+
if (client !== source && client.readyState === import_ws.default.OPEN) {
|
|
105
|
+
client.send(data, { binary: isBinary });
|
|
106
|
+
} else {
|
|
107
|
+
client.send(
|
|
108
|
+
this.createMessage("vault-action-confirmation", {
|
|
109
|
+
action: action._id
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
__publicField(this, "handleOpen", (socket) => {
|
|
116
|
+
if (this._handleOpen.has(socket)) {
|
|
117
|
+
return this._handleOpen.get(socket);
|
|
118
|
+
}
|
|
119
|
+
const handler = () => {
|
|
120
|
+
};
|
|
121
|
+
this._handleOpen.set(socket, handler);
|
|
122
|
+
return handler;
|
|
123
|
+
});
|
|
124
|
+
__publicField(this, "handleClose", (socket) => {
|
|
125
|
+
if (this._handleClose.has(socket)) {
|
|
126
|
+
return this._handleClose.get(socket);
|
|
127
|
+
}
|
|
128
|
+
const handler = () => {
|
|
129
|
+
};
|
|
130
|
+
this._handleClose.set(socket, handler);
|
|
131
|
+
return handler;
|
|
132
|
+
});
|
|
133
|
+
__publicField(this, "createMessage", (type, message) => {
|
|
134
|
+
return JSON.stringify({
|
|
135
|
+
_id: (0, import_uuid.v4)(),
|
|
136
|
+
_type: type,
|
|
137
|
+
_lastActionId: this.lastActionId,
|
|
138
|
+
...message
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
this.vault = new import_vault.Vault();
|
|
142
|
+
this.ws = new import_ws.WebSocketServer({ noServer: true });
|
|
143
|
+
this.ws.on("connection", this.setupWebSocket);
|
|
144
|
+
this.pingInterval = (0, import_timers.setInterval)(() => {
|
|
145
|
+
this.ws.clients.forEach((socket) => {
|
|
146
|
+
if (socket.isAlive === false) {
|
|
147
|
+
return socket.terminate();
|
|
148
|
+
}
|
|
149
|
+
socket.isAlive = false;
|
|
150
|
+
socket.ping();
|
|
151
|
+
});
|
|
152
|
+
}, options.pingTimeout || 3e4);
|
|
153
|
+
this.ws.on("close", () => {
|
|
154
|
+
(0, import_timers.clearInterval)(this.pingInterval);
|
|
155
|
+
if (options.onClose) {
|
|
156
|
+
options.onClose();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
this.ws.on("error", (err) => {
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async close() {
|
|
163
|
+
clearTimeout(this.pingInterval);
|
|
164
|
+
return new Promise((resolve, error) => {
|
|
165
|
+
this.ws.close((err) => {
|
|
166
|
+
if (err) {
|
|
167
|
+
error(err);
|
|
168
|
+
}
|
|
169
|
+
resolve();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/server.ts
|
|
176
|
+
var import_http = require("http");
|
|
177
|
+
var MultiVaultServer = class {
|
|
178
|
+
constructor(server) {
|
|
179
|
+
__publicField(this, "server");
|
|
180
|
+
__publicField(this, "vaults", /* @__PURE__ */ new Map());
|
|
181
|
+
__publicField(this, "handleRequest", (request, response) => {
|
|
182
|
+
if (request.url === "/vault") {
|
|
183
|
+
const vaults = Array.from(this.vaults.keys());
|
|
184
|
+
const json = JSON.stringify({ vaults });
|
|
185
|
+
response.writeHead(200, {
|
|
186
|
+
"Content-Length": Buffer.byteLength(json),
|
|
187
|
+
"Content-Type": "application/json"
|
|
188
|
+
});
|
|
189
|
+
response.write(json);
|
|
190
|
+
response.end();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
response.writeHead(404);
|
|
194
|
+
response.end();
|
|
195
|
+
});
|
|
196
|
+
__publicField(this, "handleUpgrade", (request, socket, head) => {
|
|
197
|
+
console.log("ws.url", request.url);
|
|
198
|
+
const pathname = request.url;
|
|
199
|
+
if (pathname && pathname.startsWith("/vault")) {
|
|
200
|
+
const vaultId = pathname.replace("/vault/", "");
|
|
201
|
+
let vault = this.vaults.get(vaultId);
|
|
202
|
+
if (!vault) {
|
|
203
|
+
const newVault = new ServerVault();
|
|
204
|
+
console.log("new vault", vaultId);
|
|
205
|
+
this.vaults.set(vaultId, newVault);
|
|
206
|
+
vault = newVault;
|
|
207
|
+
}
|
|
208
|
+
if (vault) {
|
|
209
|
+
const theVault = vault;
|
|
210
|
+
theVault.ws.handleUpgrade(request, socket, head, (ws) => {
|
|
211
|
+
console.log("new client", vaultId);
|
|
212
|
+
theVault.ws.emit("connection", ws, request);
|
|
213
|
+
theVault.ws.on("close", () => {
|
|
214
|
+
if (theVault.ws.clients.size === 0) {
|
|
215
|
+
console.log("destroying vault", vaultId);
|
|
216
|
+
this.vaults.delete(vaultId);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
socket.destroy();
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
226
|
+
socket.destroy();
|
|
227
|
+
});
|
|
228
|
+
this.server = server || (0, import_http.createServer)();
|
|
229
|
+
this.server.on("upgrade", this.handleUpgrade);
|
|
230
|
+
this.server.on("request", this.handleRequest);
|
|
231
|
+
}
|
|
232
|
+
listen(port, hostname, backlog, listeningListener) {
|
|
233
|
+
this.server.listen(port, hostname, backlog, listeningListener);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
237
|
+
0 && (module.exports = {
|
|
238
|
+
MultiVaultServer,
|
|
239
|
+
ServerVault
|
|
240
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Vault, AllActions, IIIFStore } from '@iiif/helpers/vault';
|
|
2
|
+
import { BatchAction } from '@iiif/helpers/vault/actions';
|
|
3
|
+
import { Collection, Manifest } from '@iiif/presentation-3';
|
|
4
|
+
import WebSocket, { WebSocketServer, RawData } from 'ws';
|
|
5
|
+
import { Server, IncomingMessage } from 'http';
|
|
6
|
+
import { Socket } from 'net';
|
|
7
|
+
|
|
8
|
+
type RemoteAction = {
|
|
9
|
+
_id: string;
|
|
10
|
+
_type: "vault-action";
|
|
11
|
+
_lastActionId: string;
|
|
12
|
+
action: AllActions | BatchAction;
|
|
13
|
+
};
|
|
14
|
+
type ClientInitAction = {
|
|
15
|
+
_id: string;
|
|
16
|
+
_type: "init-response";
|
|
17
|
+
_lastActionId: string;
|
|
18
|
+
data: IIIFStore;
|
|
19
|
+
};
|
|
20
|
+
type ClientInitRequestAction = {
|
|
21
|
+
_type: "init-request";
|
|
22
|
+
};
|
|
23
|
+
type RemoteActionConfirmation = {
|
|
24
|
+
_id: string;
|
|
25
|
+
_type: "vault-action-confirmation";
|
|
26
|
+
_lastActionId: string;
|
|
27
|
+
action: string;
|
|
28
|
+
};
|
|
29
|
+
type RemoteActionRejection = {
|
|
30
|
+
_id: string;
|
|
31
|
+
_type: "vault-action-rejection";
|
|
32
|
+
_lastActionId: string;
|
|
33
|
+
action: string;
|
|
34
|
+
};
|
|
35
|
+
type InitialiseWithResourceAction = {
|
|
36
|
+
_id: string;
|
|
37
|
+
_type: "initialise-with-resource";
|
|
38
|
+
_lastActionId: string;
|
|
39
|
+
resource: Collection | Manifest;
|
|
40
|
+
};
|
|
41
|
+
type EmptyVaultResponse = {
|
|
42
|
+
_id: string;
|
|
43
|
+
_type: "empty-vault-response";
|
|
44
|
+
_lastActionId: string;
|
|
45
|
+
};
|
|
46
|
+
type ClientAction = RemoteAction | ClientInitRequestAction | InitialiseWithResourceAction;
|
|
47
|
+
type ServerAction = RemoteAction | RemoteActionConfirmation | RemoteActionRejection | ClientInitAction | EmptyVaultResponse;
|
|
48
|
+
declare module "ws" {
|
|
49
|
+
interface WebSocket {
|
|
50
|
+
isAlive: boolean;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
interface ServerVaultOptions {
|
|
54
|
+
pingTimeout?: number;
|
|
55
|
+
onClose?: () => void;
|
|
56
|
+
}
|
|
57
|
+
declare class ServerVault {
|
|
58
|
+
vault: Vault;
|
|
59
|
+
ws: WebSocketServer;
|
|
60
|
+
lastActionId: string;
|
|
61
|
+
pingInterval: NodeJS.Timeout;
|
|
62
|
+
private _handleMessage;
|
|
63
|
+
private _handleOpen;
|
|
64
|
+
private _handleClose;
|
|
65
|
+
constructor(options?: ServerVaultOptions);
|
|
66
|
+
close(): Promise<void>;
|
|
67
|
+
setupWebSocket: (socket: WebSocket.WebSocket) => void;
|
|
68
|
+
handleMessage: (socket: WebSocket) => any;
|
|
69
|
+
send: (data: RawData) => void;
|
|
70
|
+
rebroadcast: (source: WebSocket, data: RawData, action: RemoteAction, isBinary?: boolean) => void;
|
|
71
|
+
handleOpen: (socket: WebSocket) => any;
|
|
72
|
+
handleClose: (socket: WebSocket) => any;
|
|
73
|
+
createMessage: (type: string, message: any) => string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare class MultiVaultServer {
|
|
77
|
+
server: Server;
|
|
78
|
+
vaults: Map<string, ServerVault>;
|
|
79
|
+
constructor(server?: Server);
|
|
80
|
+
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
|
81
|
+
handleRequest: (request: IncomingMessage, response: any) => void;
|
|
82
|
+
handleUpgrade: (request: IncomingMessage, socket: Socket, head: Buffer) => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { type ClientAction, MultiVaultServer, type ServerAction, ServerVault, type ServerVaultOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Vault, AllActions, IIIFStore } from '@iiif/helpers/vault';
|
|
2
|
+
import { BatchAction } from '@iiif/helpers/vault/actions';
|
|
3
|
+
import { Collection, Manifest } from '@iiif/presentation-3';
|
|
4
|
+
import WebSocket, { WebSocketServer, RawData } from 'ws';
|
|
5
|
+
import { Server, IncomingMessage } from 'http';
|
|
6
|
+
import { Socket } from 'net';
|
|
7
|
+
|
|
8
|
+
type RemoteAction = {
|
|
9
|
+
_id: string;
|
|
10
|
+
_type: "vault-action";
|
|
11
|
+
_lastActionId: string;
|
|
12
|
+
action: AllActions | BatchAction;
|
|
13
|
+
};
|
|
14
|
+
type ClientInitAction = {
|
|
15
|
+
_id: string;
|
|
16
|
+
_type: "init-response";
|
|
17
|
+
_lastActionId: string;
|
|
18
|
+
data: IIIFStore;
|
|
19
|
+
};
|
|
20
|
+
type ClientInitRequestAction = {
|
|
21
|
+
_type: "init-request";
|
|
22
|
+
};
|
|
23
|
+
type RemoteActionConfirmation = {
|
|
24
|
+
_id: string;
|
|
25
|
+
_type: "vault-action-confirmation";
|
|
26
|
+
_lastActionId: string;
|
|
27
|
+
action: string;
|
|
28
|
+
};
|
|
29
|
+
type RemoteActionRejection = {
|
|
30
|
+
_id: string;
|
|
31
|
+
_type: "vault-action-rejection";
|
|
32
|
+
_lastActionId: string;
|
|
33
|
+
action: string;
|
|
34
|
+
};
|
|
35
|
+
type InitialiseWithResourceAction = {
|
|
36
|
+
_id: string;
|
|
37
|
+
_type: "initialise-with-resource";
|
|
38
|
+
_lastActionId: string;
|
|
39
|
+
resource: Collection | Manifest;
|
|
40
|
+
};
|
|
41
|
+
type EmptyVaultResponse = {
|
|
42
|
+
_id: string;
|
|
43
|
+
_type: "empty-vault-response";
|
|
44
|
+
_lastActionId: string;
|
|
45
|
+
};
|
|
46
|
+
type ClientAction = RemoteAction | ClientInitRequestAction | InitialiseWithResourceAction;
|
|
47
|
+
type ServerAction = RemoteAction | RemoteActionConfirmation | RemoteActionRejection | ClientInitAction | EmptyVaultResponse;
|
|
48
|
+
declare module "ws" {
|
|
49
|
+
interface WebSocket {
|
|
50
|
+
isAlive: boolean;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
interface ServerVaultOptions {
|
|
54
|
+
pingTimeout?: number;
|
|
55
|
+
onClose?: () => void;
|
|
56
|
+
}
|
|
57
|
+
declare class ServerVault {
|
|
58
|
+
vault: Vault;
|
|
59
|
+
ws: WebSocketServer;
|
|
60
|
+
lastActionId: string;
|
|
61
|
+
pingInterval: NodeJS.Timeout;
|
|
62
|
+
private _handleMessage;
|
|
63
|
+
private _handleOpen;
|
|
64
|
+
private _handleClose;
|
|
65
|
+
constructor(options?: ServerVaultOptions);
|
|
66
|
+
close(): Promise<void>;
|
|
67
|
+
setupWebSocket: (socket: WebSocket.WebSocket) => void;
|
|
68
|
+
handleMessage: (socket: WebSocket) => any;
|
|
69
|
+
send: (data: RawData) => void;
|
|
70
|
+
rebroadcast: (source: WebSocket, data: RawData, action: RemoteAction, isBinary?: boolean) => void;
|
|
71
|
+
handleOpen: (socket: WebSocket) => any;
|
|
72
|
+
handleClose: (socket: WebSocket) => any;
|
|
73
|
+
createMessage: (type: string, message: any) => string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare class MultiVaultServer {
|
|
77
|
+
server: Server;
|
|
78
|
+
vaults: Map<string, ServerVault>;
|
|
79
|
+
constructor(server?: Server);
|
|
80
|
+
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
|
81
|
+
handleRequest: (request: IncomingMessage, response: any) => void;
|
|
82
|
+
handleUpgrade: (request: IncomingMessage, socket: Socket, head: Buffer) => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { type ClientAction, MultiVaultServer, type ServerAction, ServerVault, type ServerVaultOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => {
|
|
4
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/server-vault.ts
|
|
9
|
+
import { Vault } from "@iiif/helpers/vault";
|
|
10
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
11
|
+
import { v4 } from "uuid";
|
|
12
|
+
import { clearInterval, setInterval } from "timers";
|
|
13
|
+
var GenesisId = "@genesis";
|
|
14
|
+
var ServerVault = class {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
__publicField(this, "vault");
|
|
17
|
+
__publicField(this, "ws");
|
|
18
|
+
__publicField(this, "lastActionId", GenesisId);
|
|
19
|
+
__publicField(this, "pingInterval");
|
|
20
|
+
__publicField(this, "_handleMessage", /* @__PURE__ */ new Map());
|
|
21
|
+
__publicField(this, "_handleOpen", /* @__PURE__ */ new Map());
|
|
22
|
+
__publicField(this, "_handleClose", /* @__PURE__ */ new Map());
|
|
23
|
+
__publicField(this, "setupWebSocket", (socket) => {
|
|
24
|
+
socket.isAlive = true;
|
|
25
|
+
socket.on("pong", () => {
|
|
26
|
+
socket.isAlive = true;
|
|
27
|
+
});
|
|
28
|
+
socket.on("open", this.handleOpen(socket));
|
|
29
|
+
socket.on("message", this.handleMessage(socket));
|
|
30
|
+
socket.on("close", this.handleClose(socket));
|
|
31
|
+
socket.send(this.createMessage("init-response", { data: this.vault.getState() }));
|
|
32
|
+
});
|
|
33
|
+
__publicField(this, "handleMessage", (socket) => {
|
|
34
|
+
if (this._handleMessage.has(socket)) {
|
|
35
|
+
return this._handleMessage.get(socket);
|
|
36
|
+
}
|
|
37
|
+
const handler = (data, isBinary) => {
|
|
38
|
+
const parsed = JSON.parse(data.toString());
|
|
39
|
+
if (parsed._type === "init-request") {
|
|
40
|
+
socket.send(this.createMessage("init-response", { data: this.vault.getState() }));
|
|
41
|
+
}
|
|
42
|
+
if (parsed._type === "vault-action") {
|
|
43
|
+
try {
|
|
44
|
+
this.vault.dispatch(parsed.action);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
socket.send(
|
|
47
|
+
this.createMessage("vault-action-rejection", {
|
|
48
|
+
action: parsed._id
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.lastActionId = parsed._id;
|
|
54
|
+
this.rebroadcast(socket, data, parsed, isBinary);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
this._handleMessage.set(socket, handler);
|
|
58
|
+
return handler;
|
|
59
|
+
});
|
|
60
|
+
__publicField(this, "send", (data) => {
|
|
61
|
+
this.ws.clients.forEach(function each(client) {
|
|
62
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
63
|
+
client.send(data, { binary: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
__publicField(this, "rebroadcast", (source, data, action, isBinary = true) => {
|
|
68
|
+
this.ws.clients.forEach((client) => {
|
|
69
|
+
if (client !== source && client.readyState === WebSocket.OPEN) {
|
|
70
|
+
client.send(data, { binary: isBinary });
|
|
71
|
+
} else {
|
|
72
|
+
client.send(
|
|
73
|
+
this.createMessage("vault-action-confirmation", {
|
|
74
|
+
action: action._id
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
__publicField(this, "handleOpen", (socket) => {
|
|
81
|
+
if (this._handleOpen.has(socket)) {
|
|
82
|
+
return this._handleOpen.get(socket);
|
|
83
|
+
}
|
|
84
|
+
const handler = () => {
|
|
85
|
+
};
|
|
86
|
+
this._handleOpen.set(socket, handler);
|
|
87
|
+
return handler;
|
|
88
|
+
});
|
|
89
|
+
__publicField(this, "handleClose", (socket) => {
|
|
90
|
+
if (this._handleClose.has(socket)) {
|
|
91
|
+
return this._handleClose.get(socket);
|
|
92
|
+
}
|
|
93
|
+
const handler = () => {
|
|
94
|
+
};
|
|
95
|
+
this._handleClose.set(socket, handler);
|
|
96
|
+
return handler;
|
|
97
|
+
});
|
|
98
|
+
__publicField(this, "createMessage", (type, message) => {
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
_id: v4(),
|
|
101
|
+
_type: type,
|
|
102
|
+
_lastActionId: this.lastActionId,
|
|
103
|
+
...message
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
this.vault = new Vault();
|
|
107
|
+
this.ws = new WebSocketServer({ noServer: true });
|
|
108
|
+
this.ws.on("connection", this.setupWebSocket);
|
|
109
|
+
this.pingInterval = setInterval(() => {
|
|
110
|
+
this.ws.clients.forEach((socket) => {
|
|
111
|
+
if (socket.isAlive === false) {
|
|
112
|
+
return socket.terminate();
|
|
113
|
+
}
|
|
114
|
+
socket.isAlive = false;
|
|
115
|
+
socket.ping();
|
|
116
|
+
});
|
|
117
|
+
}, options.pingTimeout || 3e4);
|
|
118
|
+
this.ws.on("close", () => {
|
|
119
|
+
clearInterval(this.pingInterval);
|
|
120
|
+
if (options.onClose) {
|
|
121
|
+
options.onClose();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
this.ws.on("error", (err) => {
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async close() {
|
|
128
|
+
clearTimeout(this.pingInterval);
|
|
129
|
+
return new Promise((resolve, error) => {
|
|
130
|
+
this.ws.close((err) => {
|
|
131
|
+
if (err) {
|
|
132
|
+
error(err);
|
|
133
|
+
}
|
|
134
|
+
resolve();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/server.ts
|
|
141
|
+
import { createServer } from "http";
|
|
142
|
+
var MultiVaultServer = class {
|
|
143
|
+
constructor(server) {
|
|
144
|
+
__publicField(this, "server");
|
|
145
|
+
__publicField(this, "vaults", /* @__PURE__ */ new Map());
|
|
146
|
+
__publicField(this, "handleRequest", (request, response) => {
|
|
147
|
+
if (request.url === "/vault") {
|
|
148
|
+
const vaults = Array.from(this.vaults.keys());
|
|
149
|
+
const json = JSON.stringify({ vaults });
|
|
150
|
+
response.writeHead(200, {
|
|
151
|
+
"Content-Length": Buffer.byteLength(json),
|
|
152
|
+
"Content-Type": "application/json"
|
|
153
|
+
});
|
|
154
|
+
response.write(json);
|
|
155
|
+
response.end();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
response.writeHead(404);
|
|
159
|
+
response.end();
|
|
160
|
+
});
|
|
161
|
+
__publicField(this, "handleUpgrade", (request, socket, head) => {
|
|
162
|
+
console.log("ws.url", request.url);
|
|
163
|
+
const pathname = request.url;
|
|
164
|
+
if (pathname && pathname.startsWith("/vault")) {
|
|
165
|
+
const vaultId = pathname.replace("/vault/", "");
|
|
166
|
+
let vault = this.vaults.get(vaultId);
|
|
167
|
+
if (!vault) {
|
|
168
|
+
const newVault = new ServerVault();
|
|
169
|
+
console.log("new vault", vaultId);
|
|
170
|
+
this.vaults.set(vaultId, newVault);
|
|
171
|
+
vault = newVault;
|
|
172
|
+
}
|
|
173
|
+
if (vault) {
|
|
174
|
+
const theVault = vault;
|
|
175
|
+
theVault.ws.handleUpgrade(request, socket, head, (ws) => {
|
|
176
|
+
console.log("new client", vaultId);
|
|
177
|
+
theVault.ws.emit("connection", ws, request);
|
|
178
|
+
theVault.ws.on("close", () => {
|
|
179
|
+
if (theVault.ws.clients.size === 0) {
|
|
180
|
+
console.log("destroying vault", vaultId);
|
|
181
|
+
this.vaults.delete(vaultId);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
socket.destroy();
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
191
|
+
socket.destroy();
|
|
192
|
+
});
|
|
193
|
+
this.server = server || createServer();
|
|
194
|
+
this.server.on("upgrade", this.handleUpgrade);
|
|
195
|
+
this.server.on("request", this.handleRequest);
|
|
196
|
+
}
|
|
197
|
+
listen(port, hostname, backlog, listeningListener) {
|
|
198
|
+
this.server.listen(port, hostname, backlog, listeningListener);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
export {
|
|
202
|
+
MultiVaultServer,
|
|
203
|
+
ServerVault
|
|
204
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@manifest-editor/server-vault",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bugs": "https://github.com/digirati-co-uk/iiif-manifest-editor/issues",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/digirati-co-uk/iiif-manifest-editor.git",
|
|
9
|
+
"directory": "packages/server-vault"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18.14.1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"iiif",
|
|
16
|
+
"editor",
|
|
17
|
+
"typescript"
|
|
18
|
+
],
|
|
19
|
+
"nodeDependencies": [
|
|
20
|
+
"http",
|
|
21
|
+
"net",
|
|
22
|
+
"url",
|
|
23
|
+
"timers"
|
|
24
|
+
],
|
|
25
|
+
"main": "./dist/index.cjs",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/index.d.cts",
|
|
39
|
+
"default": "./dist/index.cjs"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts",
|
|
45
|
+
"dev": "tsup src/index.ts --watch",
|
|
46
|
+
"lint": "publint",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"prepack": "tsup src/index.ts"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@iiif/helpers": "^1.2.12",
|
|
52
|
+
"@iiif/presentation-3": "^2.2.3",
|
|
53
|
+
"uuid": "^9.0.1",
|
|
54
|
+
"ws": "^8.16.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@repo/eslint-config": "0.0.0",
|
|
58
|
+
"@types/node": "^20.11.24",
|
|
59
|
+
"@types/uuid": "^9.0.8",
|
|
60
|
+
"@types/ws": "^8.5.10",
|
|
61
|
+
"tsup": "^8.0.2"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"gitHead": "083fd2ce7d40755fb1857490e006e9854906d031"
|
|
67
|
+
}
|