@aerogel/plugin-local-first 0.0.0 → 0.1.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.
Files changed (35) hide show
  1. package/dist/aerogel-plugin-local-first.d.ts +48 -17
  2. package/dist/aerogel-plugin-local-first.js +1219 -1232
  3. package/dist/aerogel-plugin-local-first.js.map +1 -1
  4. package/package.json +5 -5
  5. package/src/components/Account.vue +9 -0
  6. package/src/components/AccountLoggedIn.vue +85 -0
  7. package/src/components/AccountLoggedOut.vue +26 -0
  8. package/src/components/AccountLoginModal.vue +36 -0
  9. package/src/components/AccountModal.vue +206 -0
  10. package/src/components/index.ts +5 -3
  11. package/src/components/settings/PurgeData.vue +43 -0
  12. package/src/components/settings/index.ts +10 -0
  13. package/src/index.css +1 -0
  14. package/src/index.ts +5 -1
  15. package/src/jobs/Migrate.test.ts +1 -1
  16. package/src/jobs/Migrate.ts +8 -1
  17. package/src/jobs/Sync.test.ts +313 -35
  18. package/src/jobs/Sync.ts +183 -77
  19. package/src/jobs/mixins/LoadsChildren.ts +48 -31
  20. package/src/jobs/mixins/LoadsTypeIndex.ts +7 -5
  21. package/src/jobs/mixins/TracksLocalModels.ts +4 -4
  22. package/src/lib/mirroring.ts +45 -21
  23. package/src/services/Cloud.ts +84 -25
  24. package/src/services/DocumentsCache.ts +6 -2
  25. package/src/testing/cloud.ts +1 -1
  26. package/src/testing/setup.ts +3 -2
  27. package/src/testing/stubs/Person.schema.ts +9 -0
  28. package/src/testing/stubs/Person.ts +18 -0
  29. package/src/testing/stubs/Post.schema.ts +17 -0
  30. package/src/testing/stubs/Post.ts +18 -0
  31. package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js +0 -9
  32. package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map +0 -1
  33. package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js +0 -5
  34. package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js.map +0 -1
  35. package/src/components/AGCloudStatus.vue +0 -31
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@aerogel/plugin-local-first",
3
- "version": "0.0.0",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
7
7
  "types": "./dist/aerogel-plugin-local-first.d.ts",
8
+ "style": "./src/index.css",
8
9
  "default": "./dist/aerogel-plugin-local-first.js"
9
10
  },
10
11
  "files": [
@@ -29,10 +30,9 @@
29
30
  "soukai-solid": "*"
30
31
  },
