@shaxpir/duiduidui-models 1.0.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 +1 -0
- package/decs.d.ts +87 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +20 -0
- package/dist/models/OutboundMessage.d.ts +18 -0
- package/dist/models/OutboundMessage.js +25 -0
- package/dist/models/content/Activity.d.ts +10 -0
- package/dist/models/content/Activity.js +2 -0
- package/dist/models/content/ArrayView.d.ts +26 -0
- package/dist/models/content/ArrayView.js +174 -0
- package/dist/models/content/Billing.d.ts +144 -0
- package/dist/models/content/Billing.js +418 -0
- package/dist/models/content/Book.d.ts +77 -0
- package/dist/models/content/Book.js +407 -0
- package/dist/models/content/Category.d.ts +16 -0
- package/dist/models/content/Category.js +20 -0
- package/dist/models/content/Checkpointable.d.ts +21 -0
- package/dist/models/content/Checkpointable.js +156 -0
- package/dist/models/content/Comment.d.ts +19 -0
- package/dist/models/content/Comment.js +53 -0
- package/dist/models/content/ConceptArt.d.ts +31 -0
- package/dist/models/content/ConceptArt.js +84 -0
- package/dist/models/content/Content.d.ts +52 -0
- package/dist/models/content/Content.js +61 -0
- package/dist/models/content/ContentKind.d.ts +10 -0
- package/dist/models/content/ContentKind.js +16 -0
- package/dist/models/content/Context.d.ts +28 -0
- package/dist/models/content/Context.js +162 -0
- package/dist/models/content/DevEnv.d.ts +5 -0
- package/dist/models/content/DevEnv.js +9 -0
- package/dist/models/content/Device.d.ts +24 -0
- package/dist/models/content/Device.js +62 -0
- package/dist/models/content/Dictionary.d.ts +31 -0
- package/dist/models/content/Dictionary.js +5 -0
- package/dist/models/content/DictionaryEntry.d.ts +20 -0
- package/dist/models/content/DictionaryEntry.js +2 -0
- package/dist/models/content/ElasticModel.d.ts +149 -0
- package/dist/models/content/ElasticModel.js +179 -0
- package/dist/models/content/Environment.d.ts +61 -0
- package/dist/models/content/Environment.js +124 -0
- package/dist/models/content/ExportOptions.d.ts +64 -0
- package/dist/models/content/ExportOptions.js +213 -0
- package/dist/models/content/Folder.d.ts +16 -0
- package/dist/models/content/Folder.js +33 -0
- package/dist/models/content/Fragment.d.ts +54 -0
- package/dist/models/content/Fragment.js +181 -0
- package/dist/models/content/GeoLocation.d.ts +4 -0
- package/dist/models/content/GeoLocation.js +2 -0
- package/dist/models/content/Hanzi.d.ts +21 -0
- package/dist/models/content/Hanzi.js +2 -0
- package/dist/models/content/HighlightRule.d.ts +9 -0
- package/dist/models/content/HighlightRule.js +2 -0
- package/dist/models/content/Manifest.d.ts +42 -0
- package/dist/models/content/Manifest.js +114 -0
- package/dist/models/content/Media.d.ts +32 -0
- package/dist/models/content/Media.js +98 -0
- package/dist/models/content/Metric.d.ts +46 -0
- package/dist/models/content/Metric.js +183 -0
- package/dist/models/content/Migration.d.ts +68 -0
- package/dist/models/content/Migration.js +155 -0
- package/dist/models/content/Model.d.ts +45 -0
- package/dist/models/content/Model.js +280 -0
- package/dist/models/content/Permissions.d.ts +7 -0
- package/dist/models/content/Permissions.js +20 -0
- package/dist/models/content/Phrase.d.ts +8 -0
- package/dist/models/content/Phrase.js +2 -0
- package/dist/models/content/Placeholder.d.ts +8 -0
- package/dist/models/content/Placeholder.js +36 -0
- package/dist/models/content/Profile.d.ts +30 -0
- package/dist/models/content/Profile.js +95 -0
- package/dist/models/content/RichText.d.ts +58 -0
- package/dist/models/content/RichText.js +79 -0
- package/dist/models/content/Session.d.ts +39 -0
- package/dist/models/content/Session.js +173 -0
- package/dist/models/content/Speech.d.ts +67 -0
- package/dist/models/content/Speech.js +97 -0
- package/dist/models/content/Stub.d.ts +24 -0
- package/dist/models/content/Stub.js +179 -0
- package/dist/models/content/Time.d.ts +56 -0
- package/dist/models/content/Time.js +295 -0
- package/dist/models/content/User.d.ts +36 -0
- package/dist/models/content/User.js +95 -0
- package/dist/models/content/Workspace.d.ts +71 -0
- package/dist/models/content/Workspace.js +237 -0
- package/dist/models/content/index.d.ts +36 -0
- package/dist/models/content/index.js +53 -0
- package/dist/models/index.d.ts +4 -0
- package/dist/models/index.js +21 -0
- package/dist/models/legacy/LegacyBodyFormat.d.ts +9 -0
- package/dist/models/legacy/LegacyBodyFormat.js +2 -0
- package/dist/models/legacy/LegacyComment.d.ts +12 -0
- package/dist/models/legacy/LegacyComment.js +2 -0
- package/dist/models/legacy/LegacyContent.d.ts +53 -0
- package/dist/models/legacy/LegacyContent.js +55 -0
- package/dist/models/legacy/LegacyConversion.d.ts +55 -0
- package/dist/models/legacy/LegacyConversion.js +401 -0
- package/dist/models/legacy/LegacyFragment.d.ts +21 -0
- package/dist/models/legacy/LegacyFragment.js +2 -0
- package/dist/models/legacy/LegacyLocator.d.ts +8 -0
- package/dist/models/legacy/LegacyLocator.js +31 -0
- package/dist/models/legacy/LegacyOutboundMessage.d.ts +16 -0
- package/dist/models/legacy/LegacyOutboundMessage.js +13 -0
- package/dist/models/legacy/LegacyPicture.d.ts +14 -0
- package/dist/models/legacy/LegacyPicture.js +2 -0
- package/dist/models/legacy/LegacyProfile.d.ts +9 -0
- package/dist/models/legacy/LegacyProfile.js +2 -0
- package/dist/models/legacy/LegacySession.d.ts +41 -0
- package/dist/models/legacy/LegacySession.js +35 -0
- package/dist/models/legacy/LegacyStory.d.ts +23 -0
- package/dist/models/legacy/LegacyStory.js +2 -0
- package/dist/models/legacy/LegacyStub.d.ts +15 -0
- package/dist/models/legacy/LegacyStub.js +2 -0
- package/dist/models/legacy/LegacyTransaction.d.ts +14 -0
- package/dist/models/legacy/LegacyTransaction.js +49 -0
- package/dist/models/legacy/LegacyUser.d.ts +28 -0
- package/dist/models/legacy/LegacyUser.js +32 -0
- package/dist/models/legacy/LegacyWorkspace.d.ts +23 -0
- package/dist/models/legacy/LegacyWorkspace.js +6 -0
- package/dist/models/legacy/index.d.ts +15 -0
- package/dist/models/legacy/index.js +32 -0
- package/dist/models/markup/BodyFormat.d.ts +14 -0
- package/dist/models/markup/BodyFormat.js +190 -0
- package/dist/models/markup/ChangeModel.d.ts +22 -0
- package/dist/models/markup/ChangeModel.js +107 -0
- package/dist/models/markup/DeltaOps.d.ts +5 -0
- package/dist/models/markup/DeltaOps.js +74 -0
- package/dist/models/markup/HtmlMarkup.d.ts +4 -0
- package/dist/models/markup/HtmlMarkup.js +21 -0
- package/dist/models/markup/Operation.d.ts +32 -0
- package/dist/models/markup/Operation.js +194 -0
- package/dist/models/markup/TextEditOps.d.ts +9 -0
- package/dist/models/markup/TextEditOps.js +50 -0
- package/dist/models/markup/index.d.ts +6 -0
- package/dist/models/markup/index.js +23 -0
- package/dist/repo/ConnectionListener.d.ts +9 -0
- package/dist/repo/ConnectionListener.js +21 -0
- package/dist/repo/PermissiveJson1.d.ts +58 -0
- package/dist/repo/PermissiveJson1.js +39 -0
- package/dist/repo/ShareSync.d.ts +60 -0
- package/dist/repo/ShareSync.js +348 -0
- package/dist/repo/index.d.ts +3 -0
- package/dist/repo/index.js +20 -0
- package/dist/util/Async.d.ts +8 -0
- package/dist/util/Async.js +18 -0
- package/dist/util/Base62.d.ts +6 -0
- package/dist/util/Base62.js +47 -0
- package/dist/util/BinarySearch.d.ts +7 -0
- package/dist/util/BinarySearch.js +46 -0
- package/dist/util/CachingHasher.d.ts +8 -0
- package/dist/util/CachingHasher.js +41 -0
- package/dist/util/Color.d.ts +32 -0
- package/dist/util/Color.js +204 -0
- package/dist/util/Dispatch.d.ts +15 -0
- package/dist/util/Dispatch.js +79 -0
- package/dist/util/EditDistance.d.ts +13 -0
- package/dist/util/EditDistance.js +184 -0
- package/dist/util/Encryption.d.ts +5 -0
- package/dist/util/Encryption.js +2 -0
- package/dist/util/Logging.d.ts +108 -0
- package/dist/util/Logging.js +412 -0
- package/dist/util/NumberFormat.d.ts +14 -0
- package/dist/util/NumberFormat.js +224 -0
- package/dist/util/Struct.d.ts +4 -0
- package/dist/util/Struct.js +15 -0
- package/dist/util/Template.d.ts +16 -0
- package/dist/util/Template.js +128 -0
- package/dist/util/Text.d.ts +45 -0
- package/dist/util/Text.js +243 -0
- package/dist/util/Tuples.d.ts +9 -0
- package/dist/util/Tuples.js +135 -0
- package/dist/util/Validate.d.ts +4 -0
- package/dist/util/Validate.js +11 -0
- package/dist/util/Vocabulary.d.ts +3 -0
- package/dist/util/Vocabulary.js +35 -0
- package/dist/util/index.d.ts +16 -0
- package/dist/util/index.js +33 -0
- package/lib/models/content/ArrayView.ts +203 -0
- package/lib/models/content/Billing.ts +558 -0
- package/lib/models/content/Content.ts +110 -0
- package/lib/models/content/ContentKind.ts +14 -0
- package/lib/models/content/DevEnv.ts +5 -0
- package/lib/models/content/Device.ts +86 -0
- package/lib/models/content/DictionaryEntry.ts +22 -0
- package/lib/models/content/GeoLocation.ts +4 -0
- package/lib/models/content/Hanzi.ts +25 -0
- package/lib/models/content/Manifest.ts +162 -0
- package/lib/models/content/Media.ts +126 -0
- package/lib/models/content/Model.ts +327 -0
- package/lib/models/content/Permissions.ts +21 -0
- package/lib/models/content/Phrase.ts +10 -0
- package/lib/models/content/Profile.ts +119 -0
- package/lib/models/content/Time.ts +328 -0
- package/lib/models/content/User.ts +130 -0
- package/lib/models/markup/ChangeModel.ts +95 -0
- package/lib/models/markup/DeltaOps.ts +71 -0
- package/lib/models/markup/Operation.ts +215 -0
- package/lib/models/markup/TextEditOps.ts +50 -0
- package/lib/repo/ConnectionListener.ts +25 -0
- package/lib/repo/PermissiveJson1.ts +14 -0
- package/lib/repo/ShareSync.ts +390 -0
- package/lib/util/Base62.ts +47 -0
- package/lib/util/CachingHasher.ts +38 -0
- package/lib/util/Dispatch.ts +92 -0
- package/lib/util/Encryption.ts +5 -0
- package/lib/util/Logging.ts +568 -0
- package/lib/util/NumberFormat.ts +194 -0
- package/lib/util/Struct.ts +14 -0
- package/lib/util/Tuples.ts +131 -0
- package/package.json +41 -0
- package/tsconfig.json +25 -0
- package/tslint.json +46 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Op as DeltaOp } from "@shaxpir/quill-delta";
|
|
2
|
+
import { Doc } from '@shaxpir/sharedb/lib/client';
|
|
3
|
+
import { ShareSyncFactory } from '../../repo/ShareSync';
|
|
4
|
+
import { Struct } from '../../util/Struct';
|
|
5
|
+
import { Content } from '../content/Content';
|
|
6
|
+
import { Model } from '../content/Model';
|
|
7
|
+
import { MultiClock, MultiTime } from '../content/Time';
|
|
8
|
+
import { TextEditOps } from './TextEditOps';
|
|
9
|
+
|
|
10
|
+
import * as Json1 from '../../repo/PermissiveJson1';
|
|
11
|
+
|
|
12
|
+
const RichText = require('@shaxpir/rich-text');
|
|
13
|
+
const UnicodeText = require('ot-text-unicode');
|
|
14
|
+
|
|
15
|
+
export type JsonPathElement = string | number;
|
|
16
|
+
export type JsonPath = JsonPathElement[];
|
|
17
|
+
|
|
18
|
+
export interface PathVal {
|
|
19
|
+
path:JsonPath,
|
|
20
|
+
val:any
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class JsonPathSelect {
|
|
24
|
+
|
|
25
|
+
public static getValueAtPath(doc:Doc, path:JsonPath):any {
|
|
26
|
+
let currentNode = doc.data;
|
|
27
|
+
for (let i = 0; i < path.length; i++) {
|
|
28
|
+
let pathElement:JsonPathElement = path[i];
|
|
29
|
+
if (currentNode !== undefined && currentNode !== null) {
|
|
30
|
+
if (typeof(pathElement) === "string" && currentNode.hasOwnProperty(pathElement)) {
|
|
31
|
+
currentNode = currentNode[pathElement];
|
|
32
|
+
} else if (typeof(pathElement) === "number" && currentNode.length > pathElement) {
|
|
33
|
+
currentNode = currentNode[pathElement];
|
|
34
|
+
} else {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
console.error(`no value at path ${JSON.stringify(path)} at index ${i} in object '${doc.collection}/${doc.id}' with value ${JSON.stringify(doc.data)}`);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return currentNode;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class BatchOperation {
|
|
47
|
+
|
|
48
|
+
private model:Model;
|
|
49
|
+
private time:MultiTime;
|
|
50
|
+
private ops:any[];
|
|
51
|
+
|
|
52
|
+
// TODO: maybe just use the timestamp on the Content?
|
|
53
|
+
constructor(model:Model, time?:MultiTime) {
|
|
54
|
+
if (model.isForbidden) {
|
|
55
|
+
throw new Error(`cannot create a BatchOperation on forbidden model: ${model.compoundKey}`);
|
|
56
|
+
}
|
|
57
|
+
this.model = model;
|
|
58
|
+
if (time) {
|
|
59
|
+
this.time = time;
|
|
60
|
+
} else {
|
|
61
|
+
this.time = MultiClock.now()
|
|
62
|
+
}
|
|
63
|
+
this.ops = [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get at():MultiTime {
|
|
67
|
+
return Struct.clone(this.time);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public hasOps():boolean {
|
|
71
|
+
return this.ops.length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public editPathText(
|
|
75
|
+
path:JsonPath,
|
|
76
|
+
value:string
|
|
77
|
+
):void {
|
|
78
|
+
const prevValue = JsonPathSelect.getValueAtPath(this.model.doc, path);
|
|
79
|
+
if (prevValue !== undefined) {
|
|
80
|
+
// If we're trying to edit a text value, but the previous value is null, then the
|
|
81
|
+
// 'editOp' will throw an exception. So we need to use a 'replaceOp' instead.
|
|
82
|
+
if (prevValue === null) {
|
|
83
|
+
this.ops.push(Json1.replaceOp(path, prevValue, value));
|
|
84
|
+
} else if (!Struct.equals(prevValue, value)) {
|
|
85
|
+
const textEditOps = TextEditOps.between(prevValue, value);
|
|
86
|
+
this.ops.push(Json1.editOp(path, UnicodeText.type, textEditOps));
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
this.ops.push(Json1.insertOp(path, value));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public setPathValue(
|
|
94
|
+
path:JsonPath,
|
|
95
|
+
value:any
|
|
96
|
+
):void {
|
|
97
|
+
const prevValue = JsonPathSelect.getValueAtPath(this.model.doc, path);
|
|
98
|
+
if (prevValue !== undefined) {
|
|
99
|
+
if (!Struct.equals(prevValue, value)) {
|
|
100
|
+
this.ops.push(Json1.replaceOp(path, prevValue, value));
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
this.ops.push(Json1.insertOp(path, value));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public move(
|
|
108
|
+
originPath:JsonPath,
|
|
109
|
+
destinationPath:JsonPath
|
|
110
|
+
):void {
|
|
111
|
+
this.ops.push(Json1.moveOp(originPath, destinationPath));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public insertIntoArray(
|
|
115
|
+
arrayPath:JsonPath,
|
|
116
|
+
index:number,
|
|
117
|
+
value:any
|
|
118
|
+
):void {
|
|
119
|
+
let array = JsonPathSelect.getValueAtPath(this.model.doc, arrayPath);
|
|
120
|
+
if (array === undefined) {
|
|
121
|
+
this.ops.push(Json1.insertOp(arrayPath, [ value ]));
|
|
122
|
+
} else {
|
|
123
|
+
if (!Array.isArray(array)) {
|
|
124
|
+
throw new Error(`path '${JSON.stringify(arrayPath)}' does not refer to an array: ${JSON.stringify(this.model.doc.data)}`);
|
|
125
|
+
}
|
|
126
|
+
const path = Struct.clone(arrayPath);
|
|
127
|
+
path.push(index);
|
|
128
|
+
this.ops.push(Json1.insertOp(path, value));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public unshiftIntoArray(
|
|
133
|
+
path:JsonPath,
|
|
134
|
+
value:any
|
|
135
|
+
):void {
|
|
136
|
+
path = Struct.clone(path);
|
|
137
|
+
let array = JsonPathSelect.getValueAtPath(this.model.doc, path);
|
|
138
|
+
if (array === undefined) {
|
|
139
|
+
this.ops.push(Json1.insertOp(path, [ value ]));
|
|
140
|
+
} else {
|
|
141
|
+
if (!Array.isArray(array)) {
|
|
142
|
+
throw new Error(`path '${JSON.stringify(path)}' does not refer to an array: ${JSON.stringify(this.model.doc.data)}`);
|
|
143
|
+
}
|
|
144
|
+
path.push(0);
|
|
145
|
+
this.ops.push(Json1.insertOp(path, value));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public pushIntoArray(
|
|
150
|
+
path:JsonPath,
|
|
151
|
+
value:any
|
|
152
|
+
):void {
|
|
153
|
+
path = Struct.clone(path);
|
|
154
|
+
let array = JsonPathSelect.getValueAtPath(this.model.doc, path);
|
|
155
|
+
if (array === undefined) {
|
|
156
|
+
this.ops.push(Json1.insertOp(path, [ value ]));
|
|
157
|
+
} else {
|
|
158
|
+
if (!Array.isArray(array)) {
|
|
159
|
+
throw new Error(`path '${JSON.stringify(path)}' does not refer to an array: ${JSON.stringify(this.model.doc.data)}`);
|
|
160
|
+
}
|
|
161
|
+
path.push(array.length);
|
|
162
|
+
this.ops.push(Json1.insertOp(path, value));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public removeValueAtPath(
|
|
167
|
+
path:JsonPath
|
|
168
|
+
):void {
|
|
169
|
+
let prevValue:any = JsonPathSelect.getValueAtPath(this.model.doc, path);
|
|
170
|
+
if (prevValue !== undefined) {
|
|
171
|
+
this.ops.push(Json1.removeOp(path, prevValue));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public replaceValueAtPath(
|
|
176
|
+
path:JsonPath,
|
|
177
|
+
value:any
|
|
178
|
+
):void {
|
|
179
|
+
let prevValue:any = JsonPathSelect.getValueAtPath(this.model.doc, path);
|
|
180
|
+
if (prevValue !== undefined) {
|
|
181
|
+
this.ops.push(Json1.replaceOp(path, prevValue, value));
|
|
182
|
+
} else {
|
|
183
|
+
this.ops.push(Json1.insertOp(path, value));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public applyRichText(
|
|
188
|
+
path:JsonPath,
|
|
189
|
+
deltaOps:DeltaOp[]
|
|
190
|
+
):void {
|
|
191
|
+
this.ops.push(Json1.editOp(path, RichText.type, deltaOps));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public commit():void {
|
|
195
|
+
const batch = this;
|
|
196
|
+
if (batch.ops.length > 0) {
|
|
197
|
+
// Update the timestamp of the model (omitting local-time for Manifest objects (which are not Content)).
|
|
198
|
+
this.setPathValue([ 'meta', 'updated_at', 'utc_time' ], batch.time.utc_time);
|
|
199
|
+
if (batch.model instanceof Content) {
|
|
200
|
+
this.setPathValue([ 'meta', 'updated_at', 'local_time' ], batch.time.local_time);
|
|
201
|
+
}
|
|
202
|
+
const reduced = batch.ops.reduce(Json1.type.compose, null);
|
|
203
|
+
const shareSync = ShareSyncFactory.get();
|
|
204
|
+
batch.model.doc.submitOp(reduced, (error:any) => {
|
|
205
|
+
if (error) {
|
|
206
|
+
shareSync.onOperationError(error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
if (batch.model instanceof Content) {
|
|
210
|
+
(batch.model as Content).modelUpdated();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getPatch, Patch, PatchItem } from "fast-array-diff";
|
|
2
|
+
|
|
3
|
+
export type TextEditRetainOp = number;
|
|
4
|
+
export type TextEditInsertOp = string;
|
|
5
|
+
export interface TextEditDeleteOp {
|
|
6
|
+
d:number|String
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type TextEditOp = TextEditRetainOp | TextEditInsertOp | TextEditDeleteOp;
|
|
10
|
+
|
|
11
|
+
export class TextEditOps {
|
|
12
|
+
public static between(before:string, after:string):TextEditOp[] {
|
|
13
|
+
let ops = [] as TextEditOp[];
|
|
14
|
+
if (before.length === 0 && after.length === 0) {
|
|
15
|
+
return ops;
|
|
16
|
+
} else if (before.length === 0) {
|
|
17
|
+
ops.push(after);
|
|
18
|
+
} else if (after.length === 0) {
|
|
19
|
+
ops.push({ d : before });
|
|
20
|
+
} else {
|
|
21
|
+
// First, we split the string into unicode characters. We use this method of splitting (regex with 'u' flag)
|
|
22
|
+
// in order to split on actual unicode characters, rather than merely on bytes. Even the String.charAt()
|
|
23
|
+
// function can't be trusted to handle multi-byte characters correctly (e.g., emojis). For more info, read here:
|
|
24
|
+
// https://stackoverflow.com/questions/35223206/how-to-split-unicode-string-to-characters-in-javascript
|
|
25
|
+
const beforeChars = before.match(/./ug);
|
|
26
|
+
const afterChars = after.match(/./ug);
|
|
27
|
+
// Create a diff-patch between these two character arrays.
|
|
28
|
+
let patch:Patch<string> = getPatch(beforeChars, afterChars);
|
|
29
|
+
// console.log(JSON.stringify(patch));
|
|
30
|
+
let cursor = 0;
|
|
31
|
+
// Iterate through the patch items, and use them to construct OT operations, in the format expected by ShareDB.
|
|
32
|
+
for (let i = 0; i < patch.length; i++) {
|
|
33
|
+
const item:PatchItem<string> = patch[i];
|
|
34
|
+
const chunk = item.items.join('');
|
|
35
|
+
if (item.oldPos > cursor) {
|
|
36
|
+
const charsToRetain = item.oldPos - cursor;
|
|
37
|
+
ops.push(charsToRetain);
|
|
38
|
+
cursor += charsToRetain;
|
|
39
|
+
}
|
|
40
|
+
if (item.type == 'add') {
|
|
41
|
+
ops.push(chunk);
|
|
42
|
+
} else if (item.type == 'remove') {
|
|
43
|
+
ops.push({ d : chunk });
|
|
44
|
+
cursor += chunk.length;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return ops;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export abstract class ConnectionListener {
|
|
2
|
+
|
|
3
|
+
private _onSocketEvent:(eventName:string, details:any) => void;
|
|
4
|
+
|
|
5
|
+
public constructor(
|
|
6
|
+
onSocketEvent:(eventName:string, details:any) => void
|
|
7
|
+
) {
|
|
8
|
+
this._onSocketEvent = onSocketEvent;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public onSocketEvent(eventName:string, details:any):void {
|
|
12
|
+
this._onSocketEvent(eventName, details);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class NoOpConnectionListener extends ConnectionListener {
|
|
17
|
+
|
|
18
|
+
public constructor() {
|
|
19
|
+
super(NoOpConnectionListener.onSocketEvent);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public static onSocketEvent(eventName:string, details:any):void {
|
|
23
|
+
// DO NOTHING
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as Json1 from 'ot-json1';
|
|
2
|
+
|
|
3
|
+
export * from 'ot-json1';
|
|
4
|
+
|
|
5
|
+
// Export a version of ot-json1 that allows various conflicts
|
|
6
|
+
export const type = {
|
|
7
|
+
...Json1.type.typeAllowingConflictsPred( (conflict) => {
|
|
8
|
+
return (
|
|
9
|
+
Json1.type.DROP_COLLISION === conflict.type
|
|
10
|
+
|| Json1.type.BLACKHOLE === conflict.type
|
|
11
|
+
|| Json1.type.RM_UNEXPECTED_CONTENT === conflict.type
|
|
12
|
+
)
|
|
13
|
+
})
|
|
14
|
+
};
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import ShareDB, { Connection, Doc } from '@shaxpir/sharedb/lib/client';
|
|
2
|
+
import ReconnectingWebSocket from 'reconnecting-websocket';
|
|
3
|
+
import { Billing } from "../models/content/Billing";
|
|
4
|
+
import { Content, ContentBody, ContentId, ContentRef } from '../models/content/Content';
|
|
5
|
+
import { ContentKind } from "../models/content/ContentKind";
|
|
6
|
+
import { Device } from '../models/content/Device';
|
|
7
|
+
import { Manifest, ManifestBody } from '../models/content/Manifest';
|
|
8
|
+
import { Model } from '../models/content/Model';
|
|
9
|
+
import { Profile } from '../models/content/Profile';
|
|
10
|
+
import { CompactDateTime } from '../models/content/Time';
|
|
11
|
+
import { ModernUser } from '../models/content/User';
|
|
12
|
+
import { ConnectionListener } from './ConnectionListener';
|
|
13
|
+
|
|
14
|
+
// Register the ShareDB types
|
|
15
|
+
import { Encryption } from '../util/Encryption';
|
|
16
|
+
import * as Json1 from './PermissiveJson1';
|
|
17
|
+
|
|
18
|
+
const RichText = require('@shaxpir/rich-text');
|
|
19
|
+
const UnicodeText = require('ot-text-unicode');
|
|
20
|
+
|
|
21
|
+
ShareDB.types.register(Json1.type);
|
|
22
|
+
ShareDB.types.register(RichText.type);
|
|
23
|
+
ShareDB.types.register(UnicodeText.type);
|
|
24
|
+
|
|
25
|
+
Json1.type.registerSubtype(RichText.type);
|
|
26
|
+
Json1.type.registerSubtype(UnicodeText.type);
|
|
27
|
+
|
|
28
|
+
export enum ShareSyncDisposalStrategy {
|
|
29
|
+
UNSUBSCRIBE,
|
|
30
|
+
DESTROY,
|
|
31
|
+
NOTHING
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ShareSyncOptions {
|
|
35
|
+
namespace:string;
|
|
36
|
+
debug:boolean;
|
|
37
|
+
disposalStrategy:ShareSyncDisposalStrategy;
|
|
38
|
+
connectionListener:ConnectionListener;
|
|
39
|
+
urlProvider:() => Promise<string>;
|
|
40
|
+
opErrorCallback:(error?:any) => void;
|
|
41
|
+
onReadyCallback?:() => void;
|
|
42
|
+
pingInterval?:number;
|
|
43
|
+
encryptionKey?:string;
|
|
44
|
+
webSocketConstructor?:any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ShareSyncFactory {
|
|
48
|
+
|
|
49
|
+
private static INSTANCE:ShareSync = null;
|
|
50
|
+
|
|
51
|
+
public static initialize(
|
|
52
|
+
encryption:Encryption,
|
|
53
|
+
options:ShareSyncOptions
|
|
54
|
+
) {
|
|
55
|
+
ShareSyncFactory.INSTANCE = new ShareSync(encryption, options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public static get():ShareSync {
|
|
59
|
+
return ShareSyncFactory.INSTANCE;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class ShareSync {
|
|
64
|
+
|
|
65
|
+
private _debug:boolean;
|
|
66
|
+
private _disposalStrategy:ShareSyncDisposalStrategy;
|
|
67
|
+
private _opErrorCallback:(error?:any) => void;
|
|
68
|
+
|
|
69
|
+
private _modelCache:Map<string, Model> = new Map();
|
|
70
|
+
|
|
71
|
+
private _encryption:Encryption;
|
|
72
|
+
private _hasDurableStore:boolean;
|
|
73
|
+
private _useDurableStoreEncryption:boolean;
|
|
74
|
+
private _durableStoreEncryptionKey:string;
|
|
75
|
+
private _socketIsOpen:boolean;
|
|
76
|
+
private _socket:ReconnectingWebSocket;
|
|
77
|
+
private _connection:Connection;
|
|
78
|
+
private _listener:ConnectionListener;
|
|
79
|
+
|
|
80
|
+
public constructor(
|
|
81
|
+
encryption:Encryption,
|
|
82
|
+
options:ShareSyncOptions
|
|
83
|
+
) {
|
|
84
|
+
const shareSync = this;
|
|
85
|
+
shareSync._encryption = encryption;
|
|
86
|
+
shareSync._debug = options.debug;
|
|
87
|
+
shareSync._disposalStrategy = options.disposalStrategy;
|
|
88
|
+
shareSync._opErrorCallback = options.opErrorCallback;
|
|
89
|
+
shareSync._hasDurableStore = options.namespace && options.namespace.length > 0;
|
|
90
|
+
shareSync._useDurableStoreEncryption = options.encryptionKey && options.encryptionKey.length > 0;
|
|
91
|
+
shareSync._durableStoreEncryptionKey = options.encryptionKey;
|
|
92
|
+
|
|
93
|
+
// Create and configure the socket.
|
|
94
|
+
if (options.webSocketConstructor) {
|
|
95
|
+
shareSync._socket = new ReconnectingWebSocket(options.urlProvider, [], { WebSocket : options.webSocketConstructor });
|
|
96
|
+
} else {
|
|
97
|
+
shareSync._socket = new ReconnectingWebSocket(options.urlProvider);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function updateSocketIsOpen() {
|
|
101
|
+
shareSync._socketIsOpen = shareSync._socket.readyState === ReconnectingWebSocket.OPEN;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The listener should forward all events: open, message, close, error
|
|
105
|
+
shareSync._listener = options.connectionListener;
|
|
106
|
+
shareSync._socket.addEventListener('open', (event:Event) => {
|
|
107
|
+
updateSocketIsOpen();
|
|
108
|
+
shareSync._listener.onSocketEvent('open', event);
|
|
109
|
+
});
|
|
110
|
+
shareSync._socket.addEventListener('message', (event:Event) => {
|
|
111
|
+
updateSocketIsOpen();
|
|
112
|
+
shareSync._listener.onSocketEvent('message', event);
|
|
113
|
+
});
|
|
114
|
+
shareSync._socket.addEventListener('close', (event:CloseEvent) => {
|
|
115
|
+
updateSocketIsOpen();
|
|
116
|
+
shareSync._listener.onSocketEvent('close', event);
|
|
117
|
+
});
|
|
118
|
+
shareSync._socket.addEventListener('error', (event:ErrorEvent) => {
|
|
119
|
+
updateSocketIsOpen();
|
|
120
|
+
shareSync._listener.onSocketEvent('error', event);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Create the ShareDB connection from the socket
|
|
124
|
+
if (shareSync._hasDurableStore) {
|
|
125
|
+
shareSync._connection = new Connection(
|
|
126
|
+
shareSync._socket,
|
|
127
|
+
{
|
|
128
|
+
durableStore : {
|
|
129
|
+
namespace : options.namespace,
|
|
130
|
+
debug : options.debug,
|
|
131
|
+
useEncryption : shareSync._useDurableStoreEncryption,
|
|
132
|
+
encryptionCallback : (plaintext:string):string => {
|
|
133
|
+
return encryption.encrypt(plaintext, shareSync._durableStoreEncryptionKey);
|
|
134
|
+
},
|
|
135
|
+
decryptionCallback : (cyphertext:string):string => {
|
|
136
|
+
return encryption.decrypt(cyphertext, shareSync._durableStoreEncryptionKey);
|
|
137
|
+
},
|
|
138
|
+
onReadyCallback : options.onReadyCallback,
|
|
139
|
+
opErrorCallback : options.opErrorCallback,
|
|
140
|
+
// Use the 'docData.meta.updated_at.utc_time' field (which is universal to all Model subclasses) as the 'version'
|
|
141
|
+
// identifier that we use when determining if the doc in the DurableStore is up-to-date. This field won't yet
|
|
142
|
+
// exist when the doc is newly created (since the 'doc.data' will be undefined), so in that case, we'll return
|
|
143
|
+
// null as the version string, which will work correctly with the versioning semantics of the DurableStore.
|
|
144
|
+
extVersionDecoder : (docData:any):string => {
|
|
145
|
+
if (
|
|
146
|
+
docData &&
|
|
147
|
+
docData.meta &&
|
|
148
|
+
docData.meta.updated_at &&
|
|
149
|
+
docData.meta.updated_at.utc_time
|
|
150
|
+
) {
|
|
151
|
+
return docData.meta.updated_at.utc_time;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
shareSync._connection = new Connection(shareSync._socket);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
shareSync._connection.on('error', (err:any) => {
|
|
163
|
+
console.error(`ShareSync connection error: ${err}\n Error code: ${err.code}\n Error message: ${err.message}\n Error stack: ${err.stack}\n`);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// If the client provided a pingInterval option, then set up a periodic ping the
|
|
167
|
+
// connection alive. Clients who don't provide this option will send pings.
|
|
168
|
+
if (options.pingInterval && typeof(options.pingInterval) === 'number') {
|
|
169
|
+
setInterval(() => { shareSync.ping() }, options.pingInterval);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!shareSync._hasDurableStore && !!options.onReadyCallback) {
|
|
173
|
+
// If there is no DurableStore, then we can call the onReadyCallback now. However,
|
|
174
|
+
// we have to use setTimeout to invoke it, because the callback will probably want
|
|
175
|
+
// to retrieve the ShareSync instance from the factory, and because the constructor
|
|
176
|
+
// hasn't actually finished yet, the INSTANCE pointer hasn't quite been assigned yet.
|
|
177
|
+
setTimeout(options.onReadyCallback, 1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public isDebug():boolean {
|
|
182
|
+
return this._debug;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public isOnline():boolean {
|
|
186
|
+
return this._socketIsOpen;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public ping():void {
|
|
190
|
+
try {
|
|
191
|
+
(this._connection as any).ping();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// Ignore the error. We aren't directly concerned with the connection status.
|
|
194
|
+
// We just need to provoke the connection to attempt sending a ping message,
|
|
195
|
+
// so that if it fails to send that message, our normal socket-event listeners
|
|
196
|
+
// will notice the change in status, and trigger our downstream logic for
|
|
197
|
+
// displaying the offline status to the user.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public makeContentId():ContentId {
|
|
202
|
+
return this._encryption.getRandomString(Content.ID_LENGTH) as ContentId;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public async exists(userId:ContentId, kind:ContentKind, ref:ContentRef):Promise<boolean> {
|
|
206
|
+
// Check to see if this item exists in the model cache
|
|
207
|
+
const compoundKey = `${kind}/${ref}`;
|
|
208
|
+
if (this._modelCache.has(compoundKey)) {
|
|
209
|
+
const model = this._modelCache.get(compoundKey);
|
|
210
|
+
return model.exists();
|
|
211
|
+
}
|
|
212
|
+
// Check to see if this item exists in the durable-store inventory
|
|
213
|
+
const existsInDurableStore = this._connection.isDocInInventory(kind, ref as string);
|
|
214
|
+
if (existsInDurableStore) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
// Check to see if this item is recorded in the manifest.
|
|
218
|
+
const manifestId:ContentId = Manifest.makeManifestId(userId);
|
|
219
|
+
const manifest = this.load(ContentKind.MANIFEST, manifestId) as Manifest;
|
|
220
|
+
await manifest.acquire();
|
|
221
|
+
const existsInManifest = manifest.doesContentExist(kind, ref);
|
|
222
|
+
manifest.release();
|
|
223
|
+
if (existsInManifest) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
// Fetch this model from from the remote ShareDB server.
|
|
227
|
+
const model = this.load(kind, ref);
|
|
228
|
+
await model.acquire();
|
|
229
|
+
const exists = model.exists();
|
|
230
|
+
model.release();
|
|
231
|
+
return exists;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public createManifest(body:ManifestBody):Manifest {
|
|
235
|
+
const shareSync = this;
|
|
236
|
+
// Create or retrieve the doc object for this manifest model.
|
|
237
|
+
const manifestId = Manifest.makeManifestId(body.meta.owner);
|
|
238
|
+
const doc = shareSync._connection.get(ContentKind.MANIFEST, manifestId);
|
|
239
|
+
doc.create(
|
|
240
|
+
body, Json1.type.uri, (createErr:any) => {
|
|
241
|
+
if (createErr) {
|
|
242
|
+
console.log(`error creating manifest ${manifestId}: ${createErr}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
shareSync.isDebug() && console.log(`created manifest ${manifestId}`);
|
|
247
|
+
const manifest = shareSync.wrap(ContentKind.MANIFEST, doc, true) as Manifest;
|
|
248
|
+
this._modelCache.set(manifest.compoundKey, manifest);
|
|
249
|
+
return manifest;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public createContent(body:ContentBody):Content {
|
|
253
|
+
const shareSync = this;
|
|
254
|
+
// Create or retrieve the doc object for this content model.
|
|
255
|
+
const compoundKey = `${body.meta.kind}/${body.meta.ref}`;
|
|
256
|
+
const doc = shareSync._connection.get(body.meta.kind, body.meta.ref);
|
|
257
|
+
doc.create(
|
|
258
|
+
body, Json1.type.uri, (createErr:any) => {
|
|
259
|
+
if (createErr) {
|
|
260
|
+
console.log(`error creating content ${compoundKey}: ${createErr}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
shareSync.isDebug() && console.log(`created content ${compoundKey}`);
|
|
265
|
+
const content = shareSync.wrap(body.meta.kind, doc, true) as Content;
|
|
266
|
+
this._modelCache.set(compoundKey, content);
|
|
267
|
+
this.updateManifestForContent(content);
|
|
268
|
+
return content;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public async acquire(kind:ContentKind, id:ContentId, timestamp?:CompactDateTime):Promise<Model> {
|
|
272
|
+
let model = this.load(kind, id);
|
|
273
|
+
if (timestamp) {
|
|
274
|
+
model = await model.acquire(timestamp);
|
|
275
|
+
} else {
|
|
276
|
+
model = await model.acquire();
|
|
277
|
+
}
|
|
278
|
+
return model;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
public load(kind:ContentKind, id:ContentId):Model {
|
|
282
|
+
// First, try getting the content from the cache
|
|
283
|
+
const compoundKey = `${kind}/${id}`;
|
|
284
|
+
if (this._modelCache.has(compoundKey)) {
|
|
285
|
+
return this._modelCache.get(compoundKey);
|
|
286
|
+
}
|
|
287
|
+
// If not in the cache, get the model doc from the connection.
|
|
288
|
+
// Then wrap it and put it in the cache for future reference.
|
|
289
|
+
const doc = this._connection.get(kind, id);
|
|
290
|
+
const model = this.wrap(kind, doc, false);
|
|
291
|
+
this._modelCache.set(compoundKey, model);
|
|
292
|
+
return model;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public async findAndAcquire(kind:ContentKind, query:any):Promise<Content[]> {
|
|
296
|
+
const shareSync = this;
|
|
297
|
+
return new Promise((resolve, reject) => {
|
|
298
|
+
shareSync._connection.createFetchQuery(kind, query, {}, (error, results) => {
|
|
299
|
+
if (error) {
|
|
300
|
+
const message = `query error (${error.code}): ${error.message}`;
|
|
301
|
+
console.log(message);
|
|
302
|
+
reject(message);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const contents:Content[] = [];
|
|
306
|
+
for (let i = 0; i < results.length; i++) {
|
|
307
|
+
const doc:Doc = results[i];
|
|
308
|
+
if (doc.type) {
|
|
309
|
+
const compoundKey = `${doc.collection}/${doc.id}`;
|
|
310
|
+
if (shareSync._modelCache.has(compoundKey)) {
|
|
311
|
+
const content = shareSync._modelCache.get(compoundKey) as Content;
|
|
312
|
+
content.acquire();
|
|
313
|
+
contents.push(content);
|
|
314
|
+
} else {
|
|
315
|
+
// NOTE: the wrap method calls 'acquire' on the model, so we don't need to do it here.
|
|
316
|
+
const content = shareSync.wrap(kind, doc, true) as Content;
|
|
317
|
+
shareSync._modelCache.set(compoundKey, content);
|
|
318
|
+
contents.push(content);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
resolve(contents);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
public async updateManifestForContent(content:Content):Promise<void> {
|
|
328
|
+
// TODO: how will we handle manifest updates in a sharing scenario?
|
|
329
|
+
// If user A owns a book, and gives B permission to edit that book, and then B makes some
|
|
330
|
+
// changes to the book. We want B to be able to update their own manifest, but not the
|
|
331
|
+
// manifest belonging to A. But then how will A know that the book has been updated?
|
|
332
|
+
this.isDebug() && console.log(`updating manifest for content: ${content.compoundKey}`);
|
|
333
|
+
content.acquire();
|
|
334
|
+
const manifestId:ContentId = Manifest.makeManifestId(content.owner);
|
|
335
|
+
const manifest = this.load(ContentKind.MANIFEST, manifestId) as Manifest;
|
|
336
|
+
await manifest.acquire();
|
|
337
|
+
manifest.update(content);
|
|
338
|
+
manifest.release();
|
|
339
|
+
content.release();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public forEachOfflinePendingModel(
|
|
343
|
+
callback:(kind:ContentKind, id:ContentId) => void
|
|
344
|
+
):void {
|
|
345
|
+
this._connection.forEachPendingDocCollectionId(callback);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
public isDocInInventory(kind:ContentKind, ref:ContentRef, minVersion?:CompactDateTime):boolean {
|
|
349
|
+
// Check to see if this doc is in the DurableStore inventory. And if so, does it have a `minVersion`
|
|
350
|
+
// greater than or equal to the supplied `minVersion`. In all shaxpir models, the `minVersion` is
|
|
351
|
+
// extracted from the `meta.updated_at.utc_time` field.
|
|
352
|
+
return this._connection.isDocInInventory(kind as string, ref as string, minVersion as string);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public dispose(model:Model):void {
|
|
356
|
+
if (this._disposalStrategy == ShareSyncDisposalStrategy.NOTHING) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// Remove the model from the cache
|
|
360
|
+
const compoundKey = model.compoundKey;
|
|
361
|
+
this._modelCache.delete(compoundKey);
|
|
362
|
+
// Dispose of the model's doc object
|
|
363
|
+
if (this._disposalStrategy == ShareSyncDisposalStrategy.DESTROY) {
|
|
364
|
+
model.disposeAndDestroy();
|
|
365
|
+
} else if (this._disposalStrategy == ShareSyncDisposalStrategy.UNSUBSCRIBE) {
|
|
366
|
+
model.disposeAndUnsubscribe();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public onOperationError(opError:any):void {
|
|
371
|
+
this._opErrorCallback && this._opErrorCallback(opError);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private wrap(kind:ContentKind, doc:Doc, shouldAcquire:boolean):Model {
|
|
375
|
+
const shareSync = this;
|
|
376
|
+
if (kind === ContentKind.MANIFEST) {
|
|
377
|
+
return new Manifest(doc, shouldAcquire, shareSync);
|
|
378
|
+
} else if (kind === ContentKind.USER) {
|
|
379
|
+
return new ModernUser(doc, shouldAcquire, shareSync);
|
|
380
|
+
} else if (kind === ContentKind.DEVICE) {
|
|
381
|
+
return new Device(doc, shouldAcquire, shareSync);
|
|
382
|
+
} else if (kind === ContentKind.BILLING) {
|
|
383
|
+
return new Billing(doc, shouldAcquire, shareSync);
|
|
384
|
+
} else if (kind === ContentKind.PROFILE) {
|
|
385
|
+
return new Profile(doc, shouldAcquire, shareSync);
|
|
386
|
+
}
|
|
387
|
+
throw new Error(`can't wrap content ${kind}/${doc.id}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
}
|