@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 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
+ });
@@ -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 };
@@ -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
+ }