31
32
  "dependencies": {
32
- "@noeldemartin/solid-utils": "0.6.0-next.cfba421315c47e4aa5a72990714376d9dcd1ffff",
33
- "@noeldemartin/utils": "0.7.0-next.928e77e4d67adf7b9e4e1c5387f321ecfe3b11ca",
34
- "idb": "^8.0.2",
35
- "vue": "^3.3.0"
33
+ "@noeldemartin/solid-utils": "^0.6.1",
34
+ "@noeldemartin/utils": "^0.7.1",
35
+ "idb": "^8.0.2"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@aerogel/core": "*",
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <AccountLoggedIn v-if="$solid.hasLoggedIn()" />
3
+ <AccountLoggedOut v-else />
4
+ </template>
5
+
6
+ <script setup lang="ts">
7
+ import AccountLoggedIn from './AccountLoggedIn.vue';
8
+ import AccountLoggedOut from './AccountLoggedOut.vue';
9
+ </script>
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <div class="relative size-12">
3
+ <button
4
+ class="clickable focus-visible:outline-primary-600 flex size-full rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
5
+ type="button"
6
+ :aria-label="$td('account.open', 'Open account')"
7
+ :title="$td('account.open', 'Open account')"
8
+ @click="$ui.modal(AccountModal)"
9
+ >
10
+ <SolidAvatar class="size-full" />
11
+ <div
12
+ v-if="accountStatus.loading"
13
+ class="absolute -inset-1 animate-spin rounded-full border-2 border-[currentColor_transparent] text-green-500"
14
+ />
15
+ </button>
16
+ <div
17
+ class="pointer-events-none absolute right-0 bottom-0 size-3 rounded-full border-2 border-white"
18
+ :class="accountStatus.classes"
19
+ >
20
+ <span class="sr-only">{{ accountStatus.message }}</span>
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { computed } from 'vue';
27
+ import { Solid, SolidAvatar } from '@aerogel/plugin-solid';
28
+ import { translateWithDefault } from '@aerogel/core';
29
+
30
+ import Cloud from '@aerogel/plugin-local-first/services/Cloud';
31
+ import { CloudStatus } from '@aerogel/plugin-local-first/services/Cloud.state';
32
+
33
+ import AccountModal from './AccountModal.vue';
34
+
35
+ interface AccountStatus {
36
+ classes: string;
37
+ message: string;
38
+ loading?: boolean;
39
+ }
40
+
41
+ const accountStatus = computed((): AccountStatus => {
42
+ if (Cloud.syncing || Solid.loginOngoing) {
43
+ return {
44
+ loading: true,
45
+ classes: 'bg-green-500 sr-only',
46
+ message: getAccountStatusMessage() ?? '',
47
+ };
48
+ }
49
+
50
+ if (Solid.error || Cloud.syncError) {
51
+ return {
52
+ classes: 'bg-red-500',
53
+ message: translateWithDefault('account.error', 'There has been an error connecting with your account.'),
54
+ };
55
+ }
56
+
57
+ if (!Solid.isLoggedIn() || Cloud.dirty || !Cloud.ready) {
58
+ return {
59
+ classes: 'bg-yellow-500',
60
+ message: translateWithDefault(
61
+ 'account.dirty',
62
+ 'There are local changes that haven\'t been synchronized yet.',
63
+ ),
64
+ };
65
+ }
66
+
67
+ return {
68
+ classes: 'bg-green-500',
69
+ message: getAccountStatusMessage() ?? '',
70
+ };
71
+ });
72
+
73
+ function getAccountStatusMessage(): string | undefined {
74
+ switch (Cloud.status) {
75
+ case CloudStatus.Syncing:
76
+ return translateWithDefault('cloud.status.syncing', 'Synchronization in progress');
77
+ case CloudStatus.Migrating:
78
+ return translateWithDefault('cloud.status.migrating', 'Migration in progress');
79
+ case CloudStatus.Online:
80
+ return translateWithDefault('cloud.status.online', 'Online');
81
+ case CloudStatus.Disconnected:
82
+ return translateWithDefault('cloud.status.disconnected', 'Disconnected');
83
+ }
84
+ }
85
+ </script>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <DropdownMenu
3
+ align="start"
4
+ :options="[
5
+ { label: $td('user.logIn', 'Connect account'), click: () => $ui.modal(AccountLoginModal) },
6
+ { label: $td('user.settings', 'Settings'), click: () => $ui.modal(SettingsModal) },
7
+ ]"
8
+ >
9
+ <Button
10
+ size="icon"
11
+ variant="ghost"
12
+ :aria-label="$td('user.menu', 'Configuration')"
13
+ :title="$td('user.menu', 'Configuration')"
14
+ >
15
+ <IconCog class="size-5" />
16
+ </Button>
17
+ </DropdownMenu>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import IconCog from '~icons/zondicons/cog';
22
+
23
+ import { Button, DropdownMenu, SettingsModal } from '@aerogel/core';
24
+
25
+ import AccountLoginModal from './AccountLoginModal.vue';
26
+ </script>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <Modal :title="$td('cloud.logIn.title', 'Connect account')">
3
+ <Markdown
4
+ lang-key="cloud.logIn.info"
5
+ lang-default="Log in to your [Solid account](https://solidproject.org) to keep data safe across devices."
6
+ />
7
+ <Link
8
+ class="text-sm"
9
+ @click="
10
+ $ui.alert(
11
+ $td('cloud.logIn.learnMore', 'What is Solid?'),
12
+ $td(
13
+ 'cloud.logIn.learnMoreMessage',
14
+ 'Solid is a decentralized storage protocol built on top of web standards. ' +
15
+ 'If you want to use this app across devices, ' +
16
+ 'you can use Solid to keep changes in sync. ' +
17
+ 'Even if you use it in a single device, ' +
18
+ 'this will also keep your data secure in case your device is lost.\n\n' +
19
+ 'In any case, Focus works 100% offline and you can connect your Solid POD later on. ' +
20
+ 'If you\'ve never heard of Solid and you\'re just trying things out, ' +
21
+ 'you can keep using it without an account.\n\n' +
22
+ '[Learn more](https://solidproject.org)'
23
+ )
24
+ )
25
+ "
26
+ >
27
+ {{ $td('cloud.logIn.learnMore', 'What is Solid?') }}
28
+ </Link>
29
+ <SolidLogin no-loading class="mt-4" />
30
+ </Modal>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { Link, Markdown, Modal } from '@aerogel/core';
35
+ import { SolidLogin } from '@aerogel/plugin-solid';
36
+ </script>
@@ -0,0 +1,206 @@
1
+ <template>
2
+ <Modal
3
+ ref="$modalRef"
4
+ title-hidden
5
+ close-hidden
6
+ :title="$td('account.title', 'Account')"
7
+ >
8
+ <div class="relative flex items-center rounded-md bg-gray-100 p-4 pr-12">
9
+ <SolidAvatar class="mr-2 size-16 shrink-0" />
10
+ <div class="flex flex-col overflow-hidden">
11
+ <span class="font-semibold">{{ $solid.user?.name ?? $td('user.anonymous', 'Anonymous') }}</span>
12
+ <Link :href="$solid.user?.webId" class="mt-0.5 truncate text-sm font-light text-gray-500">
13
+ {{ $solid.user?.webId }}
14
+ </Link>
15
+ </div>
16
+ <Button
17
+ size="icon"
18
+ variant="ghost"
19
+ class="absolute top-1 right-1 hover:bg-gray-200"
20
+ :aria-label="$td('user.settings', 'Settings')"
21
+ :title="$td('user.settings', 'Settings')"
22
+ @click="$ui.modal(SettingsModal)"
23
+ >
24
+ <IconCog class="size-5" />
25
+ </Button>
26
+ </div>
27
+
28
+ <div class="px-2">
29
+ <div v-if="$cloud.syncing" class="mt-4 flex items-center gap-2">
30
+ <IconRefresh class="mt-0.5 size-6 shrink-0 animate-spin self-start text-green-500" />
31
+ <div class="flex w-full flex-col">
32
+ <Markdown lang-key="cloud.info.syncing" lang-default="Synchronizing..." />
33
+ <ProgressBar v-if="$cloud.syncJob" :job="$cloud.syncJob" filled-class="bg-green-500" />
34
+ </div>
35
+ </div>
36
+ <div v-else-if="$solid.loginOngoing" class="mt-4 flex items-center gap-2">
37
+ <IconRefresh class="mt-0.5 size-6 shrink-0 animate-spin self-start text-green-500" />
38
+ <Markdown lang-key="cloud.info.reconnecting" lang-default="Reconnecting..." />
39
+ </div>
40
+ <div v-else-if="error" class="mt-4 flex items-center gap-2">
41
+ <IconWarning class="mt-0.5 size-6 shrink-0 self-start text-red-500" />
42
+ <div>
43
+ <Markdown :text="errorDescription" />
44
+ <Link
45
+ v-if="showErrorDetails"
46
+ class="text-sm text-red-700 opacity-50 hover:opacity-75 focus-visible:opacity-75"
47
+ @click="$errors.inspect(error)"
48
+ >
49
+ {{ $td('errors.viewDetails', 'View details') }}
50
+ </Link>
51
+ </div>
52
+ </div>
53
+ <div v-else-if="!$cloud.ready" class="mt-4 flex items-center gap-2">
54
+ <IconWarning class="mt-0.5 size-6 shrink-0 self-start text-yellow-500" />
55
+ <Markdown lang-key="cloud.info.setup" lang-default="Your data is not backed up yet." />
56
+ </div>
57
+ <div v-else-if="$cloud.dirty" class="mt-4 flex items-center gap-2">
58
+ <IconWarning class="mt-0.5 size-6 shrink-0 self-start text-yellow-500" />
59
+ <Markdown
60
+ lang-key="cloud.info.changes"
61
+ lang-default="Everything is up to date. | There is one local change. | There are {n} local changes."
62
+ :lang-params="$cloud.localChanges"
63
+ />
64
+ </div>
65
+ <div v-else-if="$cloud.online" class="mt-4 flex items-center gap-2">
66
+ <IconCheckmarkOutline class="mt-0.5 size-6 shrink-0 self-start text-green-500" />
67
+ <Markdown lang-key="cloud.info.changes" lang-default="Everything is up to date." :lang-params="0" />
68
+ </div>
69
+ <div v-else class="mt-4 flex items-center gap-2">
70
+ <IconWarning class="mt-0.5 size-6 shrink-0 self-start text-yellow-500" />
71
+ <Markdown
72
+ lang-key="cloud.info.disconnected"
73
+ lang-default="You are disconnected from your Solid account, all the changes you make will remain in your device."
74
+ />
75
+ </div>
76
+ <AdvancedOptions class="mt-2">
77
+ <ul class="flex flex-col gap-2">
78
+ <li>
79
+ <Checkbox
80
+ v-model="$solid.autoReconnect"
81
+ label-class="text-base"
82
+ :label="$td('cloud.advanced.reconnect', 'Reconnect on startup')"
83
+ />
84
+ </li>
85
+ <li>
86
+ <Checkbox
87
+ v-model="$cloud.startupSync"
88
+ label-class="text-base"
89
+ :label="$td('cloud.advanced.startupSync', 'Synchronize on startup')"
90
+ />
91
+ </li>
92
+ <li>
93
+ <Checkbox v-model="$cloud.pollingEnabled" label-class="text-base">
94
+ <div class="flex items-center">
95
+ <span aria-hidden="true">{{ pollingText[0] }}</span>
96
+ <EditableContent
97
+ type="number"
98
+ :text="`${$cloud.pollingMinutes}`"
99
+ :aria-label="
100
+ $td('cloud.advanced.pollingMinutes', 'Synchronization interval (in minutes)')
101
+ "
102
+ class="focus-within:border-primary-600 mx-1 -mb-px border-b"
103
+ @update="$cloud.pollingMinutes = $event as number"
104
+ >
105
+ {{ $cloud.pollingMinutes }}
106
+ </EditableContent>
107
+ <span v-if="pollingText.length > 1" aria-hidden="true">{{ pollingText[1] }}</span>
108
+ </div>
109
+ </Checkbox>
110
+ </li>
111
+ </ul>
112
+ </AdvancedOptions>
113
+ </div>
114
+
115
+ <div v-if="!$solid.loginOngoing" class="mt-4 flex flex-row-reverse justify-start gap-2">
116
+ <Button
117
+ v-if="$cloud.syncing"
118
+ variant="secondary"
119
+ :disabled="cancellingSync"
120
+ @click="cancelSync()"
121
+ >
122
+ <IconStop class="size-5" />
123
+ <span>{{
124
+ cancellingSync ? $td('cloud.stopping', 'Stopping...') : $td('cloud.stop', 'Stop synchronization')
125
+ }}</span>
126
+ </Button>
127
+ <Button v-else-if="!$solid.isLoggedIn()" @click="$solid.reconnect({ force: true })">
128
+ <IconRefresh class="size-5" />
129
+ <span>{{ $td('cloud.reconnect', 'Reconnect') }}</span>
130
+ </Button>
131
+ <Button v-else-if="$cloud.ready" @click="$cloud.sync({ refreshUserProfile: true })">
132
+ <IconRefresh class="size-5" />
133
+ <span>{{ $td('cloud.sync', 'Synchronize') }}</span>
134
+ </Button>
135
+ <Button v-else @click="(($cloud.setupDismissed = false), $modal?.close())">
136
+ <IconCloudUpload class="size-5" />
137
+ <span>{{ $td('cloud.setup.submit', 'Back up now') }}</span>
138
+ </Button>
139
+ <Button v-if="!$cloud.syncing" variant="secondary" @click="$solid.logout()">
140
+ <IconLogout class="size-5" />
141
+ <span>{{ $cloud.ready ? $td('cloud.logOut', 'Log out') : $td('cloud.disconnect', 'Disconnect') }}</span>
142
+ </Button>
143
+ </div>
144
+ </Modal>
145
+ </template>
146
+
147
+ <script setup lang="ts">
148
+ import IconCheckmarkOutline from '~icons/zondicons/checkmark-outline';
149
+ import IconCloudUpload from '~icons/ic/sharp-cloud-upload';
150
+ import IconCog from '~icons/zondicons/cog';
151
+ import IconLogout from '~icons/material-symbols/logout-rounded';
152
+ import IconRefresh from '~icons/zondicons/refresh';
153
+ import IconStop from '~icons/ic/baseline-stop';
154
+ import IconWarning from '~icons/ion/warning';
155
+
156
+ import {
157
+ AdvancedOptions,
158
+ Button,
159
+ Checkbox,
160
+ EditableContent,
161
+ Link,
162
+ Markdown,
163
+ Modal,
164
+ ProgressBar,
165
+ SettingsModal,
166
+ translateWithDefault,
167
+ useEvent,
168
+ } from '@aerogel/core';
169
+ import { computed, ref, useTemplateRef } from 'vue';
170
+ import { Solid, SolidAvatar } from '@aerogel/plugin-solid';
171
+
172
+ import Cloud from '@aerogel/plugin-local-first/services/Cloud';
173
+
174
+ const $modal = useTemplateRef('$modalRef');
175
+ const cancellingSync = ref(false);
176
+ const pollingText = translateWithDefault('cloud.advanced.polling', 'Synchronize every {minutes} minutes', {
177
+ minutes: '%%separator%%',
178
+ }).split('%%separator%%');
179
+ const error = computed(() => Solid.error ?? Cloud.syncError);
180
+ const errorDescription = computed(() => {
181
+ if (!error.value) {
182
+ return;
183
+ }
184
+
185
+ if (typeof error.value === 'string') {
186
+ return error.value;
187
+ }
188
+
189
+ return translateWithDefault('cloud.info.error', 'There has been an error connecting.');
190
+ });
191
+ const showErrorDetails = computed(() => error.value && typeof error.value !== 'string');
192
+
193
+ async function cancelSync() {
194
+ if (!Cloud.syncJob) {
195
+ return;
196
+ }
197
+
198
+ cancellingSync.value = true;
199
+
200
+ await Promise.race([Cloud.syncJob.cancel(), Cloud.syncJob.completed]);
201
+
202
+ cancellingSync.value = false;
203
+ }
204
+
205
+ useEvent('auth:logout', () => $modal.value?.close());
206
+ </script>
@@ -1,3 +1,5 @@
1
- import AGCloudStatus from './AGCloudStatus.vue';
2
-
3
- export { AGCloudStatus };
1
+ export { default as Account } from './Account.vue';
2
+ export { default as AccountLoggedIn } from './AccountLoggedIn.vue';
3
+ export { default as AccountLoggedOut } from './AccountLoggedOut.vue';
4
+ export { default as AccountLoginModal } from './AccountLoginModal.vue';
5
+ export { default as AccountModal } from './AccountModal.vue';
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <details v-if="!$solid.hasLoggedIn()" class="mt-4">
3
+ <summary>
4
+ <span class="text-base font-semibold">{{ $td('settings.dangerZone', 'Danger zone') }}</span>
5
+ </summary>
6
+ <Button variant="danger" class="mt-4" @click="purgeData()">
7
+ <IconTrash class="size-4" />
8
+ <span class="ml-1">{{ $td('settings.purge', 'Purge data') }}</span>
9
+ </Button>
10
+ </details>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import IconTrash from '~icons/zondicons/trash';
15
+
16
+ import { Button, Storage, UI, translateWithDefault } from '@aerogel/core';
17
+
18
+ import AccountLoginModal from '@aerogel/plugin-local-first/components/AccountLoginModal.vue';
19
+
20
+ async function purgeData(): Promise<void> {
21
+ const confirmed = await UI.confirm(
22
+ translateWithDefault('settings.purgeConfirmTitle', 'Delete everything?'),
23
+ translateWithDefault(
24
+ 'settings.purgeConfirmMessage',
25
+ 'If you proceed, all your data will be deleted forever. ' +
26
+ 'If you want to save it before wiping this device, ' +
27
+ 'you can [connect to your Solid account](#action:connect) first.',
28
+ ),
29
+ {
30
+ acceptVariant: 'danger',
31
+ acceptText: translateWithDefault('settings.purgeConfirmAccept', 'Purge data'),
32
+ actions: { connect: () => UI.modal(AccountLoginModal) },
33
+ },
34
+ );
35
+
36
+ if (!confirmed) {
37
+ return;
38
+ }
39
+
40
+ await UI.closeAllModals();
41
+ await Storage.purge();
42
+ }
43
+ </script>
@@ -0,0 +1,10 @@
1
+ import { defineSettings } from '@aerogel/core';
2
+
3
+ import PurgeData from './PurgeData.vue';
4
+
5
+ export default defineSettings([
6
+ {
7
+ priority: 0,
8
+ component: PurgeData,
9
+ },
10
+ ]);
package/src/index.css ADDED
@@ -0,0 +1 @@
1
+ @source './';
package/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { bootServices } from '@aerogel/core';
1
+ import { App, bootServices } from '@aerogel/core';
2
2
  import type { Plugin } from '@aerogel/core';
