@tiddlywiki/mws 0.1.13 → 0.2.0
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/README.md +41 -20
- package/dist/metafile-esm.json +1 -1
- package/dist/mws.js +40249 -20249
- package/dist/mws.js.map +1 -1
- package/mws.run.mjs +28 -8
- package/package.json +20 -33
- package/plugins/client/plugin.info +1 -1
- package/plugins/client/src/multiwikiclientadaptor.ts +11 -39
- package/plugins/client/src/mws.d.ts +2 -1
- package/plugins/client/src/new-multiwikiclientadaptor.ts +634 -0
- package/plugins/client/src/types.d.ts +2531 -1036
- package/plugins/client/tiddlers/managetiddleraction.js +0 -5
- package/plugins/client/tiddlers/syncer/multiwikiclientadaptor.js +1 -1
- package/plugins/client/tiddlers/syncer/new-multiwikiclientadaptor.js +432 -0
- package/plugins/client/tiddlers/syncer/syncer.js +123 -241
- package/plugins/client/tsconfig.json +5 -1
- package/prisma/browser.ts +2 -0
- package/prisma/client/browser.ts +84 -0
- package/prisma/client/client.ts +108 -0
- package/prisma/client/commonInputTypes.ts +598 -0
- package/prisma/client/enums.ts +42 -0
- package/prisma/client/internal/class.ts +326 -0
- package/prisma/client/internal/prismaNamespace.ts +1870 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +259 -0
- package/prisma/client/models/Bag.ts +1701 -0
- package/prisma/client/models/BagPermission.ts +1284 -0
- package/prisma/client/models/Recipe.ts +1722 -0
- package/prisma/client/models/RecipeBag.ts +1518 -0
- package/prisma/client/models/RecipePermission.ts +1284 -0
- package/prisma/client/models/Roles.ts +1316 -0
- package/prisma/client/models/Sessions.ts +1344 -0
- package/prisma/client/models/Settings.ts +1093 -0
- package/prisma/client/models/Template.ts +1477 -0
- package/prisma/client/models/TemplatePermission.ts +1284 -0
- package/prisma/client/models/Tiddler.ts +1434 -0
- package/prisma/client/models/TiddlerEvent.ts +1360 -0
- package/prisma/client/models/Users.ts +1579 -0
- package/prisma/client/models.ts +24 -0
- package/prisma/client/pjtg.ts +183 -0
- package/prisma/index.ts +74 -0
- package/prisma/migrations/20260708160259_init/migration.sql +172 -0
- package/prisma/migrations-v1/migration_lock.toml +3 -0
- package/prisma/package.json +12 -0
- package/prisma/schema.prisma +147 -89
- package/prisma/types.ts +9 -0
- package/public/.default.html +1 -0
- package/public/admin-vanilla/fish.svg +399 -0
- package/public/admin-vanilla/fish2.svg +389 -0
- package/public/admin-vanilla/main-64WYHWYI.js +1791 -0
- package/public/admin-vanilla/main-64WYHWYI.js.map +7 -0
- package/public/admin-vanilla/main-LYDE4NDJ.css +1 -0
- package/public/admin-vanilla/normalize.css +349 -0
- package/public/admin-vanilla/polyfill-ADOI2A4V.js +2 -0
- package/public/admin-vanilla/polyfill-ADOI2A4V.js.map +7 -0
- package/public/{react-admin → admin-vanilla}/site.webmanifest +1 -1
- package/public/admin-vanilla.html +62 -0
- package/public/admin-vanilla.json +1 -0
- package/public/{react-admin → stats}/stats.html +5 -2
- package/public/{react-admin → stats}/stats.js +2 -1
- package/scripts/build-docs.sh +30 -0
- package/scripts/helpers.mjs +107 -0
- package/scripts/json.js +8 -0
- package/scripts/scripts.mjs +114 -0
- package/prisma/client/client.d.ts +0 -1
- package/prisma/client/client.js +0 -3
- package/prisma/client/default.d.ts +0 -1
- package/prisma/client/default.js +0 -3
- package/prisma/client/edge.d.ts +0 -1
- package/prisma/client/edge.js +0 -297
- package/prisma/client/index-browser.js +0 -275
- package/prisma/client/index.d.ts +0 -17770
- package/prisma/client/index.js +0 -324
- package/prisma/client/package.json +0 -173
- package/prisma/client/query_engine_bg.js +0 -811
- package/prisma/client/query_engine_bg.wasm +0 -0
- package/prisma/client/runtime/edge-esm.js +0 -10000
- package/prisma/client/runtime/edge.js +0 -10036
- package/prisma/client/runtime/index-browser.d.ts +0 -370
- package/prisma/client/runtime/index-browser.js +0 -2300
- package/prisma/client/runtime/library.d.ts +0 -3681
- package/prisma/client/runtime/library.js +0 -10604
- package/prisma/client/runtime/react-native.js +0 -10629
- package/prisma/client/runtime/wasm-compiler-edge.js +0 -11238
- package/prisma/client/runtime/wasm-engine-edge.js +0 -7801
- package/prisma/client/schema.prisma +0 -156
- package/prisma/client/wasm-edge-light-loader.mjs +0 -4
- package/prisma/client/wasm-worker-loader.mjs +0 -4
- package/prisma/client/wasm.d.ts +0 -1
- package/prisma/client/wasm.js +0 -304
- package/prisma/global.ts +0 -53
- package/public/react-admin/chunk-YALWN5OT.js +0 -2
- package/public/react-admin/esm-L4WLVWJN.js +0 -3
- package/public/react-admin/esm-L4WLVWJN.js.map +0 -7
- package/public/react-admin/esm-QMNGI3LF.js +0 -3
- package/public/react-admin/esm-QMNGI3LF.js.map +0 -7
- package/public/react-admin/index.html +0 -36
- package/public/react-admin/main.js +0 -268
- package/public/react-admin/main.js.map +0 -7
- package/public/react-admin/stats.json +0 -1
- package/scripts.mjs +0 -142
- /package/prisma/{migrations → migrations-v1}/20250606001949_init/migration.sql +0 -0
- /package/prisma/{migrations → migrations-v1}/20250613000526_remove_group/migration.sql +0 -0
- /package/prisma/{migrations → migrations-v1}/20250620014502_settings/migration.sql +0 -0
- /package/prisma/{migrations → migrations-v1}/20250707205229_recipes/migration.sql +0 -0
- /package/public/{react-admin → admin-vanilla}/android-chrome-192x192.png +0 -0
- /package/public/{react-admin → admin-vanilla}/android-chrome-512x512.png +0 -0
- /package/public/{react-admin → admin-vanilla}/apple-touch-icon.png +0 -0
- /package/public/{react-admin → admin-vanilla}/favicon-16x16.png +0 -0
- /package/public/{react-admin → admin-vanilla}/favicon-32x32.png +0 -0
- /package/public/{react-admin → admin-vanilla}/favicon.ico +0 -0
- /package/public/{react-admin → admin-vanilla}/favicon.png +0 -0
- /package/public/{react-admin/chunk-YALWN5OT.js.map → admin-vanilla/main-LYDE4NDJ.css.map} +0 -0
- /package/public/{react-admin → admin-vanilla}/missing-favicon.png +0 -0
- /package/public/{react-admin → admin-vanilla}/motovun-jack.jpg +0 -0
- /package/public/{react-admin → admin-vanilla}/mws-icon.png +0 -0
- /package/public/{react-admin → admin-vanilla}/vite.svg +0 -0
- /package/public/{react-admin → stats}/stats.css +0 -0
- /package/public/{react-admin → stats}/stats.svg +0 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/*\
|
|
2
|
+
title: $:/plugins/mws/client/new-multiwikiclientadaptor.js
|
|
3
|
+
type: application/javascript
|
|
4
|
+
module-type: syncadaptor
|
|
5
|
+
|
|
6
|
+
A sync adaptor module for synchronising with MultiWikiServer-compatible servers.
|
|
7
|
+
|
|
8
|
+
It has three key areas of concern:
|
|
9
|
+
|
|
10
|
+
* Basic operations like put, get, and delete a tiddler on the server
|
|
11
|
+
* Real time updates from the server (handled by SSE)
|
|
12
|
+
* Bags and recipes, which are unknown to the syncer
|
|
13
|
+
|
|
14
|
+
A key aspect of the design is that the syncer never overlaps basic server operations; it waits for the
|
|
15
|
+
previous operation to complete before sending a new one.
|
|
16
|
+
|
|
17
|
+
\*/
|
|
18
|
+
|
|
19
|
+
// the blank line is important, and so is the following use strict
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
// import type { ServerEventsMap } from '@tiddlywiki/events';
|
|
23
|
+
// import type { ZodRoute, WikiStatusRoutes, WikiRecipeRoutes } from '@tiddlywiki/mws';
|
|
24
|
+
// import type { zod } from '@tiddlywiki/server';
|
|
25
|
+
import type { Syncer, Tiddler, TiddlerFields, Wiki } from 'tiddlywiki';
|
|
26
|
+
|
|
27
|
+
// import {} from "@tiddlywiki/mws-prisma";
|
|
28
|
+
declare global { const fflate: typeof import("./fflate"); }
|
|
29
|
+
declare const self: never;
|
|
30
|
+
|
|
31
|
+
declare class Logger {
|
|
32
|
+
constructor(componentName: any, options: any);
|
|
33
|
+
componentName: any;
|
|
34
|
+
colour: any;
|
|
35
|
+
enable: any;
|
|
36
|
+
save: any;
|
|
37
|
+
saveLimit: any;
|
|
38
|
+
saveBufferLogger: this;
|
|
39
|
+
buffer: string;
|
|
40
|
+
alertCount: number;
|
|
41
|
+
setSaveBuffer(logger: any): void;
|
|
42
|
+
log(...args: any[]): any;
|
|
43
|
+
getBuffer(): string;
|
|
44
|
+
table(value: any): void;
|
|
45
|
+
alert(...args: any[]): void;
|
|
46
|
+
clearAlerts(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
declare module 'tiddlywiki' {
|
|
50
|
+
export interface Syncer<AD> {
|
|
51
|
+
wiki: Wiki;
|
|
52
|
+
logger: Logger;
|
|
53
|
+
tiddlerInfo: Record<string, {
|
|
54
|
+
changeCount: number,
|
|
55
|
+
adaptorInfo: AD,
|
|
56
|
+
revision: string,
|
|
57
|
+
timestampLastSaved: Date
|
|
58
|
+
}>;
|
|
59
|
+
enqueueLoadTiddler(title: string): void;
|
|
60
|
+
storeTiddler(tiddler: Tiddler): void;
|
|
61
|
+
processTaskQueue(): void;
|
|
62
|
+
syncFromServer(): void;
|
|
63
|
+
}
|
|
64
|
+
interface ITiddlyWiki {
|
|
65
|
+
browserStorage: any;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ServerStatusCallback = (
|
|
70
|
+
err: any,
|
|
71
|
+
/**
|
|
72
|
+
* $:/status/IsLoggedIn mostly appears alongside the username
|
|
73
|
+
* or other login-conditional behavior.
|
|
74
|
+
*/
|
|
75
|
+
isLoggedIn?: boolean,
|
|
76
|
+
/**
|
|
77
|
+
* $:/status/UserName is still used for things like drafts even if the
|
|
78
|
+
* user isn't logged in, although the username is less likely to be shown
|
|
79
|
+
* to the user.
|
|
80
|
+
*/
|
|
81
|
+
username?: string,
|
|
82
|
+
/**
|
|
83
|
+
* $:/status/IsReadOnly puts the UI in readonly mode,
|
|
84
|
+
* but does not prevent automatic changes from attempting to save.
|
|
85
|
+
*/
|
|
86
|
+
isReadOnly?: boolean,
|
|
87
|
+
/**
|
|
88
|
+
* $:/status/IsAnonymous does not appear anywhere in the TW5 repo!
|
|
89
|
+
* So it has no apparent purpose.
|
|
90
|
+
*/
|
|
91
|
+
isAnonymous?: boolean
|
|
92
|
+
) => void
|
|
93
|
+
|
|
94
|
+
interface SyncAdaptor<AD> {
|
|
95
|
+
name?: string;
|
|
96
|
+
|
|
97
|
+
isReady?(): boolean;
|
|
98
|
+
|
|
99
|
+
registerSyncer?(syncer: Syncer<AD>): void;
|
|
100
|
+
|
|
101
|
+
getStatus?(
|
|
102
|
+
cb: ServerStatusCallback
|
|
103
|
+
): void;
|
|
104
|
+
|
|
105
|
+
getSkinnyTiddlers?(
|
|
106
|
+
cb: (err: any, tiddlerFields: Record<string, string>[]) => void
|
|
107
|
+
): void;
|
|
108
|
+
getUpdatedTiddlers?(
|
|
109
|
+
syncer: Syncer<AD>,
|
|
110
|
+
cb: (
|
|
111
|
+
err: any,
|
|
112
|
+
/** Arrays of titles that have been modified or deleted */
|
|
113
|
+
updates?: { modifications: string[], deletions: string[] }
|
|
114
|
+
) => void
|
|
115
|
+
): void;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* used to override the default Syncer getTiddlerRevision behavior
|
|
119
|
+
* of returning the revision field
|
|
120
|
+
*
|
|
121
|
+
*/
|
|
122
|
+
getTiddlerRevision?(title: string): string;
|
|
123
|
+
/**
|
|
124
|
+
* used to get the adapter info from a tiddler in situations
|
|
125
|
+
* other than the saveTiddler callback
|
|
126
|
+
*/
|
|
127
|
+
getTiddlerInfo(tiddler: Tiddler): AD | undefined;
|
|
128
|
+
|
|
129
|
+
saveTiddler(
|
|
130
|
+
tiddler: any,
|
|
131
|
+
cb: (
|
|
132
|
+
err: any,
|
|
133
|
+
adaptorInfo?: AD,
|
|
134
|
+
revision?: string
|
|
135
|
+
) => void,
|
|
136
|
+
extra: { tiddlerInfo: SyncerTiddlerInfo<AD> }
|
|
137
|
+
): void;
|
|
138
|
+
|
|
139
|
+
saveTiddlers?(options: {
|
|
140
|
+
syncer: Syncer<AD>,
|
|
141
|
+
tiddlers: Tiddler[],
|
|
142
|
+
onNext: (title: string, adaptorInfo: any, revision: string) => void,
|
|
143
|
+
onDone: () => void,
|
|
144
|
+
onError: (err: Error) => void
|
|
145
|
+
}): void;
|
|
146
|
+
|
|
147
|
+
loadTiddlers?(options: {
|
|
148
|
+
syncer: Syncer<AD>,
|
|
149
|
+
titles: string[],
|
|
150
|
+
onNext: (tiddlerFields: TiddlerFields) => void,
|
|
151
|
+
onDone: () => void,
|
|
152
|
+
onError: (err: Error) => void
|
|
153
|
+
}): void;
|
|
154
|
+
|
|
155
|
+
deleteTiddlers?(options: {
|
|
156
|
+
syncer: Syncer<AD>,
|
|
157
|
+
titles: string[],
|
|
158
|
+
onNext: (title: string) => void,
|
|
159
|
+
onDone: () => void,
|
|
160
|
+
onError: (err: Error) => void
|
|
161
|
+
}): void;
|
|
162
|
+
|
|
163
|
+
setLoggerSaveBuffer?: (loggerForSaving: Logger) => void;
|
|
164
|
+
displayLoginPrompt?(syncer: Syncer<AD>): void;
|
|
165
|
+
login?(username: string, password: string, cb: (err: any) => void): void;
|
|
166
|
+
logout?(cb: (err: any) => void): any;
|
|
167
|
+
|
|
168
|
+
}
|
|
169
|
+
interface SyncerTiddlerInfo<AD> {
|
|
170
|
+
/** this comes from the wiki changeCount record */
|
|
171
|
+
changeCount: number;
|
|
172
|
+
/** Adapter info returned by the sync adapter */
|
|
173
|
+
adaptorInfo: AD;
|
|
174
|
+
/** Revision return by the sync adapter */
|
|
175
|
+
revision: string;
|
|
176
|
+
/** Timestamp set in the callback of the previous save */
|
|
177
|
+
timestampLastSaved: Date;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
declare const $tw: any;
|
|
181
|
+
|
|
182
|
+
declare const exports: {
|
|
183
|
+
adaptorClass: typeof MultiWikiClientAdaptor;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Constants
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
const CONFIG_HOST_TIDDLER = "$:/config/multiwikiclient/host";
|
|
191
|
+
const DEFAULT_HOST_TIDDLER = "$protocol$//$host$/";
|
|
192
|
+
const CONFIG_RECIPE_TIDDLER = "$:/config/multiwikiclient/recipe";
|
|
193
|
+
const IS_DEV_MODE_TIDDLER = "$:/state/multiwikiclient/dev-mode";
|
|
194
|
+
const LAST_REVISION_ID_TIDDLER = "$:/state/multiwikiclient/recipe/last_revision_id";
|
|
195
|
+
const MWC_STATE_TIDDLER_PREFIX = "$:/state/multiwikiclient/";
|
|
196
|
+
const BAG_STATE_TIDDLER = "$:/state/multiwikiclient/tiddlers/bag";
|
|
197
|
+
const REVISION_STATE_TIDDLER = "$:/state/multiwikiclient/tiddlers/revision";
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Types
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
interface MWSAdaptorInfo {
|
|
204
|
+
bag: string;
|
|
205
|
+
revision: string;
|
|
206
|
+
title: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Status response from GET /recipe/:id/status
|
|
210
|
+
interface RecipeStatus {
|
|
211
|
+
isAdmin: boolean;
|
|
212
|
+
user_id: string;
|
|
213
|
+
username: string;
|
|
214
|
+
isLoggedIn: boolean;
|
|
215
|
+
template: { type: string; definition: unknown; parameters: unknown };
|
|
216
|
+
bags: { bag_id: string; bag_name: string; is_writable: boolean; priority: number; canUserWrite: boolean; info: unknown }[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// TiddlerInfo from resolver
|
|
220
|
+
interface TiddlerInfo {
|
|
221
|
+
title: string;
|
|
222
|
+
writeTo: string | null;
|
|
223
|
+
readFrom: string | null;
|
|
224
|
+
existsIn: string[];
|
|
225
|
+
canWrite: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
type BatchMutationResult = { title: string; info: TiddlerInfo; revision?: string };
|
|
229
|
+
type BatchReadResult = { fields: Record<string, any> & { title: string }; info: TiddlerInfo } | null;
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Adaptor
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
class MultiWikiClientAdaptor implements SyncAdaptor<MWSAdaptorInfo> {
|
|
236
|
+
name = "multiwikiclient";
|
|
237
|
+
|
|
238
|
+
private wiki: Wiki;
|
|
239
|
+
private host: string;
|
|
240
|
+
private recipe: string;
|
|
241
|
+
private isDevMode: boolean;
|
|
242
|
+
private logger: Logger;
|
|
243
|
+
private syncer: Syncer<MWSAdaptorInfo> | null = null;
|
|
244
|
+
|
|
245
|
+
private isLoggedIn = false;
|
|
246
|
+
private isReadOnly = true;
|
|
247
|
+
private offline = false;
|
|
248
|
+
private username = "";
|
|
249
|
+
error: string | null = null;
|
|
250
|
+
|
|
251
|
+
private lastSeq = "0";
|
|
252
|
+
private initialLoadDone = false;
|
|
253
|
+
/** title → bag name, populated on load/save */
|
|
254
|
+
private tiddlerBag = new Map<string, string>();
|
|
255
|
+
/** title → revision, populated on save */
|
|
256
|
+
private tiddlerRevision = new Map<string, string>();
|
|
257
|
+
|
|
258
|
+
constructor(options: { wiki: Wiki }) {
|
|
259
|
+
this.wiki = options.wiki;
|
|
260
|
+
this.host = this.getHost();
|
|
261
|
+
this.recipe = this.wiki.getTiddlerText(CONFIG_RECIPE_TIDDLER, "")!;
|
|
262
|
+
this.isDevMode = this.wiki.getTiddlerText(IS_DEV_MODE_TIDDLER) === "yes";
|
|
263
|
+
this.lastSeq = this.wiki.getTiddlerText(LAST_REVISION_ID_TIDDLER, "0")!;
|
|
264
|
+
this.initialLoadDone = this.lastSeq !== "0";
|
|
265
|
+
this.logger = new $tw.utils.Logger("MultiWikiClientAdaptor");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
isReady() { return true; }
|
|
269
|
+
|
|
270
|
+
setLoggerSaveBuffer(logger: Logger) { this.logger.setSaveBuffer(logger); }
|
|
271
|
+
|
|
272
|
+
registerSyncer(syncer: Syncer<MWSAdaptorInfo>) { this.syncer = syncer; }
|
|
273
|
+
|
|
274
|
+
private isStateTiddler(title: string) {
|
|
275
|
+
return title.startsWith(MWC_STATE_TIDDLER_PREFIX);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private setTiddlerInfo(title: string, bag: string | null, revision?: string) {
|
|
279
|
+
if (bag) {
|
|
280
|
+
this.tiddlerBag.set(title, bag);
|
|
281
|
+
this.wiki.setText(BAG_STATE_TIDDLER, null, title, bag, { suppressTimestamp: true });
|
|
282
|
+
} else {
|
|
283
|
+
this.tiddlerBag.delete(title);
|
|
284
|
+
this.wiki.setText(BAG_STATE_TIDDLER, null, title, undefined, { suppressTimestamp: true });
|
|
285
|
+
}
|
|
286
|
+
if (revision) {
|
|
287
|
+
this.tiddlerRevision.set(title, revision);
|
|
288
|
+
this.wiki.setText(REVISION_STATE_TIDDLER, null, title, revision, { suppressTimestamp: true });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private clearTiddlerInfo(title: string) {
|
|
293
|
+
this.tiddlerBag.delete(title);
|
|
294
|
+
this.tiddlerRevision.delete(title);
|
|
295
|
+
this.wiki.setText(BAG_STATE_TIDDLER, null, title, undefined, { suppressTimestamp: true });
|
|
296
|
+
this.wiki.setText(REVISION_STATE_TIDDLER, null, title, undefined, { suppressTimestamp: true });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private setLastSeq(seq: string) {
|
|
300
|
+
this.lastSeq = seq;
|
|
301
|
+
this.wiki.setText(LAST_REVISION_ID_TIDDLER, null, "text", seq, { suppressTimestamp: true });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getTiddlerRevision(title: string) {
|
|
305
|
+
return this.wiki.extractTiddlerDataItem(REVISION_STATE_TIDDLER, title) ?? "";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getTiddlerInfo(tiddler: Tiddler): MWSAdaptorInfo | undefined {
|
|
309
|
+
const title = tiddler.fields.title as string;
|
|
310
|
+
const bag = this.wiki.extractTiddlerDataItem(BAG_STATE_TIDDLER, title) ?? this.tiddlerBag.get(title);
|
|
311
|
+
const revision = this.wiki.extractTiddlerDataItem(REVISION_STATE_TIDDLER, title) ?? this.tiddlerRevision.get(title);
|
|
312
|
+
return bag && revision ? { bag, revision, title } : undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private getHost() {
|
|
316
|
+
let text = this.wiki.getTiddlerText(CONFIG_HOST_TIDDLER, DEFAULT_HOST_TIDDLER)!;
|
|
317
|
+
[
|
|
318
|
+
{ name: "protocol", value: document.location.protocol },
|
|
319
|
+
{ name: "host", value: document.location.host },
|
|
320
|
+
{ name: "pathname", value: document.location.pathname },
|
|
321
|
+
].forEach(({ name, value }) => {
|
|
322
|
+
text = $tw.utils.replaceString(text, new RegExp("\\$" + name + "\\$", "mg"), value);
|
|
323
|
+
});
|
|
324
|
+
return text;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// -------------------------------------------------------------------------
|
|
328
|
+
// Status
|
|
329
|
+
// -------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
async getStatus(callback: ServerStatusCallback) {
|
|
332
|
+
const [ok, , result] = await this.recipeRequest({ method: "GET", url: "/status" });
|
|
333
|
+
if (!ok && result?.status === 0) {
|
|
334
|
+
this.offline = true;
|
|
335
|
+
this.isLoggedIn = false;
|
|
336
|
+
this.isReadOnly = true;
|
|
337
|
+
this.username = "(offline)";
|
|
338
|
+
this.error = "The webpage is forbidden from contacting the server.";
|
|
339
|
+
} else if (ok) {
|
|
340
|
+
const status = result!.responseJSON as RecipeStatus;
|
|
341
|
+
this.offline = false;
|
|
342
|
+
this.error = null;
|
|
343
|
+
this.isLoggedIn = status?.isLoggedIn ?? false;
|
|
344
|
+
this.username = status?.username ?? "(anon)";
|
|
345
|
+
this.isReadOnly = !(status?.bags?.some(b => b.canUserWrite) ?? false);
|
|
346
|
+
} else {
|
|
347
|
+
this.error = `Server error ${result?.status}`;
|
|
348
|
+
}
|
|
349
|
+
callback(this.error, this.isLoggedIn, this.username, this.isReadOnly, false);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// -------------------------------------------------------------------------
|
|
353
|
+
// Update polling
|
|
354
|
+
// -------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
async getUpdatedTiddlers(
|
|
357
|
+
_syncer: Syncer<MWSAdaptorInfo>,
|
|
358
|
+
callback: (err: any, updates?: { modifications: string[]; deletions: string[] }) => void
|
|
359
|
+
) {
|
|
360
|
+
if (this.offline) return callback(null);
|
|
361
|
+
try {
|
|
362
|
+
if (!this.initialLoadDone) {
|
|
363
|
+
// Fetch full list + current lastSeq in parallel on first load
|
|
364
|
+
const [[listOk, , listResult], [updOk, , updResult]] = await Promise.all([
|
|
365
|
+
this.recipeRequest({ method: "GET", url: "/list.json" }),
|
|
366
|
+
this.recipeRequest({ method: "GET", url: "/updates", queryParams: { since: "0" } }),
|
|
367
|
+
]);
|
|
368
|
+
if (!listOk) throw new Error("Failed to fetch tiddler list");
|
|
369
|
+
if (!updOk) throw new Error("Failed to fetch updates");
|
|
370
|
+
const list = listResult!.responseJSON as TiddlerInfo[];
|
|
371
|
+
const upd = updResult!.responseJSON as { modifications: string[]; deletions: string[]; lastSeq: string };
|
|
372
|
+
this.setLastSeq(upd.lastSeq);
|
|
373
|
+
this.initialLoadDone = true;
|
|
374
|
+
callback(null, { modifications: list.map(t => t.title), deletions: [] });
|
|
375
|
+
} else {
|
|
376
|
+
const [ok, , result] = await this.recipeRequest({
|
|
377
|
+
method: "GET",
|
|
378
|
+
url: "/updates",
|
|
379
|
+
queryParams: { since: this.lastSeq },
|
|
380
|
+
});
|
|
381
|
+
if (!ok) throw new Error("Failed to fetch updates");
|
|
382
|
+
const upd = result!.responseJSON as { modifications: string[]; deletions: string[]; lastSeq: string };
|
|
383
|
+
this.setLastSeq(upd.lastSeq);
|
|
384
|
+
callback(null, { modifications: upd.modifications, deletions: upd.deletions });
|
|
385
|
+
}
|
|
386
|
+
} catch (e: any) {
|
|
387
|
+
callback(e);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// -------------------------------------------------------------------------
|
|
392
|
+
// Batch operations (new API)
|
|
393
|
+
// -------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
async loadTiddlers(options: {
|
|
396
|
+
syncer: Syncer<MWSAdaptorInfo>;
|
|
397
|
+
titles: string[];
|
|
398
|
+
onNext: (fields: TiddlerFields) => void;
|
|
399
|
+
onDone: () => void;
|
|
400
|
+
onError: (err: Error) => void;
|
|
401
|
+
}) {
|
|
402
|
+
const { titles, onNext, onDone, onError } = options;
|
|
403
|
+
try {
|
|
404
|
+
const results = await this.batchOp<BatchReadResult[]>("read", { titles });
|
|
405
|
+
for (const item of results) {
|
|
406
|
+
if (!item) continue;
|
|
407
|
+
this.setTiddlerInfo(item.fields.title, item.info.readFrom, typeof item.fields.revision === "string" ? item.fields.revision : undefined);
|
|
408
|
+
onNext(item.fields as TiddlerFields);
|
|
409
|
+
}
|
|
410
|
+
onDone();
|
|
411
|
+
} catch (e: any) { onError(e); }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async saveTiddlers(options: {
|
|
415
|
+
syncer: Syncer<MWSAdaptorInfo>;
|
|
416
|
+
tiddlers: Tiddler[];
|
|
417
|
+
onNext: (title: string, adaptorInfo: MWSAdaptorInfo, revision: string) => void;
|
|
418
|
+
onDone: () => void;
|
|
419
|
+
onError: (err: Error) => void;
|
|
420
|
+
}) {
|
|
421
|
+
const { tiddlers, onNext, onDone, onError } = options;
|
|
422
|
+
try {
|
|
423
|
+
const results = await this.batchOp<BatchMutationResult[]>("save", {
|
|
424
|
+
tiddlers: tiddlers.map(t => t.getFieldStrings()),
|
|
425
|
+
});
|
|
426
|
+
for (const item of results) {
|
|
427
|
+
const bag = item.info.writeTo ?? item.info.readFrom ?? "";
|
|
428
|
+
this.setTiddlerInfo(item.title, bag || null, item.revision ?? "");
|
|
429
|
+
if ($tw.browserStorage?.isEnabled()) $tw.browserStorage.removeTiddlerFromLocalStorage(item.title);
|
|
430
|
+
onNext(item.title, { bag, revision: item.revision ?? "", title: item.title }, item.revision ?? "");
|
|
431
|
+
}
|
|
432
|
+
onDone();
|
|
433
|
+
} catch (e: any) { onError(e); }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async deleteTiddlers(options: {
|
|
437
|
+
syncer: Syncer<MWSAdaptorInfo>;
|
|
438
|
+
titles: string[];
|
|
439
|
+
onNext: (title: string) => void;
|
|
440
|
+
onDone: () => void;
|
|
441
|
+
onError: (err: Error) => void;
|
|
442
|
+
}) {
|
|
443
|
+
const { titles, onNext, onDone, onError } = options;
|
|
444
|
+
try {
|
|
445
|
+
const results = await this.batchOp<BatchMutationResult[]>("delete", { titles });
|
|
446
|
+
for (const item of results) {
|
|
447
|
+
this.clearTiddlerInfo(item.title);
|
|
448
|
+
onNext(item.title);
|
|
449
|
+
}
|
|
450
|
+
onDone();
|
|
451
|
+
} catch (e: any) { onError(e); }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// -------------------------------------------------------------------------
|
|
455
|
+
// Single-tiddler operations (fallback for older server versions)
|
|
456
|
+
// -------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
async saveTiddler(
|
|
459
|
+
tiddler: Tiddler,
|
|
460
|
+
callback: (err: any, adaptorInfo?: MWSAdaptorInfo, revision?: string) => void
|
|
461
|
+
) {
|
|
462
|
+
const title = tiddler.fields.title as string;
|
|
463
|
+
if (title === "$:/StoryList" || this.isReadOnly || this.isStateTiddler(title)) return callback(null);
|
|
464
|
+
try {
|
|
465
|
+
const results = await this.batchOp<BatchMutationResult[]>("save", { tiddlers: [tiddler.getFieldStrings()] });
|
|
466
|
+
const item = results[0];
|
|
467
|
+
if (!item) return callback(new Error("No result returned"));
|
|
468
|
+
const bag = item.info.writeTo ?? item.info.readFrom ?? "";
|
|
469
|
+
this.setTiddlerInfo(title, bag || null, item.revision ?? "");
|
|
470
|
+
if ($tw.browserStorage?.isEnabled()) $tw.browserStorage.removeTiddlerFromLocalStorage(title);
|
|
471
|
+
callback(null, { bag, revision: item.revision ?? "", title }, item.revision ?? "");
|
|
472
|
+
} catch (e: any) { callback(e); }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async loadTiddler(title: string, callback: (err: any, fields?: any) => void, _options: any) {
|
|
476
|
+
try {
|
|
477
|
+
const results = await this.batchOp<BatchReadResult[]>("read", { titles: [title] });
|
|
478
|
+
const item = results[0];
|
|
479
|
+
if (!item) return callback(null, null);
|
|
480
|
+
this.setTiddlerInfo(title, item.info.readFrom, typeof item.fields.revision === "string" ? item.fields.revision : undefined);
|
|
481
|
+
callback(null, item.fields);
|
|
482
|
+
} catch (e: any) { callback(e); }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async deleteTiddler(title: string, callback: (err: any, adaptorInfo?: any) => void, _options: any) {
|
|
486
|
+
if (this.isReadOnly) return callback(null);
|
|
487
|
+
try {
|
|
488
|
+
const results = await this.batchOp<BatchMutationResult[]>("delete", { titles: [title] });
|
|
489
|
+
const item = results[0];
|
|
490
|
+
if (!item) return callback(new Error("No result returned"));
|
|
491
|
+
this.clearTiddlerInfo(title);
|
|
492
|
+
callback(null, null);
|
|
493
|
+
} catch (e: any) { callback(e); }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// -------------------------------------------------------------------------
|
|
497
|
+
// HTTP helpers
|
|
498
|
+
// -------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
private async batchOp<T>(op: string, body: Record<string, any>): Promise<T> {
|
|
501
|
+
const [ok, err, result] = await this.recipeRequest({
|
|
502
|
+
method: "PUT",
|
|
503
|
+
url: "/batch/" + op,
|
|
504
|
+
requestBodyString: JSON.stringify(body),
|
|
505
|
+
headers: { "content-type": "application/json" },
|
|
506
|
+
});
|
|
507
|
+
if (!ok) throw err;
|
|
508
|
+
if (!result!.responseJSON) throw new Error("No response JSON from batch/" + op);
|
|
509
|
+
return result!.responseJSON as T;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private async recipeRequest(options: {
|
|
513
|
+
method: string;
|
|
514
|
+
url: string;
|
|
515
|
+
headers?: HeadersInit;
|
|
516
|
+
queryParams?: Record<string, string>;
|
|
517
|
+
requestBodyString?: string;
|
|
518
|
+
}) {
|
|
519
|
+
if (!options.url.startsWith("/")) throw new Error("URL must start with /");
|
|
520
|
+
const isDevMode = this.isDevMode;
|
|
521
|
+
return httpRequest({
|
|
522
|
+
...options,
|
|
523
|
+
responseType: "blob",
|
|
524
|
+
url: this.host + "recipe/" + encodeURIComponent(this.recipe) + options.url,
|
|
525
|
+
}).then(async e => {
|
|
526
|
+
if (!e.ok) return [false, new Error(
|
|
527
|
+
`Server returned ${e.status}: ${e.headers.get("x-reason") ?? "(no reason)"}`
|
|
528
|
+
), { ...e, responseJSON: undefined }] as const;
|
|
529
|
+
|
|
530
|
+
let responseString: string;
|
|
531
|
+
if (e.headers.get("x-gzip-stream") === "yes") {
|
|
532
|
+
responseString = await new Promise<string>((resolve) => {
|
|
533
|
+
let s = "";
|
|
534
|
+
const gz = new fflate.AsyncGunzip((err, chunk, final) => {
|
|
535
|
+
if (err) return;
|
|
536
|
+
s += fflate.strFromU8(chunk);
|
|
537
|
+
if (final) resolve(s);
|
|
538
|
+
});
|
|
539
|
+
if (isDevMode) gz.onmember = m => console.log("gunzip member", m);
|
|
540
|
+
readBlobAsArrayBuffer(e.response as Blob).then(buf => {
|
|
541
|
+
gz.push(new Uint8Array(buf));
|
|
542
|
+
gz.push(new Uint8Array(0), true);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
} else {
|
|
546
|
+
responseString = fflate.strFromU8(new Uint8Array(await readBlobAsArrayBuffer(e.response as Blob)));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return [true, undefined, {
|
|
550
|
+
...e,
|
|
551
|
+
responseJSON: e.status === 200 ? tryParseJSON(responseString) : undefined,
|
|
552
|
+
}] as const;
|
|
553
|
+
}, e => [false, e, undefined] as const);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Utilities
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
function tryParseJSON(s: string) {
|
|
562
|
+
try { return JSON.parse(s); } catch (e) { console.error("JSON parse error", e); return undefined; }
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
type ParamsInput = URLSearchParams | [string, string][] | object | string | undefined;
|
|
566
|
+
|
|
567
|
+
interface HttpRequestOptions<TYPE extends "arraybuffer" | "blob" | "text"> {
|
|
568
|
+
method: string;
|
|
569
|
+
url: string;
|
|
570
|
+
responseType: TYPE;
|
|
571
|
+
headers?: HeadersInit;
|
|
572
|
+
queryParams?: ParamsInput;
|
|
573
|
+
requestBodyString?: string;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function httpRequest<TYPE extends "arraybuffer" | "blob" | "text">(options: HttpRequestOptions<TYPE>) {
|
|
577
|
+
return new Promise<{
|
|
578
|
+
ok: boolean; status: number; statusText: string; headers: Headers;
|
|
579
|
+
response: TYPE extends "arraybuffer" ? ArrayBuffer : TYPE extends "blob" ? Blob : string;
|
|
580
|
+
}>((resolve, reject) => {
|
|
581
|
+
options.method = options.method.toUpperCase();
|
|
582
|
+
const url = new URL(options.url, location.href);
|
|
583
|
+
paramsInput(options.queryParams).forEach((v, k) => url.searchParams.append(k, v));
|
|
584
|
+
const headers = new Headers(options.headers || {});
|
|
585
|
+
const request = new XMLHttpRequest();
|
|
586
|
+
request.responseType = options.responseType;
|
|
587
|
+
request.open(options.method, url, true);
|
|
588
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/x-www-form-urlencoded; charset=UTF-8");
|
|
589
|
+
if (!headers.has("x-requested-with")) headers.set("x-requested-with", "TiddlyWiki");
|
|
590
|
+
headers.set("accept", "application/json");
|
|
591
|
+
headers.forEach((v, k) => request.setRequestHeader(k, v));
|
|
592
|
+
request.onreadystatechange = function () {
|
|
593
|
+
if (this.readyState !== 4) return;
|
|
594
|
+
const h = new Headers();
|
|
595
|
+
request.getAllResponseHeaders()?.trim().split(/[\r\n]+/).forEach(line => {
|
|
596
|
+
const parts = line.split(": ");
|
|
597
|
+
const key = parts.shift()?.toLowerCase();
|
|
598
|
+
if (key) h.append(key, parts.join(": "));
|
|
599
|
+
});
|
|
600
|
+
resolve({ ok: this.status >= 200 && this.status < 300, status: this.status, statusText: this.statusText, response: this.response, headers: h });
|
|
601
|
+
};
|
|
602
|
+
request.send(options.requestBodyString);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
function paramsInput(input: ParamsInput) {
|
|
606
|
+
if (!input) return new URLSearchParams();
|
|
607
|
+
if (input instanceof URLSearchParams) return input;
|
|
608
|
+
if (Array.isArray(input) || typeof input === "string") return new URLSearchParams(input);
|
|
609
|
+
const params = new URLSearchParams();
|
|
610
|
+
for (const key in input) {
|
|
611
|
+
if (Object.prototype.hasOwnProperty.call(input, key)) {
|
|
612
|
+
params.append(key, (input as Record<string, string>)[key]);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return params;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function readBlobAsArrayBuffer(blob: Blob) {
|
|
620
|
+
return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
621
|
+
const reader = new FileReader();
|
|
622
|
+
reader.onload = () => resolve(reader.result as ArrayBuffer);
|
|
623
|
+
reader.onerror = () => reject(new Error("Error reading blob"));
|
|
624
|
+
reader.readAsArrayBuffer(blob);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// Export
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
if ($tw.browser && document.location.protocol.startsWith("http")) {
|
|
633
|
+
exports.adaptorClass = MultiWikiClientAdaptor;
|
|
634
|
+
}
|