3
3
 
4
4
  import Cloud from '@aerogel/plugin-local-first/services/Cloud';
5
5
  import DocumentsCache from '@aerogel/plugin-local-first/services/DocumentsCache';
6
6
 
7
+ import settings from './components/settings';
8
+
7
9
  const services = { $cloud: Cloud };
8
10
 
9
11
  export { Cloud };
@@ -16,6 +18,8 @@ export default function localFirst(): Plugin {
16
18
  return {
17
19
  name: '@aerogel/local-first',
18
20
  async install(app) {
21
+ settings.forEach((setting) => App.addSetting(setting));
22
+
19
23
  await bootServices(app, {
20
24
  ...services,
21
25
  $_documentsCache: DocumentsCache,
@@ -168,7 +168,7 @@ describe('Migrate', () => {
168
168
 
169
169
  async function setupMigration(count: number = 1) {
170
170
  // Arrange - Mint urls
171
- const parentContainerUrl = Solid.requireUser().storageUrls[0];
171
+ const parentContainerUrl = Solid.requireUser().storageUrls[0] + 'tasks/';
172
172
  const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
173
173
  const documentUrls = range(count).map(() => fakeDocumentUrl({ containerUrl })) as [string, ...string[]];
174
174
 
@@ -36,6 +36,12 @@ export default class Migrate extends mixed(BaseJob, [LoadsChildren, LoadsTypeInd
36
36
  });
37
37
  }
38
38
 
39
+ constructor() {
40
+ super();
41
+
42
+ this.localModels = map([], 'url');
43
+ }
44
+
39
45
  protected _models?: SolidModel[];
40
46
 
41
47
  public get models(): SolidModel[] | undefined {
@@ -45,7 +51,8 @@ export default class Migrate extends mixed(BaseJob, [LoadsChildren, LoadsTypeInd
45
51
  public set models(models: SolidModel[] | undefined) {
46
52
  if (!models) {
47
53
  delete this._models;
48
- delete this.localModels;
54
+
55
+ this.localModels = map([], 'url');
49
56
 
50
57
  return;
51
58
  }